(转)Go语言针对C++程序指南
原文地址:http://www.yiibai.com/go/go_cplusplus.html#go_cplusplus
讲述了go语言设计的特别之处,与其他语言的异同,可以更好了解go语言
1. 概念差异
- Go没有支持构造和析构的class类型,也没有继承和虚函数的概念。但是go提供接口interfaces 支持,我们可以把接口看作是C++中模板类似的技术。
- Go提供垃圾内存回收支持。我们没有必要显式释放内存,go的运行时系统会帮我们收集垃圾内存。
- Go中有指针,但是没有指针算术。因此,你不可能通过指针以字节方式来遍历一个字符串。 数组一个普通类型变量。当用数组作为参数调用函数时,将会复制整个数组。当然,Go语言中一般用切片 (slices)代替数组作为参数,切片是建立在底层数组地址之上的,因此传递的是数组的 地址。切片在后面会详细讨论。
- 内建对字符串的支持。并且字符串创建后就不能修改。
- 内建hash表支持,术语叫字典(map)。
- 语言本身提供并发和管道通讯功能,细节在后面会讨论。
- 有少数类型是通过引用传递(字典和管道,将在后面讨论)。也就是说,将字典传递给一个 函数不会复制整个字典,而且函数对字典的修改会影响到函数调用者的字典数据。这和C++中引用 概念类似。
- Go不使用头文件。每个源文件都被定义在特定的包package中,在包中以大写 字母定义的对象(例如类型,常量,变量,函数等)对外是可见的,可以被别的代码导入使用。
- Go不会作隐式类型转换。如果在不同类型之间赋值,必须强制转换类型。
- Go不支持函数重载,也不支持运算符定义。
- Go不支持const和volatile 修饰符。
- Go使用nil表示无效的指针,C++中使用NULL或0 表示空指针。
2. 语法
Go中变量的声明语法和C++相反。定义变量时,先写变量的名字,然后是变量的类型。这样 不会出现像C++中, 类型不能匹配后面所有变量的情况(指针类型)。而且语法清晰,便于 阅读。
Go C++ var v1 int // int v1; var v2 string // const std::string v2; (approximately 近似等价) var v3 [10]int // int v3[10]; var v4 []int // int* v4; (approximately 近似等价) var v5 struct { f int } // struct { int f; } v5; var v6 *int // int* v6; (but no pointer arithmetic 没有指针算术) var v7 map[string]int // unordered_map<string, int>* v7; (approximately 近似等价) var v8 func(a int) int // int (*v8)(int a);
变量的声明通常是从某些关键字开始,例如var, func,const或type。对于类型的专有方法定义, 在变量名前面还要加上对应该方法发类型对象变量,细节清参考discussion of interfaces。
你也可以在关键字后面加括号,这样可以同时定义多个变量。
var ( i int m float )
When declaring a function, you must either provide a name for each parameter or not provide a name for any parameter; you can't omit some names and provide others. You may group several names with the same type:
定义函数的时候,你可以指定每个参数的名字或者不指定任何参数名字,但是你不能只指定部分函数参数的 名字。如果是相邻的参数是相同的类型,也可以统一指定类型。
func f(i, j, k int, s, t string)
对于变量,可以在定时进行初始化。对于这种情况,我们可以省略变量的类型部分,因为Go编译器 可以根据初始化的值推导出变量的类型。
var v = *p
如果变量定义时没有初始化,则必须指定类型。没有显式初始化的变量,会被自动初始化为空的值, 例如0,nil等。Go不存在完全未初始化的变量。
用:=操作符,还有更简短的定义语法:
v1 := v2
和下面语句等价:
var v1 = v2
Go还提供多个变量同时赋值:
i, j = j, i // Swap i and j.
函数也可以返回多个值,多个返回值需要用括号括起来。返回值可以用一个等于符号赋给 多个变量。
func f() (i int, j int) { ... } v1, v2 = f()
Go中使用很少的分号,虽然每个语句之间实际上是用分号分割的。因为,go编译器会在看似 完整的语句末尾自动添加分号(具体细节清参考Go语言手册)。 当然,自动添加分号也可能带来一些问题。例如:
func g() { // INVALID }
在g()函数后面会被自动添加分号,导致函数编译出错。下面的代码也有类似的 问题:
if x { } else { // INVALID }
在第一个花括号}的后面会被自动添加分号,导致else语句 出现语法错误。
分号可以用来分割语句,你仍然可以安装C++的方式来使用分号。不过Go语言中,常常省略不 必要的分号。 只有在 循环语句的初始化部分,或者一行写多个语句的时候才是必须的。
继续前面的问题。我们并不用担心因为花括号的位置导致的编译错误,因此我们可以用 gofmt 来排版程序代码。 gofmt 工具总是可以将代码排版成统一的风格。While the style may initially seem odd, it is as good as any other style, and familiarity will lead to comfort.
当用指针访问结构体的时候,我们用.代替->语法。 因此,用结构体类型和结构体指针类型访问结构体成员的语法是一样的。
type myStruct struct { i int } var v9 myStruct // v9 has structure type var p9 *myStruct // p9 is a pointer to a structure f(v9.i, p9.i)
Go不要求在if语句的条件部分用小括弧,但是要求if后面的代码 部分必须有花括弧。类似的规则也适用于for和switch等语句。
if a < b { f() } // Valid if (a < b) { f() } // Valid (condition is a parenthesized expression) if (a < b) f() // INVALID for i = 0; i < 10; i++ {} // Valid for (i = 0; i < 10; i++) {} // INVALID
Go语言中没有while和do/while循环语句。我们可以用只有一个 条件语句的for来代替while循环。如果省略for 的条件部分,则是一个无限循环。
Go增加了带标号的break 和continue语法。不过标号必须 是针对for,switch或select代码段的。
对于switch语句,case匹配后不会再继续匹配后续的部分。 对于没有任何匹配的情况,可以用fallthrough 语句。
switch i { case 0: // empty case body case 1: f() // f is not called when i == 0! }
case语句还可以带多个值:
switch i { case 0, 1: f() // f is called if i == 0 || i == 1. }
case语句不一定必须是整数或整数常量。如果省略switch的 要匹配的值,那么case可以是任意的条件语言。
switch { case i < 0: f1() case i == 0: f2() case i > 0: f3() }
++ 和 -- 不再是表达式,它们只能在语句中使用。因此, c = *p++ 是错误的。语句 *p++ 的含义也完全不同,在go中等价于 (*p)++ 。
defer可以用于指定函数返回前要执行的语句。
fd := open("filename") defer close(fd) // fd will be closed when this function returns.
3. 常量
Go语言中的常量可以没有固定类型(untyped)。我们可以用const和一个 untyped类型的初始值来 定义untyped常量。如果是untyped常量, 那么常量在使用的时候会根据上下文自动进行隐含的类型转换。 这样,可以更自由的使用untyped常量。
var a uint f(a + 1) // untyped numeric constant "1" becomes typed as uint
untyped类型常量的大小也没有限制。只有在最终使用的地方才有大小的限制。
const huge = 1 << 100 f(huge >> 98)
Go没有枚举类型(enums)。作为代替,可以在一个独立的const区域中使用 iota来生成递增的值。如果const中,常量没有初始值则会 用前面的初始化表达式代替。
const ( red = iota // red == 0 blue // blue == 1 green // green == 2 )
4. Slices(切片)
切片(slice)底层对应类结构体,主要包含以下信息:指向数据的指针,有效数据的数目,和总 的内存空间大小。 切片支持用语法获取底层数组的某个元素。内建的len 方法可以获取切片的长度。内建的cap返回切片的最大容量。
对于一个数组或另一个切片,我们用aI:J?语句再它基础上创建一个新的切片。 这个新创建的切片底层 引用a(数组或之前的另一个切片),从数组的I位置 开始,到数组的J位置结束。新切片的长度是J - I。 新切片的 容量是数组的容量减去切片在数组中的开始位置I。我们还可以将数组的地址直接赋给 切片:s = &a, 这默认是对应整个数组,和这个语句等价:s = a0:len(a)?。
因此,我们在在C++中使用指针的地方用切片来代替。例如,创建一个100?byte类型的 值(100个字节的数组, 或许是做为缓冲用)。但是,在将数组传递给函数的时候不想复制整个数组(go语言 中数组是值,函数参数 传值是复制的),可以将函数参数定一个为byte切片类型, 从而实现只传递数组地址的目的。不过我们 并不需要像C++中那样传递缓存的长度——在Go中它们已经包含 在切片信息中了。
切片还可以应用于字符串。当需要将某个字符串的字串作为你新字符产返回的时候可以用切片代替。 因为go中的字符串是不可修改的,因此使用字符串切片并不需要分配新的内存空间。
5. 构造值对象
Go有一个内建的new函数,用于在堆上为任意类型变量分配一个空间。新分配的 内存会内自动初始化为0。 例如,new(int) 会在堆上分配一个整型大小的空间, 然后初始化为0,然后返回 *int 类型的地址。 和C++中不同的 是,new是一个函数而不是运算符,因此 new int 用法是错误的。
对于字典和管道,必须用内建的make函数分配空间。对于没有初始化的字典或 管道变量,会被自动初始化为nil。 调用make(mapint?int) 返回一个新的字典空间,类型为mapint?int。需要强调的是,make 返回的是值, 而不是指针!与此对应的是,字典和管道是通过引用传递的。对于make 分配字典空间,还可以有一个可选的函数, 用于指定字典的容量。如果是用于创建管道,则可选的参数 对应管道的缓冲大小,默认0表示不缓存。
make函数还可以用于创建切片。这时,会在堆中分配一个不可见的数组,然后返回 对这个数组引用的切片。 对于切片,make函数除了一个指定切片大小的参数外, 还有一个可选的用于指定切片容量的参数(最多有3个参数)。 例如,make(int, 10, 20), 用于创建一个大小是10,容量是20的切片。当然,用new函数也能实现: new(20?int)0:10?。go支持垃圾内存自动回收,因此新分配的内存空间没有 任何切片引用的时候,可能会被自动释放。
6. Interfaces(接口)
C++提供了class,类继承和模板,类似的go语言提供了接口支持。go中的接口和C++中的纯虚 基类 (只有虚函数,没有数据成员)类似。在Go语言中,任何实现了接口的函数的类型,都可以 看作是接口的一个实现。 类型在实现某个接口的时候,不需要显式关联该接口的信息。接口的实现 和接口的定义完全分离了。
类型的方法和普通函数定义类似,只是前面多了一个对象接收者receiver。 对象接受者和C++中的this指针类似。
type myType struct { i int } func (p *myType) get() int { return p.i }
方法get依附于myType类型。myType对象在 函数中对应p。
方法在命名的类型上定义。如果,改变类型的话,那么就是针对新类型的另一个函数了。
如果要在内建类型上定义方法,则需要给内建类型重新指定一个名字,然后在新指定名字的类型上 定义方法。新定义的类型和内建的类型是有区别的。
type myInteger int func (p myInteger) get() int { return int(p) } // Conversion required. func f(i int) { } var v myInteger // f(v) is invalid. // f(int(v)) is valid; int(v) has no defined methods.
把方法抽象到接口:
type myInterface interface { get() int set(i int) }
为了让我们前面定义的myType满足接口,需要再增加一个方法:
func (p *myType) set(i int) { p.i = i }
现在,任何以myInterface类型作为参数的函数,都可以用 *myType 类型传递了。
func getAndSet(x myInterface) {} func f1() { var p myType getAndSet(&p) }
以C++的观点来看,如果把myInterface看作一个纯虚基类,那么实现了 set 和 get方法的 *myType 自动成为 了从myInterface纯虚基类继承的子类了。在Go中,一个类型可以同时 实现多种接口。
使用匿名成员,我们可以模拟C++中类的继承机制。
type myChildType struct { myType; j int } func (p *myChildType) get() int { p.j++; return p.myType.get() }
这里的myChildType可以看作是myType的子类。
func f2() { var p myChildType getAndSet(&p) }
myChildType类型中是有set方法的。在go中,匿名成员的方法 会默认被提升为类型本身的方法。 因为myChildType含有一个myType 类型的匿名成员,因此也就继承了myType中的set方法, 另一个 get方法则相当于被重载了。
不过这和C++也不是完全等价的。当一个匿名方法被调用的时候,方法对应的类型对象 是匿名成员类型, 并不是当前类型!换言之,匿名成员上的方法并不是C++中的虚函数。 如果你需要模拟虚函数机制, 那么可以使用接口。
一个接口类型的变量可以通过接口的一个内建的特殊方法转换为另一个接口类型变量。 这是由运行时库动态完成的, 和C++中的dynamic_cast有些类似。 但是在Go语言中,两个相互转换的接口类型之间并不需要什么信息。
type myPrintInterface interface { print() } func f3(x myInterface) { x.(myPrintInterface).print() // type assertion to myPrintInterface }
向myPrintInterface类型的转换是动态的。它可以工作在底层实现了 print方法的变量上。
因为,这里动态类型转换机制,我们可以用它来模拟实现C++中的模板功能。这里我们需要 定一个最小的接口:
type Any interface { }
该接口可以持有任意类型的数据,但是在使用的时候需要将该接口变量转换为需要的类型。 因为,这里类型转换是动态实现的,因此,没有办法定义像C++中的内联函数。类型的验证 由运行时库完成, 我们可以调用该变量类型支持的所有方法。
type iterator interface { get() Any set(v Any) increment() equal(arg *iterator) bool }
7. Goroutines
Go语言中使用go可以启动一个goroutine。goroutine 和线程的概念类似,和程序共享一个地址空间。
goroutines和支持多路并发草组系统中的协程(coroutines)类似,用户不用关心具体 的实现细节。
func server(i int) { for { print(i) sys.sleep(10) } } go server(1) go server(2)
(需要注意的是server函数中的for循环语句和 C++ while (true)的循环类似。)
Goroutines资源开销小,比较廉价。
go也可以用于启动新定义的内部函数(闭包)为Goroutines。
var g int go func(i int) { s := 0 for j := 0; j < i; j++ { s += j } g = s }(1000) // Passes argument 1000 to the function literal.
8. Channels(管道)
管道可以用于两个goroutines之间的通讯。我们可以用管道传递任意类脂的变量。Go语言中管道是 廉价并且便捷的。 二元操作符 <- 用于向管道发送数据。一元操作符<- 用于从管道接收数据。在函数参数中,管道通过引用传递给函数。
虽然go语言的标准库中提供了互斥的支持,但是我们也可以用一个单一的goroutine提供对变量的 共享操作。 例如,下面的函数用于管理对变量的读写操作。
type cmd struct { get bool; val int } func manager(ch chan cmd) { var val int = 0 for { c := <- ch if c.get { c.val = val ch <- c } else { val = c.val } } }
在这个例子中,管道被同时用于输入和输出。但是当多个goroutines对变量操作时可能导致 问题:对管道的读操作可能读到的是请求命令。解决的方法是将命令和数据分为不同的管道。
type cmd2 struct { get bool; val int; ch <- chan int } func manager2(ch chan cmd2) { var val int = 0 for { c := <- ch if c.get { c.ch <- val } else { val = c.val } } }
这里掩饰了如何使用manager2:
func f4(ch <- chan cmd2) int { myCh := make(chan int) c := cmd2{ true, 0, myCh } // Composite literal syntax. ch <- c return <-myCh }
发表评论: