找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 156|回复: 0

[翻译] Go语言指南:面向C++程序员的迁移手册

[复制链接]

155

主题

0

回帖

1117

积分

管理员

积分
1117
发表于 2025-4-9 15:18:10 | 显示全部楼层 |阅读模式
    Go语言是一种旨在成为通用系统级编程的语言,类似于C++。本文为有经验的C++开发者梳理Go语言的关键差异,重点讨论差异而非相似性。
    需首要明确的是,精通这两种语言需要截然不同的思维模式。最显著的区别在于:C++的对象模型基于类及其继承体系,而Go的对象模型基于接口(本质上是扁平化的)。因此,C++的设计模式几乎无法直接移植到Go。要高效使用Go,开发者应聚焦于待解决的问题本身,而非尝试复刻C++的实现机制。
    如需更基础的Go语言入门,请参考官方文档《Go语言之旅》《如何编写Go代码》《Go语言实战指南》
    如需Go语言的详细规范说明,请查阅官方《Go语言规范》

概念差异详解
  • Go语言没有类和构造函数或析构函数。Go语言没有类方法、类继承层次结构和虚函数,而是提供了接口(interface),接口将在下文详细讨论。在C++使用模板的地方,Go语言也使用接口。
  • Go语言提供了对分配内存的自动垃圾回收机制。因此,无需(也不可能)显式释放内存。开发者无需担心堆分配与栈分配存储、new与malloc、或delete与delete[]与free的区别。也无需单独管理std::unique_ptr、std::shared_ptr、std::weak_ptr、std::auto_ptr以及普通的非智能“原始”指针。Go语言的运行时系统会替开发者处理这些容易出错的代码。
  • Go语言有指针,但没有指针运算。因此,Go语言的指针更类似于C++的引用。开发者无法使用Go语言的指针变量遍历字符串的字节。切片(slice)将在下文进一步讨论,它满足了大多数指针运算的需求。
  • Go默认启用内存安全保护:指针不能指向任意内存地址,缓冲区溢出会导致程序崩溃而非安全漏洞。通过unsafe包可显式绕过部分安全限制。
  • Go语言中的数组是一等值(first-class value)。当数组作为函数参数使用时,函数接收到的是数组的副本,而不是指向数组的指针。然而,在实际应用中,函数通常使用切片作为参数;切片持有指向底层数组的指针。切片将在下文进一步讨论。
  • 字符串由语言本身提供。字符串为不可变类型,创建后内容不可修改。
  • 哈希表直接由语言提供(称为map),无需第三方库实现。
  • 独立的执行线程以及线程间的通信通道由语言本身提供。这将在下文进一步讨论。
  • 某些类型(如下文讨论的映射和通道)是按引用传递的,而不是按值传递。也就是说,将映射传递给函数时,不会复制映射,如果函数修改了映射,调用者也会看到这些修改。用C++的术语来说,可以将这些类型视为引用类型。
  • Go语言不使用头文件。相反,每个源文件都属于一个定义的包。当一个包定义了一个名称以大写字母开头的对象(类型、常量、变量、函数)时,该对象对导入该包的其他任何文件都是可见的。
  • Go语言不支持隐式类型转换。涉及不同类型的操作需要显式转换(在Go语言中称为“转换”)。这甚至适用于同一底层类型的不同用户定义别名。
  • Go语言不支持函数重载,也不支持用户定义运算符。
  • Go语言不支持const或volatile限定符。Go语言使用nil表示无效指针,而C++使用NULL或简单的0(或在C++11中使用nullptr)。
  • 符合Go语言惯用法的代码使用多返回值来传递错误——一个或多个数据结果加上一个错误代码——而不是使用哨兵值(例如-1)或结构化异常处理(C++的try…catch和throw或Go语言的panic…recover)。


语法特性
    与C++相比,声明的语法是颠倒的。在Go中,先写变量名,再写类型。与C++不同,类型的语法与变量的使用方式并不匹配。类型声明可以很容易地从左到右阅读。(var v1 int → “变量v1是一个整型。”)
  1. //Go                      C++
  2. var v1 int                // int v1;
  3. var v2 string             // const std::string v2;  (approximately)
  4. var v3 [10]int            // int v3[10];
  5. var v4 []int              // int* v4;  (approximately)
  6. var v5 struct { f int }   // struct { int f; } v5;
  7. var v6 *int               // int* v6;  (but no pointer arithmetic)
  8. var v7 map[string]int     // unordered_map<string, int>* v7;  (approximately)
  9. var v8 func(a int) int    // int (*v8)(int a);
