Go Wiki: GoForCPPProgrammers
Go 是一种系统编程语言,旨在成为一种通用的系统语言,类似于 C++。以下是一些面向经验丰富的 C++ 程序员的 Go 笔记。本文档讨论了 Go 和 C++ 之间的区别,而很少或根本不讨论它们的相似之处。
需要牢记的一个重要观点是,要精通这两种语言,需要一些基本的思维过程差异。最显著的是,C++ 的对象模型基于类和类层次结构,而 Go 的对象模型基于接口(并且本质上是扁平的)。因此,C++ 的设计模式很少能直接照搬到 Go。要有效地使用 Go 编程,人们必须考虑要解决的*问题*,而不是用来解决问题的 C++ 机制。
有关 Go 的更通用介绍,请参阅 Go Tour、如何编写 Go 代码和Effective Go。
有关 Go 语言的详细描述,请参阅 Go 规范。
概念上的差异
- Go 没有带有构造函数或析构函数的类。Go 使用*接口*代替类方法、类继承层次结构和虚函数,接口将在下文详细讨论。接口也用于 C++ 使用模板的场景。
- Go 提供自动内存垃圾回收。无需(也无法)显式释放内存。无需担心堆分配与栈分配存储、
new
与malloc
,或delete
与delete[]
与free
。也无需单独管理std::unique_ptr
、std::shared_ptr
、std::weak_ptr
、std::auto_ptr
以及普通、非智能的“原生”指针。Go 的运行时系统会替程序员处理所有这些易出错的代码。 - Go 具有指针但不支持指针算术。因此,Go 指针更接近 C++ 的引用。不能使用 Go 指针变量来遍历字符串的字节。切片(将在下文进一步讨论)可以满足指针算术的大部分需求。
- Go 默认是“安全”的。指针不能指向任意内存,缓冲区溢出会导致崩溃,而不是安全漏洞。
unsafe
包允许程序员在明确要求时绕过 Go 的一些保护机制。 - Go 中的数组是第一类值。当数组用作函数参数时,函数会接收数组的副本,而不是指向数组的指针。然而,在实际中,函数通常使用切片作为参数;切片包含指向底层数组的指针。切片将在下文进一步讨论。
- 语言本身提供了字符串。创建后,字符串不可更改。
- 语言本身提供了哈希表。它们被称为映射(map)。
- 语言本身提供了独立的执行线程以及它们之间的通信通道。这将在下文进一步讨论。
- 某些类型(映射和通道,将在下文进一步讨论)按引用传递,而不是按值传递。也就是说,将映射传递给函数不会复制映射,如果函数更改了映射,调用者将看到更改。用 C++ 的术语来说,可以认为这些是引用类型。
- Go 不使用头文件。相反,每个源文件都属于一个已定义的*包*。当一个包定义了一个以大写字母开头的名称的对象(类型、常量、变量、函数)时,该对象对导入该包的任何其他文件都是可见的。
- Go 不支持隐式类型转换。混合不同类型的操作需要强制转换(在 Go 中称为转换)。即使是相同底层类型的不同用户定义别名也需要转换。这确保了额外的安全性。
- Go 不支持函数重载,也不支持用户定义的运算符。
- Go 不支持
const
或volatile
限定符。 - Go 在 C++ 使用
NULL
或0
(或 C++11 中的nullptr
)的地方使用nil
表示无效指针。 - 惯用的 Go 使用多个返回值来传达错误——一个或多个数据结果加上一个错误代码——而不是使用哨兵值(例如
-1
)或结构化异常处理(C++ 的try
…catch
和throw
或 Go 的panic
…recover
)。
语法
声明语法与 C++ 相反。先写名称,后写类型。与 C++ 不同,类型的语法与变量的使用方式不匹配。类型声明可以从左到右轻松读取。(var v1 int
→“变量 v1
是一个 int
。”)
//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
之一。方法声明是一个小的例外,其中接收者出现在被声明对象名称之前;请参阅关于接口的讨论。
您还可以使用关键字后跟括号中的一系列声明。
var (
i int
m float64
)
声明函数时,必须为每个参数提供名称,或者不为任何参数提供名称。(也就是说,C++ 允许 void f(int i, int);
,但 Go 不允许类似的 func f(i int, int)
。)然而,为了方便起见,在 Go 中您可以将多个具有相同类型的名称分组。
func f(i, j, k int, s, t string)
声明变量时可以对其进行初始化。执行此操作时,允许指定类型,但不是必需的。未指定类型时,变量的类型是初始化表达式的类型。
var v = *p
另请参阅下文关于常量的讨论。如果变量未显式初始化,则必须指定类型。在这种情况下,它将隐式初始化为类型的零值(0
、nil
等)。Go 中没有未初始化的变量。
在函数内部,可以使用 :=
提供简短的声明语法。
v1 := v2 // C++11: auto v1 = v2;
这等同于
var v1 = v2 // C++11: auto v1 = v2;
Go 允许并行执行多个赋值。也就是说,首先计算右侧的所有值,然后将这些值赋给左侧的变量。
i, j = j, i // Swap i and j.
函数可以有多个返回值,由括号中的列表表示。返回的值可以赋值给变量列表来存储。
func f() (i int, j int) { ... }
v1, v2 = f()
多个返回值是 Go 处理错误的主要机制。
result, ok := g()
if !ok {
// Something bad happened.
return nil
}
// Continue as normal.
…
或者,更简洁地说,
if result, ok := g(); !ok {
// Something bad happened.
return nil
}
// Continue as normal.
…
实际上,Go 代码很少使用分号。严格来说,所有 Go 语句都以分号结尾。然而,Go 将非空行的末尾视为分号,除非该行明显不完整(确切规则请参阅语言规范)。其后果是,在某些情况下 Go 不允许您使用换行符。例如,您不能写
func g()
{ // INVALID
}
将在 g()
之后插入分号,使其成为函数声明而不是函数定义。同样,您也不能写
if x {
}
else { // INVALID
}
将在 else
前面的 }
之后插入分号,导致语法错误。
由于分号确实会结束语句,因此您可以像在 C++ 中一样继续使用它们。然而,这不符合推荐的风格。惯用的 Go 代码省略了不必要的分号,实际上就是除了初始 for
循环子句和您希望在同一行上放置多个短语句的情况之外的所有分号。
既然谈到这个话题,我们建议您不要担心分号和花括号的位置,而是使用 gofmt
程序来格式化您的代码。这将产生一种标准的 Go 风格,让您专注于代码本身,而不是格式化。虽然这种风格起初可能看起来奇怪,但与其他任何风格一样好,并且熟悉了就会感到舒适。
在使用结构体指针时,您使用 .
而不是 ->
。因此,从语法上讲,结构体和结构体指针的使用方式相同。
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
语句的条件、for
语句的表达式或 switch
语句的值周围不需要括号。另一方面,它在 if
或 for
语句的正文中需要花括号。
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
语句。完全省略条件则是一个无限循环。
Go 允许 break
和 continue
指定标签。标签必须引用 for
、switch
或 select
语句。
在 switch
语句中,case
标签不会向下穿透。您可以使用 fallthrough
关键字使其向下穿透。这甚至适用于相邻的 case。
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
值,则默认为 true
。
switch {
case i < 0:
f1()
case i == 0:
f2()
case i > 0:
f3()
}
defer
语句可用于在包含 defer
语句的函数返回后调用一个函数。defer
通常可以替代 C++ 中的析构函数,但它与调用代码相关联,而不是与任何特定的类或对象关联。
fd := open("filename")
defer close(fd) // fd will be closed when this function returns.
运算符
++
和 --
运算符只能用在语句中,不能用在表达式中。不能写 c = *p++
。*p++
被解析为 (*p)++
。
运算符的优先级不同。例如,4 & 3 << 1
在 Go 中求值为 0
,在 C++ 中求值为 4
。
Go operator precedence:
1. * / % << >> & &^
2. + - | ^
3. == != < <= > >=
4. &&
5. ||
C++ operator precedence (only relevant operators):
1. * / %
2. + -
3. << >>
4. < <= > >=
5. == !=
6. &
7. ^
8. |
9. &&
10. ||
常量
在 Go 中,常量可以是*无类型的*。即使是使用 const
声明命名的常量也是如此,如果声明中没有给出类型,并且初始化表达式只使用无类型常量。来自无类型常量的值在需要类型值的上下文中进行转换时会变为有类型的。这允许常量相对自由地使用,而无需普遍的隐式类型转换。
var a uint
f(a + 1) // untyped numeric constant "1" becomes typed as uint
语言不对无类型数值常量或常量表达式的大小施加任何限制。只有当常量用于需要类型的场合时,才会应用限制。
const huge = 1 << 100
f(huge >> 98)
Go 不支持枚举。相反,您可以在单个 const
声明中使用特殊名称 iota
来获得一系列递增的值。当省略 const
的初始化表达式时,它会重用前面的表达式。
const (
red = iota // red == 0
blue // blue == 1
green // green == 2
)
类型
C++ 和 Go 提供类似但又不完全相同的内置类型:各种位数的有符号和无符号整数、32 位和 64 位浮点数(实数和复数)、struct
、指针等。在 Go 中,uint8
、int64
以及类似命名的整数类型是语言的一部分,而不是基于依赖于实现的整数(例如 long long
)。Go 另外还提供原生的 string
、map
和 chan
(通道)类型,以及第一类数组和切片(下文将介绍)。字符串使用 Unicode 编码,而不是 ASCII。
Go 的类型系统比 C++ 严格得多。特别是,Go 中没有隐式类型转换,只有显式类型转换。这提供了额外的安全性和避免了一类错误,但代价是需要更多的输入。Go 中也没有 union
类型,因为这会允许对类型系统进行规避。然而,Go 的 interface{}
(见下文)提供了一种类型安全的选择。
C++ 和 Go 都支持类型别名(C++ 中的 typedef
,Go 中的 type
)。但是,与 C++ 不同,Go 将它们视为不同的类型。因此,以下代码在 C++ 中是有效的
// C++
typedef double position;
typedef double velocity;
position pos = 218.0;
velocity vel = -9.8;
pos += vel;
但如果没有显式类型转换,等效的代码在 Go 中是无效的
type position float64
type velocity float64
var pos position = 218.0
var vel velocity = -9.8
pos += vel // INVALID: mismatched types position and velocity
// 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 中,取变量的地址始终是安全的,并且永远不会产生悬空指针。如果程序获取了变量的地址,它将在必要时在堆上分配。所以这些函数是等效的
type S struct { I int }
func f1() *S {
return new(S)
}
func f2() *S {
var s S
return &s
}
func f3() *S {
// More idiomatic: use composite literal syntax.
return &S{}
}
相反,在 C++ 中返回指向局部变量的指针是不安全的
// C++
S* f2() {
S s;
return &s; // INVALID -- contents can be overwritten at any time
}
映射和通道值必须使用内置函数 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
指针。
type myType struct{ i int }
func (p *myType) Get() int { return p.i }
这会声明一个与 myType
关联的 Get
方法。在函数体内,接收者被命名为 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)
}
换句话说,如果我们把 myInterface
看作一个 C++ 纯抽象基类,那么为 *myType
定义 Set
和 Get
会让 *myType
自动继承自 myInterface
。一个类型可以实现多个接口。
匿名字段可用于实现非常类似于 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)
}
Set
方法实际上是从 myType
继承的,因为与匿名字段关联的方法会被提升为包围类型的方法。在这种情况下,因为 myChildType
有一个类型为 myType
的匿名字段,所以 myType
的方法也成为 myChildType
的方法。在此示例中,Get
方法被重写,而 Set
方法被继承。
这与 C++ 中的子类不完全相同。当调用匿名字段的方法时,其接收者是字段,而不是外层结构体。换句话说,匿名字段上的方法不是虚函数。当您想要等同于虚函数时,请使用接口。
具有接口类型的变量可以使用一种称为类型断言的特殊构造转换为具有不同接口类型。这在运行时动态实现,类似于 C++ 的 dynamic_cast
。与 dynamic_cast
不同,这两个接口之间不需要有任何声明的关系。
type myPrintInterface interface {
Print()
}
func f3(x myInterface) {
x.(myPrintInterface).Print() // type assertion to myPrintInterface
}
转换为 myPrintInterface
是完全动态的。只要 x 的动态类型定义了 Print
方法,转换就会成功。
由于转换是动态的,因此可以用于实现类似于 C++ 模板的泛型编程。这是通过操作 Any
的值来实现的。通过操作 Any
的值来实现这一点。
type Any interface{}
容器可以用 Any
来编写,但调用者必须通过类型断言来取消装箱以恢复包含类型的值。由于类型是动态的而不是静态的,因此没有等同于 C++ 模板可以内联相关操作的方式。这些操作在运行时进行了完全的类型检查,但所有操作都将涉及函数调用。
type Iterator interface {
Get() Any
Set(v Any)
Increment()
Equal(arg Iterator) bool
}
请注意,Equal
的参数类型是 Iterator
。这不像 C++ 模板。请参阅FAQ。
函数闭包
在 C++11 之前的版本中,创建具有隐藏状态的函数的常见方法是使用“仿函数”——一个重载 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
。
// C++
#include <iostream>
#include <cstddef>
template <class UnaryOperator>
void my_transform (size_t n_elts, int* in, int* out, UnaryOperator op)
{
size_t i;
for (i = 0; i < n_elts; i++)
out[i] = op(in[i]);
}
class MyFunctor {
public:
int total;
int operator()(int v) {
total += v;
return total;
}
MyFunctor() : total(0) {}
};
int main (void)
{
int data[7] = {8, 6, 7, 5, 3, 0, 9};
int result[7];
MyFunctor accumulate;
my_transform(7, data, result, accumulate);
std::cout << "Result is [ ";
for (size_t i = 0; i < 7; i++)
std::cout << result[i] << ' ';
std::cout << "]\n";
return 0;
}
C++11 添加了匿名(“lambda”)函数,这些函数可以存储在变量中并传递给函数。它们可以充当闭包,这意味着它们可以引用父作用域中的状态。此功能极大地简化了 my_transform
。
// C++11
#include <iostream>
#include <cstddef>
#include <functional>
void my_transform (size_t n_elts, int* in, int* out, std::function<int(int)> op)
{
size_t i;
for (i = 0; i < n_elts; i++)
out[i] = op(in[i]);
}
int main (void)
{
int data[7] = {8, 6, 7, 5, 3, 0, 9};
int result[7];
int total = 0;
my_transform(7, data, result, [&total] (int v) {
total += v;
return total;
});
std::cout << "Result is [ ";
for (size_t i = 0; i < 7; i++)
std::cout << result[i] << ' ';
std::cout << "]\n";
return 0;
}
典型的 Go 版本 my_transform
在很多方面都类似于 C++11 版本。
package main
import "fmt"
func my_transform(in []int, xform func(int) int) (out []int) {
out = make([]int, len(in))
for idx, val := range in {
out[idx] = xform(val)
}
return
}
func main() {
data := []int{8, 6, 7, 5, 3, 0, 9}
total := 0
fmt.Printf("Result is %v\n", my_transform(data, func(v int) int {
total += v
return total
}))
}
(请注意,我们选择从 my_transform
返回 out
,而不是将其传递一个 out
来写入。这是一个美学决定;在这方面,代码本可以写得更像 C++ 版本。)
在 Go 中,函数始终是完整的闭包,等同于 C++11 中的 [&]
。一个重要的区别是,在 C++11 中,闭包引用作用域已消失的变量(可能由向上 funarg 引起——返回一个引用局部变量的 lambda 的函数)是无效的。在 Go 中,这是完全有效的。
并发
与 C++11 的 std::thread
类似,Go 允许启动在共享地址空间中并发运行的新执行线程。这些称为*goroutine*,使用 go
语句生成。虽然典型的 std::thread
实现启动重量级的操作系统线程,但 goroutine 实现为轻量级的用户级线程,这些线程被多路复用到多个操作系统线程上。因此,goroutine 是(意图上)廉价的,并且可以在程序中广泛使用。
func server(i int) {
for {
fmt.Print(i)
time.Sleep(10 * time.Second)
}
}
go server(1)
go server(2)
(请注意,server
函数中的 for
语句等同于 C++ 的 while (true)
循环。)
函数字面量(Go 将其实现为闭包)可以与 go
语句一起使用。
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.
与 C++11 类似,但与 C++ 早期版本不同,Go 为未同步的内存访问定义了一个内存模型。尽管 Go 在其 sync
包中提供了 std::mutex
的类似物,但这并不是 Go 程序中实现线程间通信和同步的常规方式。相反,Go 线程更通常地通过消息传递进行通信,这是一种与锁和屏障根本不同的方法。关于此主题的 Go 箴言是:
不要通过共享内存来通信;相反,通过通信来共享内存。
也就是说,*通道*用于 goroutine 之间的通信。任何类型的值(包括其他通道!)都可以通过通道发送。通道可以是无缓冲的,也可以是缓冲的(使用在通道创建时指定的缓冲区长度)。
通道是第一类值;它们可以像其他任何值一样存储在变量中,并像其他任何值一样传递给函数和从函数返回。(当提供给函数时,通道是按引用传递的。)通道也是有类型的:chan int
与 chan string
不同。
由于通道在 Go 程序中被广泛使用,因此它们是(意图上)高效且廉价的。要通过通道发送值,请使用 <-
作为二元运算符。要从通道接收值,请使用 <-
作为一元运算符。通道可以在多个发送者和多个接收者之间共享,并保证每个发送的值最多被一个接收者接收。
这是一个使用管理器函数控制对单个值的访问的示例。
type Cmd struct {
Get bool
Val int
}
func Manager(ch chan Cmd) {
val := 0
for {
c := <-ch
if c.Get {
c.Val = val
ch <- c
} else {
val = c.Val
}
}
}
在该示例中,同一个通道用于输入和输出。如果同时有多个 goroutine 与管理器通信,这会是错误的:等待管理器响应的 goroutine 可能会收到来自另一个 goroutine 的请求。解决方案是传递一个通道。
type Cmd2 struct {
Get bool
Val int
Ch chan<- int
}
func Manager2(ch <-chan Cmd2) {
val := 0
for {
c := <-ch
if c.Get {
c.Ch <- val
} else {
val = c.Val
}
}
}
要使用 Manager2
,请给定一个通道给它
func getFromManagedChannel(ch chan<- Cmd2) int {
myCh := make(chan int)
c := Cmd2{true, 0, myCh} // Composite literal syntax.
ch <- c
return <-myCh
}
func main() {
ch := make(chan Cmd2)
go Manager2(ch)
// ... some code ...
currentValue := getFromManagedChannel(ch)
// ... some more code...
}
此内容是 Go Wiki 的一部分。