复制代码
    声明通常采用“关键字+被声明对象的名称”的形式。关键字是var、func、const或type中的一个。方法声明是一个小例外,因为接收者(receiver)出现在被声明对象的名称之前;请参阅接口的相关讨论。
    此外,还支持在关键字后使用括号包裹的声明序列,例如:
  1. var (
  2.     i int
  3.     m float64
  4. )
复制代码
   在声明函数时,你必须为每个参数提供一个名称,或者不为任何参数提供名称。(也就是说,C++允许void f(int i, int);,但Go不允许类似的func f(i int, int)。)不过,为了方便起见,在Go中你可以将多个具有相同类型的名称分组:
  1. func f(i, j, k int, s, t string)
复制代码
   变量可以在声明时进行初始化。在这种情况下,可以指定类型,但不是必须的。当未指定类型时,变量的类型就是初始化表达式的类型。
  1. var v = *p
复制代码
    另请参阅下文关于常量的讨论。如果变量未显式初始化,则必须指定类型。在这种情况下,它将隐式初始化为该类型的零值(0、nil等)。在Go语言中,不存在未初始化的变量。
    在函数内部,可以使用简短声明语法:=。
  1. v1 := v2 // C++11: auto v1 = v2;
复制代码
   这等价于
  1. var v1 = v2 // C++11: auto v1 = v2;
复制代码
    Go语言允许进行多重赋值,这些赋值是并行完成的。也就是说,首先计算右侧的所有值,然后将这些值赋给左侧的变量。
  1. i, j = j, i // Swap i and j.
复制代码
   函数可以有多个返回值,用括号中的列表表示。返回的值可以通过赋值给变量列表来存储。
  1. func f() (i int, j int) { ... }
  2. v1, v2 = f()
复制代码
   多返回值是Go语言处理错误的主要机制:
  1. result, ok := g()
  2. if !ok {
  3.   // Something bad happened.
  4.   return nil
  5. }
  6. // Continue as normal.
复制代码
   或者,更简洁地表示成
  1. if result, ok := g(); !ok {
  2.   // Something bad happened.
  3.   return nil
  4. }
  5. // Continue as normal.
复制代码
   在实际编写Go代码时,很少使用分号。从技术上讲,Go中的所有语句都以分号结尾。然而,Go会将非空行的末尾视为分号,除非该行明显不完整(具体规则见语言规范)。由此产生的一个结果是,在某些情况下,Go不允许使用换行符。例如,你不能这样写:
  1. func g()
  2. {                  // INVALID
  3. }
复制代码
   在 g() 后会插入一个分号,导致它成为一个函数声明而非函数定义。同理,您也不能这样写
  1. if x {
  2. }
  3. else {             // INVALID
  4. }
复制代码
   在 else 之前的 } 后会插入一个分号,从而导致语法错误。
   
    由于分号确实用于结束语句,你可以像在C++中那样继续使用它们。然而,这并不是推荐的风格。符合Go语言习惯的代码会省略不必要的分号,实际上,除了初始的for循环子句以及你希望将几个简短语句放在同一行的情况外,其他所有分号都可以省略。
    既然谈到了这个话题,我们建议你不必担心分号和花括号的放置问题,而是使用gofmt程序来格式化你的代码。这将生成一种标准的Go语言风格,让你专注于代码本身,而不是代码格式。虽然这种风格一开始可能看起来有些奇怪,但它和其他任何风格一样好,熟悉之后你就会感到舒适。


    当使用指向结构体的指针时,你使用.而不是->。因此,从语法上讲,结构体和指向结构体的指针的使用方式是相同的。
  1. type myStruct struct{ i int }
  2. var v9 myStruct  // v9 has structure type
  3. var p9 *myStruct // p9 is a pointer to a structure
  4. f(v9.i, p9.i)
复制代码
   Go语言在if语句的条件、for语句的表达式或switch语句的值周围不需要加括号。另一方面,它确实要求在if或for语句的主体周围加上花括号。
  1. if a < b { f() }             // Valid
  2. if (a < b) { f() }           // Valid (condition is a parenthesized expression)
  3. if (a < b) f()               // INVALID
  4. for i = 0; i < 10; i++ {}    // Valid
  5. for (i = 0; i < 10; i++) {}  // INVALID
复制代码
   Go语言没有while语句,也没有do/while语句。for语句可以与单个条件一起使用,这使其等同于while语句。完全省略条件则构成一个无限循环。
    Go语言允许break和continue指定一个标签。该标签必须引用一个for、switch或select语句。
    在switch语句中,case标签不会贯穿(即不会自动执行下一个case)。你可以使用fallthrough关键字使它们贯穿,这甚至适用于相邻的case。
  1. switch i {
  2. case 0: // empty case body
  3. case 1:
  4.     f() // f is not called when i == 0!
  5. }
复制代码
   但一个case可以有多个值。   
  1. switch i {
  2. case 0, 1:
  3.     f() // f is called if i == 0 || i == 1.
  4. }
复制代码

    case中的值不必是常量,甚至不必是整数;任何支持相等比较运算符的类型都可以使用,比如字符串或指针;如果省略了switch的值,则默认其为true。
  1. switch {
  2. case i < 0:
  3.     f1()
  4. case i == 0:
  5.     f2()
  6. case i > 0:
  7.     f3()
  8. }
复制代码
   defer语句可用于在包含defer语句的函数返回后调用一个函数。defer在C++中通常起到析构函数的作用,但它与调用代码相关联,而不是与任何特定的类或对象相关联。
  1. fd := open("filename")
  2. defer close(fd) // fd will be closed when this function returns.
复制代码


运算符
    ++和--运算符只能用于语句中,不能用于表达式中。你不能编写c = *p++这样的代码,因为*p++会被解析为(*p)++。
    运算符优先级不同。例如,4 & 3 << 1 在 Go 中求值结果为 0,而在 C++ 中为 4。
  1. Go operator precedence:
  2. 1. *   /   %  <<  >>  &  &^
  3. 2. +   -   |  ^
  4. 3. ==  !=  <  <=  >   >=
  5. 4. &&
  6. 5. ||
复制代码
  1. C++ operator precedence (only relevant operators):
  2. 1.  *    /   %
  3. 2.  +    -
  4. 3.  <<   >>
  5. 4.  <    <=  >   >=
  6. 5.  ==   !=
  7. 6.  &
  8. 7.  ^
  9. 8.  |
  10. 9.  &&
  11. 10. ||
复制代码


常量
    在Go语言中,常量可以是无类型的。这甚至适用于使用const声明命名的常量,如果在声明中没有给出类型,且初始化表达式仅使用无类型常量,那么该常量就是无类型的。当无类型常量在需要类型值的上下文中使用时,其值就会变为有类型的。这允许常量在不需要一般隐式类型转换的情况下相对自由地使用。
  1. var a uint
  2. f(a + 1) // untyped numeric constant "1" becomes typed as uint
复制代码
    该语言对无类型数值常量或常量表达式的大小不施加任何限制。只有在常量被用于需要类型的上下文中时,才会施加限制。
  1. const huge = 1 << 100
  2. f(huge >> 98)
复制代码
   Go语言不支持枚举。不过,你可以在单个const声明中使用特殊名称iota来获取一系列递增的值。当const的初始化表达式被省略时,它会重用前一个表达式。
  1. const (
  2.     red   = iota // red == 0
  3.     blue         // blue == 1
  4.     green        // green == 2
  5. )
复制代码


类型
    C++ 和 Go 提供了相似但不完全相同的内置类型:各种宽度的有符号和无符号整数、32 位和 64 位浮点数(实数和复数)、结构体、指针等。在 Go 中,uint8、int64 等类似命名的整数类型是语言的一部分,而不是基于实现依赖的整数大小(例如 long long)构建的。此外,Go 还提供了原生的字符串、映射(map)和通道(chan)类型,以及一等公民数组和切片(如下所述)。字符串使用 Unicode 编码,而非 ASCII。
    Go 的类型系统比 C++ 严格得多。特别是,Go 中没有隐式类型转换,只有显式类型转换。这提供了额外的安全性和避免了一类错误,但代价是需要编写更多的类型声明代码。此外,Go 中也没有联合类型,因为这可能会破坏类型系统。然而,Go 的 interface{}(见下文)提供了一种类型安全的替代方案。
    C++ 和 Go 都支持类型别名(C++ 中的 typedef,Go 中的 type)。然而,与 C++ 不同的是,Go 将这些视为不同的类型。因此,以下代码在 C++ 中是有效的:
  1. // C++
  2. typedef double position;
  3. typedef double velocity;
  4. position pos = 218.0;
  5. velocity vel = -9.8;
  6. pos += vel;
复制代码
   但在 Go 中,如果没有显式的类型转换,等效的代码则是无效的:
  1. type position float64
  2. type velocity float64
  3. var pos position = 218.0
  4. var vel velocity = -9.8
  5. pos += vel // INVALID: mismatched types position and velocity
  6. // pos += position(vel)  // Valid
复制代码
    即便对于未设置别名的类型也是如此:int 和 uint 不能在表达式中直接组合使用,除非显式地将其中一个转换为另一个类型。
    与 C++ 不同,Go 不允许将指针与整数进行相互转换。然而,Go 的 unsafe 包允许在必要时显式绕过这一安全机制(例如,用于底层系统代码)。


切片
    切片在概念上是一个包含三个字段的结构体:一个指向数组的指针、一个长度和一个容量。切片支持使用 [] 运算符来访问底层数组的元素。内置的 len 函数返回切片的长度。内置的 cap 函数返回切片的容量。
    给定一个数组或另一个切片,可以通过 a[i:j] 创建一个新的切片。这会创建一个新的切片,它引用数组 a,从索引 i 开始,到索引 j 之前结束。它的长度为 j-i。如果省略 i,则切片从索引 0 开始。如果省略 j,则切片在 len(a) 处结束。新切片引用与 a 相同的数组。这句话有两个含义:① 使用新切片所做的更改可以通过 a 看到;② 切片创建(旨在)是廉价的;不需要复制底层数组。新切片的容量只是 a 的容量减去 i。数组的容量就是数组的长度。
    这意味着 Go 在某些情况下使用切片,而 C++ 则使用指针。如果你创建了一个类型为 [100]byte 的值(一个包含 100 个字节的数组,可能是一个缓冲区),并且你想在不复制它的情况下将其传递给一个函数,你应该将函数参数声明为 []byte 类型,并传递数组的一个切片(a[:] 将传递整个数组)。与 C++ 不同,不需要传递缓冲区的长度;它可以通过 len 高效地访问。
    切片语法也可以与字符串一起使用。它返回一个新的字符串,其值是原始字符串的一个子字符串。因为字符串是不可变的,所以字符串切片可以在不为切片内容分配新存储的情况下实现。


值的创建
    Go 有一个内置函数 new,它接受一个类型并在堆上分配空间。为该类型分配的空间将被零值初始化。例如,new(int) 在堆上分配一个新的 int,将其初始化为值 0,并返回其地址,该地址的类型为 *int。与 C++ 不同,new 是一个函数,而不是一个运算符;new int 是一个语法错误。
    也许令人惊讶的是,new 在 Go 程序中并不常用。在 Go 中,取变量的地址总是安全的,并且永远不会产生悬空指针。如果程序取一个变量的地址,必要时它将在堆上分配。因此,以下这些函数是等价的:
  1. type S struct { I int }
  2. func f1() *S {
  3.     return new(S)
  4. }
  5. func f2() *S {
  6.     var s S
  7.     return &s
  8. }
  9. func f3() *S {
  10.     // More idiomatic: use composite literal syntax.
  11.     return &S{}
  12. }
复制代码
   相比之下,在 C++ 中返回指向局部变量的指针是不安全的:
  1. // C++
  2. S* f2() {
  3.   S s;
  4.   return &s;   // INVALID -- contents can be overwritten at any time
  5. }
复制代码
   映射(map)和通道(channel)值必须使用内置函数 make 来分配。声明为映射或通道类型但未初始化的变量将自动初始化为 nil。调用 make(map[int]int) 会返回一个新分配的 map[int]int 类型的值。注意,make 返回的是一个值,而不是一个指针。这与映射和通道值按引用传递的事实是一致的。用映射类型调用 make 时,可以提供一个可选参数,该参数是映射的预期容量。用通道类型调用 make 时,也可以提供一个可选参数,该参数设置通道的缓冲容量;默认值为 0(无缓冲)。
    make 函数也可以用于分配切片。在这种情况下,它会为底层数组分配内存,并返回一个引用该数组的切片。有一个必需参数,即切片中的元素数量。第二个参数是可选的,表示切片的容量。例如,make([]int, 10, 20)。这与 new([20]int)[0:10] 是相同的。由于 Go 使用垃圾回收,因此当没有引用返回的切片时,新分配的数组将在某个时候被丢弃。


接口
    在 C++ 提供类、子类和模板的地方,Go 提供了接口。Go 的接口类似于 C++ 的纯抽象类:一个没有数据成员、所有方法都是纯虚函数的类。然而,在 Go 中,任何提供了接口中命名的方法的类型都可以被视为该接口的实现。不需要显式声明的继承。接口的实现与接口本身是完全分离的。
    方法看起来像一个普通的函数定义,只是它有一个接收者。接收者类似于 C++ 类方法中的 this 指针。
  1. type myType struct{ i int }
  2. func (p *myType) Get() int { return p.i }
复制代码
   这声明了一个与 myType 关联的方法 Get。在函数体中,接收者被命名为 p。
    方法是在命名类型上定义的。如果将值转换为不同的类型,新值将具有新类型的方法,而不是旧类型的方法。
    可以通过声明一个从内置类型派生的新命名类型来在内置类型上定义方法。新类型与内置类型是不同的。
  1. type myInteger int
  2. func (p myInteger) Get() int { return int(p) } // Conversion required.
  3. func f(i int)                {}
  4. var v myInteger
  5. // f(v) is invalid.
  6. // f(int(v)) is valid; int(v) has no defined methods.
复制代码
   给定这个接口:
  1. type myInterface interface {
  2.     Get() int
  3.     Set(i int)
  4. }
复制代码
   我们可以通过添加以下方法使 myType 满足该接口:
  1. func (p *myType) Set(i int) { p.i = i }
复制代码
   现在,任何以 myInterface 作为参数的函数都将接受一个类型为 *myType 的变量。
  1. func GetAndSet(x myInterface) {}
  2. func f1() {
  3.     var p myType
  4.     GetAndSet(&p)
  5. }
复制代码
   换句话说,如果我们把 myInterface 看作是一个 C++ 纯抽象基类,那么为 *myType 定义 Set 和 Get 方法,就使得 *myType 自动继承了 myInterface。一个类型可以满足多个接口。
    匿名字段可以用来实现类似于 C++ 子类的功能。
  1. type myChildType struct {
  2.     myType
  3.     j int
  4. }
  5. func (p *myChildType) Get() int { p.j++; return p.myType.Get() }
复制代码
   这实际上实现了 myChildType 作为 myType 的子类。
  1. func f2() {
  2.     var p myChildType
  3.     GetAndSet(&p)
  4. }
复制代码
   Set 方法实际上是从 myType 继承而来的,因为与匿名字段关联的方法会被提升到成为外围类型的方法。在这种情况下,由于 myChildType 有一个类型为 myType 的匿名字段,因此 myType 的方法也成为 myChildType 的方法。在这个例子中,Get 方法被重写,而 Set 方法被继承。
    这与 C++ 中的子类并不完全相同。当调用匿名字段的方法时,其接收者是字段本身,而不是外围结构体。换句话说,匿名字段上的方法不是虚函数。当需要虚函数的功能时,可以使用接口。
    具有接口类型的变量可以使用一种称为类型断言的特殊结构转换为不同的接口类型。这在运行时动态实现,类似于 C++ 的 dynamic_cast。但与 dynamic_cast 不同的是,这两个接口之间不需要声明任何关系。
  1. type myPrintInterface interface {
  2.     Print()
  3. }
  4. func f3(x myInterface) {
  5.     x.(myPrintInterface).Print() // type assertion to myPrintInterface
  6. }
复制代码
   转换为 myPrintInterface 是完全动态的。只要 x 的动态类型定义了 Print 方法,转换就会成功。
    由于这种转换是动态的,因此它可以用来实现类似于 C++ 模板的泛型编程。这是通过操作最小接口的值来实现的。
  1. type Any interface{}
复制代码
   容器可以用 Any 来编写,但调用者必须使用类型断言来解包,以恢复所包含类型的值。由于类型是动态的而不是静态的,因此没有像 C++ 模板那样内联相关操作的方式。操作在运行时会进行完整的类型检查,但所有操作都会涉及函数调用。
  1. type Iterator interface {
  2.     Get() Any
  3.     Set(v Any)
  4.     Increment()
  5.     Equal(arg Iterator) bool
  6. }
复制代码
   注意,Equal 有一个类型为 Iterator 的参数。这与 C++ 模板的行为不同。请参阅常见问题解答(FAQ)。


函数闭包
    在 C++11 之前的版本中,创建带有隐藏状态的函数的最常见方法是使用“仿函数”(functor)——一个重载了 operator() 的类,使得其实例看起来像函数。例如,以下代码定义了一个 my_transform 函数(它是 STL 的 std::transform 的简化版本),该函数将给定的一元运算符(op)应用于数组(in)的每个元素,并将结果存储在另一个数组(out)中。为了实现前缀和(即,{x[0], x[0]+x[1], x[0]+x[1]+x[2], …}),代码创建了一个仿函数(MyFunctor),该仿函数跟踪运行总数(total),并将此仿函数的一个实例传递给 my_transform。
  1. // C++
  2. #include <iostream>
  3. #include <cstddef>
  4. template <class UnaryOperator>
  5. void my_transform (size_t n_elts, int* in, int* out, UnaryOperator op)
  6. {
  7.   size_t i;
  8.   for (i = 0; i < n_elts; i++)
  9.     out[i] = op(in[i]);
  10. }
  11. class MyFunctor {
  12. public:
  13.   int total;
  14.   int operator()(int v) {
  15.     total += v;
  16.     return total;
  17.   }
  18.   MyFunctor() : total(0) {}
  19. };
  20. int main (void)
  21. {
  22.   int data[7] = {8, 6, 7, 5, 3, 0, 9};
  23.   int result[7];
  24.   MyFunctor accumulate;
  25.   my_transform(7, data, result, accumulate);
  26.   std::cout << "Result is [ ";
  27.   for (size_t i = 0; i < 7; i++)
  28.     std::cout << result[i] << ' ';
  29.   std::cout << "]\n";
  30.   return 0;
  31. }
复制代码
   C++11 增加了匿名(“lambda”)函数,这些函数可以存储在变量中并传递给其他函数。它们还可以选择性地作为闭包使用,这意味着它们可以引用父作用域中的状态。这一特性极大地简化了 my_transform:
  1. // C++11
  2. #include <iostream>
  3. #include <cstddef>
  4. #include <functional>
  5. void my_transform (size_t n_elts, int* in, int* out, std::function<int(int)> op)
  6. {
  7.   size_t i;
  8.   for (i = 0; i < n_elts; i++)
  9.     out[i] = op(in[i]);
  10. }
  11. int main (void)
  12. {
  13.   int data[7] = {8, 6, 7, 5, 3, 0, 9};
  14.   int result[7];
  15.   int total = 0;
  16.   my_transform(7, data, result, [&total] (int v) {
  17.       total += v;
  18.       return total;
  19.     });
  20.   std::cout << "Result is [ ";
  21.   for (size_t i = 0; i < 7; i++)
  22.     std::cout << result[i] << ' ';
  23.   std::cout << "]\n";
  24.   return 0;
  25. }
复制代码
   Go 语言中典型的 my_transform 实现与 C++11 版本非常相似:
  1. package main
  2. import "fmt"
  3. func my_transform(in []int, xform func(int) int) (out []int) {
  4.     out = make([]int, len(in))
  5.     for idx, val := range in {
  6.         out[idx] = xform(val)
  7.     }
  8.     return
  9. }
  10. func main() {
  11.     data := []int{8, 6, 7, 5, 3, 0, 9}
  12.     total := 0
  13.     fmt.Printf("Result is %v\n", my_transform(data, func(v int) int {
  14.         total += v
  15.         return total
  16.     }))
  17. }
复制代码
   (注意,我们选择从 my_transform 返回 out,而不是传递一个 out 给它写入。这是一个风格上的决定;在这方面,代码本可以写得更像 C++ 版本。)
    在 Go 中,函数总是完整的闭包,相当于 C++11 中的 [&]。一个重要的区别是,在 C++11 中,闭包引用作用域已消失的变量是无效的(这可能是由于向上 funarg 引起的——即返回一个引用局部变量的 lambda 的函数)。在 Go 中,这是完全有效的。


并发
    与 C++11 的 std::thread 类似,Go 允许启动在共享地址空间中并发运行的新执行线程。这些线程被称为 goroutine,并使用 go 语句生成。虽然典型的 std::thread 实现会启动重量级的操作系统线程,但 goroutine 是作为轻量级的用户级线程实现的,这些线程会在多个操作系统线程之间进行多路复用。因此,goroutine 的开销(设计上是)很低的,可以在程序中自由使用。
  1. func server(i int) {
  2.     for {
  3.         fmt.Print(i)
  4.         time.Sleep(10 * time.Second)
  5.     }
  6. }
  7. go server(1)
  8. go server(2)
复制代码
   (注意,server 函数中的 for 语句等同于 C++ 中的 while (true) 循环。)
    函数字面量(Go 中实现为闭包)与 go 语句一起使用时可能会很有用。
  1. var g int
  2. go func(i int) {
  3.     s := 0
  4.     for j := 0; j < i; j++ {
  5.         s += j
  6.     }
  7.     g = s
  8. }(1000) // Passes argument 1000 to the function literal.
复制代码
   与 C++11 类似,但与早期版本的 C++ 不同,Go 为非同步内存访问定义了一个内存模型。尽管 Go 在其 sync 包中提供了类似于 std::mutex 的功能,但这并不是在 Go 程序中实现线程间通信和同步的常规方式。相反,Go 线程通常通过消息传递进行通信,这与锁和屏障有着根本的不同。关于这个主题,Go 的口号是:
不要通过共享内存来通信;相反,要通过通信来共享内存。
    也就是说,通道(channel)用于在 goroutine 之间进行通信。任何类型的值(包括其他通道!)都可以通过通道发送。通道可以是无缓冲的或有缓冲的(在通道创建时指定缓冲区长度)。
    通道是一等值;它们可以存储在变量中,并像其他值一样传递给函数或从函数返回。(当传递给函数时,通道是按引用传递的。)通道也是有类型的:一个 chan int 与一个 chan string 是不同的。
    由于通道在 Go 程序中广泛使用,因此它们(设计上)是高效且开销低的。要在通道上发送值,使用 <- 作为二元运算符。要从通道上接收值,使用 <- 作为一元运算符。通道可以在多个发送者和多个接收者之间共享,并保证每个发送的值最多被一个接收者接收。
    以下是一个使用管理函数来控制对单个值的访问的示例。
  1. type Cmd struct {
  2.     Get bool
  3.     Val int
  4. }
  5. func Manager(ch chan Cmd) {
  6.     val := 0
  7.     for {
  8.         c := <-ch
  9.         if c.Get {
  10.             c.Val = val
  11.             ch <- c
  12.         } else {
  13.             val = c.Val
  14.         }
  15.     }
  16. }
复制代码
   在那个示例中,同一个通道被同时用于输入和输出。如果有多个 goroutine 同时与管理器通信,这是不正确的:一个等待管理器响应的 goroutine 可能会接收到来自另一个 goroutine 的请求。一个解决方案是传入一个通道。
  1. type Cmd2 struct {
  2.     Get bool
  3.     Val int
  4.     Ch  chan<- int
  5. }
  6. func Manager2(ch <-chan Cmd2) {
  7.     val := 0
  8.     for {
  9.         c := <-ch
  10.         if c.Get {
  11.             c.Ch <- val
  12.         } else {
  13.             val = c.Val
  14.         }
  15.     }
  16. }
复制代码
   要使用 Manager2,需要给定一个指向它的通道:
  1. func getFromManagedChannel(ch chan<- Cmd2) int {
  2.     myCh := make(chan int)
  3.     c := Cmd2{true, 0, myCh} // Composite literal syntax.
  4.     ch <- c
  5.     return <-myCh
  6. }
  7. func main() {
  8.     ch := make(chan Cmd2)
  9.     go Manager2(ch)
  10.     // ... some code ...
  11.     currentValue := getFromManagedChannel(ch)
  12.     // ... some more code...
  13. }
复制代码



原文链接


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表