本文翻译自官方FAQ
该链接可能需要科学上网 orz
其中一些专有名词为了防止翻译引起的歧义,索性保留英文:)
Usage
Go
程序能和C/C++
程序链接在一起吗 ?
Do Go programs link with C/C++ programs?
可以的,单这并不是很自然的做法,并且需要特别的接口软件。此外,将C
与Go
链接在一起会丧失原本Go
可以提供的安全地内存和栈的管理。有时为了解决一个问题,我们一定要使用C
库,但这么做总会引入一些风险,毕竟链接后就不是纯Go
程序了,所以千万当心!
如果您确实需要将C
与Go
配合使用,那么如何进行取决于Go
编译器的实现。当前Go
团推支持三种编译器:gc
(默认),gccgo
(使用gcc
作为后端), 还有一个以LLVM
框架还不太成熟的gollvm
。
gc
使用与C
语言不同的调用约定(calling conventions
)和链接器,因此不能直接从C
程序直接调用,反之亦然。CGO
程序提供了一种“外部函数接口”的机制,允许从Go
代码安全地调用C
库。SWIG
将此机制扩展到C++
库。
您也可以在gccgo
和gollvm
中使用cgo
和SWIG
。由于它们使用的是传统的API
,所以您还可以非常小心地将这些编译器的代码直接与GCC/LLVM
编译的C
或C++
程序链接。只是,这么做需要您非常熟习语言的调用约定,同时还要注意从Go
调用C
或C++
时是又堆栈限制的。
Go
支持什么IDE
?
What IDEs does Go support?
Go
项目没有自带IDE
。但是Go
的语言和库的设计使得分析源代码十分容易。因此,大多数著名的编辑器和IDE
支持Go
,要么是直接使用,要么是提供插件支持。
这里罗列了一些支持Go
的著名IDE
和编辑器:emacs
、vim
、vscode
、atom
、eclipse
、sublime
、intellij
等等
Design
Go
有runtime
的概念的吗 ?
Does Go have a runtime ?
是的!runtime
库作为一个扩展库, 是每个Go
程序的组成部分。runtime
库实现了如垃圾回收、并发控制、栈管理等一系列Go
语言的特性。尽管与语言本身的关系更紧密,Go
的runtime
其实和C
语言程序中经常使用libc
库地位差不多。
需要强调的是,Go
的runtime
没有虚拟机的概念(这一点不同于Java
),Go
程序在编译时就已经翻译成机器码了。所以说,尽管runtime
这个概念在其他语言中经常用来表示程序运行的虚拟环境,但在Go
中,它就仅仅是一个库,用来提供Go
语言的特性。
为什么Go
没有X
特性 Why does Go not have feature X ?
Why does Go not have feature X
每一种语言都包含着新颖的特征,并且放弃了一些在其他语言中比较受欢迎的特性。Go
的设计着眼于编程的便利性、编译的速度、概念的正交性以及支持并发和垃圾回收等功能。如果你在Go
中找不到其他语言的X特性,那么只能说明这个特性不适合Go
,比如它会影响编译速度或设计的清晰度,或者使基础系统变得特别复杂。
如果Go
缺少的功能X
让您感到困扰,请原谅我们!您可以多了解下Go
已有的功能,这些功能说不定可以代替您想要的X
为什么Go
不支持泛型?
Why does Go not have generic types ?
也许在以后的某个时候,我们会让Go
支持泛型,但我们并不觉得这很急迫。
Go
是一种用来编写服务器程序的语言,这些程序是比较容易维护的(有关更多背景,请参阅本文)。所以Go
程序的设计目标应该更集中在可伸缩性、可读性和并发性等方面。在设计之初,我们认为多态编程对这些目标帮助不大,所以简单起见,Go
并没有支持泛型。
现在,Go
变得越来越成熟通用,并且有些场景的确可能需要某些形式的‘泛型编程’。然而,离Go
支持泛型仍有一些阻碍。
泛型确实很方便,但引入泛型会使得Go
得类型系统和runtime
的设计变得复杂。我们还没有找到一种设计能够使收益与代价成比例,尽管我们还是会继续考虑它。而且,在许多情况下,Go
的内置map
和slice
,加上空interface
构造容器(调用者可以显式unboxing
)的能力,可以让程序员编写代码实现泛型所能实现的功能。
这个问题依然是开放的。要了解之前为Go
设计好的泛型解决方案的几次失败尝试,请参阅此链接。
为什么Go
不支持exception
?
Why does Go not have exceptions
我们认为将exception
加入到控制结构(如try-catch-finally
用法)会导致代码复杂化。exception
还变相鼓励程序员将过多的常见错误(如无法打开文件)标记为异常错误。
Go
语言采用了一种不同的方法。对于一些明显的错误,Go
的多值返回特性使得它可以在不重载返回值的情况下更清晰地报告错误。一个标准的错误类型,加上Go
的其他特性,使得Go
的错误处理相当轻松,这一点与其他语言有很大的不同。
Go
还内置了一些功能,可以让程序在真正的异常情况发生时发出信号(panic
)并恢复(recover
)。恢复机制的代码仅在真正发生panic
时才会被调用,这足以处理异常了,并且还不需要额外的控制结构,如果程序员使用得当,会发现Go
的错误处理相当清晰。
可以点击Defer, Panic, and Recover查看更多异常恢复的资料。此外,Errors are values中描述了一种在Go
中处理错误的简明例子。这些例子都说明,Go
的错误处理十分强大。
为什么用goroutines
取代了threads
?
Why goroutines instead of threads ?
Goroutines
可以使得程序员更容易地使用并发。将独立执行的functions—coroutines
放在一组thread
执行的办法已经被广泛使用了。当一个coroutine
阻塞(比如阻塞的系统调用)时, runtime
会自动把运行在同一thread
上的其他coroutine
移动到其他可运行的thread
,这样它们就不会被阻塞。重点是程序员看不到这些,这就是Goroutines
。除了堆栈的内存(通常为几千字节)消耗外,Goroutines
几乎没有什么消耗。
为了使消耗的堆栈空间尽可能小,runtime
使用可调整大小的堆栈,它会为一个新创建的goroutine
初始分配几千字节大小的堆栈空间,这就基本够用了。如果需要更多,runtime
会自动扩展这个空间(不需要时也会收缩),通过这种机制,大量goroutine
可以同时运行。如果将goroutine
换成thread
,那么系统资源早就被耗尽了。
为什么对map
的操作不是原子的 ?
Why are map operations not defined to be atomic ?
经过长期讨论,我们决定不去保证多个goroutine
访问map
的原子性。如果真的需要,您可以把map
放置在一个更大的数据结构中,在这个数据结构中去做同步和互斥。因为如果访问一个map
时都需要做互斥的话,那么程序会变慢,并且安全性也不会增加太多。诚然,做出这个决定并非易事,毕竟如果程序员完全不加控制多goroutine
访问map
的话,程序是可能崩溃的。
map
只有在进行更新时,访问才可能不安全。如果goroutine
只是读取map
去查找元素,包括使用for-range
循环去迭代访问map
,而不去修改元素和删除元素,那么在并发情况下就无须同步互斥操作。
为了帮助程序员正确地使用map
,Go
实现了一个特殊的检查,当map
在并发条件下被不安全地修改时,runtime
会自动报告。
Go
是一门面向对象的语言吗 ?
Is Go an object-oriented language ?
既可以说是,又可以说不是。虽然Go
有类型和方法,并且允许面向对象的编程风格,但它的类型之间没有层次结构。Go
中的interface
提供了一种更为通用和易用的不同的方式。我们还可以将一种类型嵌入到另一种类型中,以提供一种与子类化相似但不同的编程方法。此外,GO
中的方法比C++
或Java
更为通用:它们可以为任何类型(包括内置类型)的数据定义方法,而不仅限于结构(类)。
此外,由于没有类型之间的层次结构,Go
中的“对象”比C++
或Java
中的更轻量化。
怎样动态地调用不同的方法呢 ?
How do I get dynamic dispatch of methods
使用interface
是唯一的方法。为struct
(或其他具体类型)定义的方法在编译时就静态确定了。
为什么Go
中没有类型的继承 ?
Why is there no type inheritance ?
在面向对象的编程语言(比如C++
或Java
)中,我们会讨论许多各个类型之间的关系。而Go
采用了一种完全不同的方式。
Go
程序员不需要提前声明两个类型是相关的,在Go
中,一个类型自动满足其方法子集的任何interface
。这种方方式带来的好处不止是减少了语句,它还使得一种类型可以天然地满足多个接口,而不需要关注传统语言中的多重继承问题。interface
可以做到非常轻量化,比如它可以只包含0个或者1个方法。interface
。如果程序员在开发时有了新的想法,他甚至可以追加定义interface
,而不需要修改原始数据类型,因为数据类型和interface
之间是独立的,它们并没有层次关系。
我们可以用这种思想构建出类似Unix
中pipe
这样的东西:fmt.fprintf
可以将信息打印到输出到任何地方,而不仅是文件;bufio
包可以与文件I/O完全分离;image
包可以产生出压缩文件。这三者的实现的思想都来源于io.Writer
这个interface
中的中Write
方法。这些都是表面上能看到的东西,Go
的interface
对程序结构有深刻的影响。
初学者需要时间去习惯Go
中这种隐式依赖的风格。习惯后,你会意识到这是Go
语言中最具有创造性的发明。
为什么Go
不支持方法和操作符的重载 ?
Why does Go not support overloading of methods and operators ?
其他语言的经验告诉我们,定义多个相同名称但不同参数的方法有时是有用的,但在实践中它也容易引起混淆。在Go
的类型系统中,规定了只按名称匹配并要求类型的一致性是一个的简化决策。
对于运算符重载,不支持看上去比支持更为方便。
再说一次,没有重载,事情就简单了。
怎么保证类型满足一个interface
呢 ?
How can I guarantee my type satisfies an interface ?
您可以让编译器来帮助完成这件事。假设您要检查类型T
是否满足接口I
, 那么可以尝试把T
的零值或指向T
的指针赋值给I
,像下面这样:
type T struct{}
var _ I = T{} // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I.
如果T
或者*T
没有实现接口I
,那么编译器会报错.
如果希望使用interface
的用户显式声明他们实现了它,则可以向interface
的方法集添加一个具有描述性名称的方法。例如:
type Fooer interface {
Foo()
ImplementsFooer()
}
这样,用户定义的类型必须实现ImplementsFooer()
,这可以记录在go-doc
的输出中。
type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}
不过,大多数代码其实不会这么干,因为这样实际上限制了interface
。但是,有时如果真的相似interface
会引起歧义时,这么做是有必要的。
为什么类型T
不满足Equal
接口 ?
Why doesn't type T satisfy the Equal interface ?
思考下面的interface
的定义,它里面包含一个测试与其他值是否相等的方法。
type Equaler interface {
Equal(Equaler) bool
}
and this type, T:
类型T
尝试去实现这个interface
type T int
func (t T) Equal(u T) bool { return t == u } // does not satisfy Equaler
与其他多态语言的类型系统不同的是,在这种情况下,T
不满足Equaler
这个interface
,T.Equal
的参数类型是T
,而不是需要的Equaler
而下面这种情况中的T2
就是满足interface
的:
type T2 int
func (t T2) Equal(u Equaler) bool { return t == u.(T2) } // satisfies Equaler
这是因为在Go
中,任何满足Equaler
接口的类型都可以作为T2.Equal
的参数,所以在运行的时候,我们必须检查入参是否真的就是T2
类型,而其他语言是在编译的时候就保证的。
另一个例子:
type Opener interface {
Open() Reader
}
func (t T3) Open() *os.File
在Go
语言中,T3
不满足接口Opener
,即使在其他语言中答案可能是相反的。
通过以上三个例子可以看出来,Go
的类型系统不会帮程序员做类型的自动推断,因此在Go
中判断类型是否满足接口也就非常容易了:函数的名称、参数、返回值是否与接口的声明完全一致?我们认为这种简单性带来的好处完全可以弥补缺少自动类型推断的不足。
可以把一个[]T
的变量转换为[]interface
的变量吗 ?
Can I convert a []T to an []interface{} ?
不能直接转换。语言标准不允许这么做,因为这两种类型在内存中的表达方式不同。所以要想完成上面的目标,只能一个一个拷贝。
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
s[i] = v
}
我可以将[]T1
转换为[]T2
吗,如果它们的底层数据类型相同的话
Can I convert []T1 to []T2 if T1 and T2 have the same underlying type ?
type T1 int
type T2 int
var t1 T1
var x = T2(t1) // OK
var st1 []T1
var sx = ([]T2)(st1) // NOT OK
在Go
中,类型与方法紧密相连,每个命名类型都有一个方法集合(可能为空)。一般地,你只能对一个简单类型的变量进行格式转换(这可能会导致方法集合的变化),而不能改变组合类型的变量的类型。Go
需要你做显式的类型转换
为什么我的nil
错误码不能等于nil
?
Why is my nil error value not equal to nil ?
interface
变量包含两个元素,类型T
和值V
。例如,如果我们将int
值3
存储在一个interface
中,那么得到interface
变量就可以表示为(T
=int
,V
=3
)。值V
也称为interface
的动态值,因为在程序运行期间给定的interface
变量可能存储不同的值V
(和相应的类型T
)。
只有当V
和T
都未设置时(T
=nil
,V
未设置),接口的值才为nil
。如果我们在一个interface
变量中存储一个类型为*int
的nil
指针,那么不管指针的值是多少,内部类型都将是:(T
=*int
,V
=nil
)。因此,即使内部指针值V
为nil
,这样的接口值也将为non-nil
。
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error.
}
如果一切正常,函数返回值为nil
的变量p
,所以返回值可以表示为(T
=*myError
,V
=nil
)。这意味着,如果调用者将返回的错误与nil
进行比较,即使没有发生任何错误,它也将始终看起来像是发生了错误。所以如果你希望向调用方返回正确的nil
错误,函数必须返回显式的nil
:
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
对于那些返回错误码的函数来说,为了保证生成错误码的正确性,最好就是使用error
类型作为函数签名(就像上面那样),而不是返回像*myError
这样的具体类型。
Value
为什么Go
不支持数字类型之间的自动转换 ?
why does Go not provide implicit numeric conversions?
C
语言支持数字类型之间自动转换,它带来便利性的同时也会引起混乱。表达式(expression
)什么时候无符号?数值到底有多大?它溢出了吗?得到的结果是与机器无关可移植的吗?而且,它还使编译器复杂化;如果涉及到跨架构,即使只是“一般的算术转换”,也不是那么容易实现。所以,出于可移植性的原因,我们决定以代码中的进行显式转换为代价,使编程变得清晰和简单。还要说一句,Go
的数字常量是无符号的任意精度值,在没有赋值之前没有类型。
还有一个不同于C
语言中的细节是,Go
语言中的int
和int64
是两种不同的类型(即使int
本身是64
位)。int
类型是通用的;如果你关心一个整数的位数,那么最好的办法就是显示地声明使用它的类型。
Go
语言中的常量是如何工作的 ?
How do constants work in Go?
尽管Go
对不同数值类型的变量之间的转换非常严格,但对常量却比较灵活。像23
、3.14159
和math.pi
这样的常量被保存在特定的一片数字空间中,它们具有任意精度,不会溢出或下溢。例如,在源代码中,math.pi
的值被指定为63
位,而所有涉及该值的常量表达式都将精度保持在超过float64
所能容纳的范围。只有当常量或常量表达式被赋值给变量(程序中的内存位置)时,它才会变成一个具有浮点属性和精度的“计算机”数字。
此外,由于它们只是数字,不带类型,所以Go
中的常量可以比变量更自由地使用,这可以化解严格转换规则下带来的一些不变。例如:
sqrt2 := math.Sqrt(2)
编译器不会抱怨上面的语句,因为数字2
会安全地被转换为float64
的精度。
这里有一篇博客详细阐述了Go中常量的用法。
为什么Go
内置了map
类型
Why are maps built in ?
在语言层面实现强大并且的重要的数据结构可以使编程工作更加愉快。我们认为Go
中内置的map
可以强大到可以用于绝大多数程序。反过来说,如果一种自定义实现只能用于特定的应用,那么最好就在该应用中实现而不是在语言层面实现;这似乎是一个合理权衡之后的结果。
为什么map
不允许slice
作为key
?
Why don't maps allow slices as keys?
在map
中进行查找需要一个相等比较的运算,而slice
没有实现相等比较,因为slice
中不太好定义这个运算;这个问题涉及了一些关于浅拷贝与深拷贝之间的比较、指针与值之间的比较、如何处理递归类型等等。也许之后我们会重新审视这个问题,去实现slice
之间的相等比较操作,同时保证现有程序依然能正常工作。但现在来看,直接规定不允许是一个更简单的决定。
与更早的类型相比,在Go 1.X
版本中,我们定义了struct
之间、array
之间的比较相等操作,所以这些类型可以作为map
的key
,而slice
依然不可以。
为什么map
,slice
和channel
是引用类型,而array
是值类型 ?
Why are maps, slices, and channels references while arrays are values ?
这个问题牵涉到许多历史。早期,map
和channel
都限定为指针,不能声明为非指针实例。另外,我们在决定array
如何工作的问题上挣扎了许久。最终我们认为,严格地区分指针和值会使得语言变得难以使用,所以我们把map
和channel
转变为引用类型,通过共享数据结构解决了这个问题。这一变化的确增加了一些复杂性,但极大地增加了可用性。要知道,Go
语言的设计目标就是成为一种更高效、更舒适的语言。
Pointers and Allocation
函数什么时候以值进行参数传递 ?
When are function parameters passed by value?
与C
家族的所有语言一样,Go
都是以值进行传递的。也就是说,一个函数总是得到正在传递的对象的一个副本,就像有一个赋值语句将值赋给参数一样。例如,将int
值传递给函数将生成int
的副本,传递指针值将生成指针的副本,但不复制指向的数据。
map
和slice
变量的值的行为类似于指针(引用类型):它们是包含指向底层map
或slice
数据的指针的描述符。复制map
或slice
变量的值不会复制它指向的数据。复制interface
变量的值将复制存储在interface
变量的值中的内容。如果interface
变量的值包含struct
,则复制interface
的值将生成struct
的副本。如果interface
变量的值包含指针,则复制interface
的值会复制指针,但不会复制指向的数据。
注意,这里讨论的是语法上的操作。实际实现中,只要不会改变语义,那么编译器可能会进行优化避免来避免复制。
什么时候我才应该用指针去指向一个interface
?
When should I use a pointer to an interface?
几乎永远不要这么做!指向interface
值的指针只出现在非常罕见且棘手的情况下,这其中涉及到为了延迟计算而要隐藏interface
总储存的值得类型。
将指向interface
值的指针传递给期望接收interface
的函数是一个常见的错误。编译器会报错,但这种情况仍然会令人困惑,因为有时确实需要一个指针来满足interface
。记住这个事实吧,尽管指向具体类型的指针可以满足interface
,但除了一个例外,指向interface
的指针永远不能满足interface
。
思考下面的声明:
var w io.Writer
打印函数fmt.fprintf
将满足io.writer
的值作为其第一个参数—
所以我们就可以写
fmt.Fprintf(w, "hello, world\n")
If however we pass the address of w, the program will not compile.
如果我们传递的是w
的地址,那么程序将不能编译成功。
fmt.Fprintf(&w, "hello, world\n") // Compile-time error.
唯一的例外是,任何值,甚至是指向interface
的指针,都可以赋值给空的interface
类型的变量(即interface{}
)。但即使如此,如果值是指向interface
的指针,几乎肯定是一个错误。
我应该使用值还是指针作为方法的接收者呢 ?
Should I define methods on values or pointers?
func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct) valueMethod() { } // method on value
对于不习惯使用指针的程序员来说,可能会搞不清楚上面两个示例之间的有什么区别,但实际上情况非常简单。在为一个数据类型定义方法时,receiver
(上面示例中的s
)的行为与它是该方法的参数的行为完全相同。将接收器定义为值还是指针与函数参数应该使用值还是指针是同一个问题。总的说来,有几个考虑因素。
首先,也是最重要的,该方法是否需要修改receiver
?如果是,那么receiver
必须是指针。(slice
和map
是引用类型,因此它们的情况稍微特殊点,但要是更改方法会更改slice
的长度,接收者必须仍然是指针。)在上面的示例中,如果pointerMethod
修改s
的字段,则调用者将感知到这些更改,但valueMethod
方法将拷贝一份调用者的参数(者正是值传递的定义),因此它所做的更改对调用者是不可见的。
顺便说一下,在Java
的方法中,receiver
总是指针,尽管它们的指针本质有些隐晦(当前已经有一个为方法增加receiver
的提案给Java
了)。Go
中采用值作为receiver
是有一点特别的。
其次,是效率上的考虑。如果receiver
的数据结构很大,那么使用pointer receiver
开销就小得多。
其三,是一致性。如果一个类型的某些方法有pointer receiver
,那么其余的方法也应该有pointer receiver
,这样才能保证无论如何使用类型,方法集合都是一致的。有关详细信息,请参见方法集合部分。
对于基本类型、slice
和小型结构等类型,value receiver
开销不大,因此除非方法必须使用指针,否则value receiver
是高效且清晰的。
new 和 make 有什么区别 ?
What's the difference between new and make?
简单地说,new
分配内存空间,而make
初始化slice
、map
以及channel
等结构。
欢迎点击relevant section of Effective Go了解更多细节
在64位的机器上,一个int占多大 ?
What is the size of an int on a 64 bit machine?
int
和uint
占用的空间与平台相关,但在给定的平台上是这二者相同的。为了保证可移植性,对占用空间有依赖的代码应该使用显式大小的类型,如int64
。在32
位机器上,编译器默认使用32
位整数,而在64
位机器上,整数使用64
位整数。
另一方面,浮点类型和复数类型占用空间大小是动态确定的(Go
中没有表示浮点或复数的基本类型),因为程序员在使用浮点数字时应该知道精度。默认的浮点常量类型是float64
。因此foo:=3.0
声明float64
类型的变量foo
。对于由(无类型)常量初始化的float32
变量,必须像下面这样,在变量声明中显式指定变量类型:
var foo float32 = 3.0
当然, 你也可以通过下面的语句转换精度
foo := float32(3.0)`.
Q: 我怎么知道一个变量是分配在堆上还是栈上呢?
How do I know whether a variable is allocated on the heap or the stack?
从正确性的角度来看,你不需要知道。Go
中的每个有引用的变量都存储在一地方。实现存储位置与语言的语义无关。
存储位置的确会对编写高效程序有影响。Go
的编译器总是尽可能地将为在函数的栈帧中为函数分配局部变量。但是,如果编译器无法证明在函数返回后该变量没有其他地方引用,那么编译器就会在在gc
的堆上为该变量分配空间,以避免指针悬空。另外,如果一个局部变量非常大,那么将它也会存储在堆上而不是栈上。
对当前的Go
编译器,如果一个变量的地址被作为返回值,那么该变量就可能会在堆上分配(变量逃逸分析)。之所以是可能,是因为如果逃逸分析识别出如果函数外实际没有使用该变量地址,那么这个变量还是会分配在栈上。
Functions and Methods
Q: 为什么T
和 *T
有不同的方法集合 ?
Go
规范规定了,类型T
的方法集包含所有类型为T
的接收器的方法,而对应指针类型*T
的方法集包含所有类型为T
或T
的接收器的方法,这意味着方法集*T
是T
的超集。
造成这种区别的原因是如果一个包含指针*T
的interface
变量作为方法的receiver
,那么方法可以通过对指针的解引用来获取值,但是如果interface
包含值T
,则方法无法安全地获取其指针(这样做将允许一个方法修改interface
内值的内容,这是语言规范不允许的)。
即使编译器可以获取传递给该方法的值的地址,如果该方法修改了该值,这个更改的作用范围也仅限于方法内部,调用程者不会有任何变化(因为值传递是拷贝进行的)。例如,如果bytes.buffer
的写入方法使用value receiver
而不是pointer receiver
,则此代码:
var buf bytes.Buffer
io.Copy(buf, os.Stdin)
不会将标准输入中的内容拷贝到buf
中,这当然不是程序的原有目的。
闭包中运行的goroutine
发生了什么 ?
What happens with closures running as goroutines?
也许有一些小伙伴会对闭包引入的并发性感到疑惑,考虑下面的这个例子:
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
您可能会误以为程序会依次输出:A、B、C
。事实是你可能看到的会是 C,C,C
。这是因为循环的每个迭代都使用变量V
的相同实例,所以所有闭包都共享以个变量。在运行闭包时,它在执行fmt.println
时会打印V
的值,但V
可能在goroutine
启动后被修改。为了能在这些问题发生之前发现它们,请使用go vet。
为了在每个闭包启动时将当前的V
绑定到该闭包上,必须在每次迭代时,创建一个新的变量。一种方法就是将迭代的变量作为参数传递给闭包。
for _, v := range values {
go func(u string) {
fmt.Println(u)
done <- true
}(v)
}
在上面的例子中,V
的值作为参数传递给匿名函数。然后可以在函数内部访问该值作为变量u
。
还有一种办法就是创建一个新的v
,新的v
把迭代变量接下来。
for _, v := range values {
v := v // create a new 'v'.
go func() {
fmt.Println(v)
done <- true
}()
}
Control flow
为什么Go
没有?:
运算符
Why does Go not have the ?: operator?
Go
没有三元运算符。您可以用以下的代码得到相同的结果。
if expr {
n = trueVal
} else {
n = falseVal
}
不支持?:
的原因是Go
语言的设计者认为三元运算符会增加表达式的复杂程度。if-else
虽然看上去长一些,看毫无疑问它表达的意义更清晰。一种语言有一套条件控制形式就足够了。
Packages and Testing
Changes from C
为什么 Go 的语法与 C 差别这么大?
Why is the syntax so different from C?
除了变量声明的语法之外,两者之间的差别并不大。这些差别源自Go
的两个设计目标。首先,语法应该令人感觉轻松,没有太多的强制关键字、重复或晦涩难懂的地方。第二,语言应该被设计地易于分析,并且可以在没有符号表的情况下进行解析。这样做会使使得构建诸如调试器、依赖性分析器、自动化文档提取器、IDE插件等工具变得更加容易。而C
及其后继者(比如C++
)在这方面是出了名的困难。
为什么Go
使用反向的声明顺序 ?
Why are declarations backwards?
只有当你习惯了C
语言时,你才会觉得它们是反向的。在C
语言中,一个变量被声明为一个表示其类型的表达式,这是一个好主意,但是类型和表达式语法并不是很好地结合,结果可能会令人困惑(想想函数指针)。Go
主要将表达式和类型语法分开,并简化了操作(指针使用前缀*是一个例外)。在C中
int* a, b;
声明a
是一个指针,但b
不是,在Go
中:
var a, b *int
a
和b
都是指针。这样声明更清晰。另外,短声明方式和完整变量声明方式的顺序也是一样的。
var a uint64 = 1
和下面的语句有相同的效果
a := uint64(1)
代码的解析也因为这种独立的类型声明方式得到了简化;像func
和chan
等关键字可以使代码逻辑保持清晰。
为什么Go
中不能进行指针的算术运算?
Why is there no pointer arithmetic?
这是出于安全性的考虑。由于没有了指针的算术运算,因此Go
语言不会出现引用非法地址的错误。当前的编译器和硬件技术已经发展到在循环居中中,使用索引和使用指针同样高效。另外,没有指针的算术运算还可以大大简化gc
的实现
为什么++
和--
只是语句(statement),而不是表达式(expression)? 为什么是后增量,而不是前增量?
Why are ++ and -- statements and not expressions? And why postfix, not prefix?
由于Go
没有指针的算术运算,因此前、后固定增量运算符其实已经不能提供多少便利性了。通过将它们从表达式的层次结构中删除,Go
简化了表达式语法,并且也消除了围绕计算+
和-
的顺序的混乱问题(考虑f(i++)
和p[i]=q[++i]
)。这种简化至关重要。至于前增量和后增量,两者都可以,但后增量版本更传统;对前增量的的坚持源自C++ STL
,具有讽刺意味的是,这个名称使用的也是后增量形式。
为什么Go
中有括号却没有分好? 为什么我不能将左括号新起一行?
Why are there braces but no semicolons? And why can't I put the opening brace on the next line?
Go
使用括号进行语句分组,这是一种C
系列编程人员熟悉的语法。然而,分号是用于语法分析器(parser
)的,而不是用于人的,因此我们希望尽可能地消除它们。为了实现这一目标,Go
借鉴了BCPL
中的一个技巧:分号只需要由词法分析器lexer
在任何可能是语句结尾的行的末尾自动注入,而不需要提前添加。这在实践中非常有效,但其副作用就是需要限制括号的使用形式。例如,函数的左括号不能新起一行。
也有一些人认为,词法分析器应该向前看,以允许括号新起一行。我们不同意。因为Go
代码是由gofmt
自动格式化的,所以必须选定某种样式。当然这种风格可能不同于你在C
或Java
中使用的,但是Go
是一种不同的语言,gofmt
的风格也很好。更重要的是,更为重要的是,对于所有Go
程序,单一强制的格式带来的优点远远超过了任何特定样式的已知缺点。还要注意,Go
的风格意味着Go
的交互式实现可以一次使用一行标准语法,而无需特殊规则。
为什么Go
要支持垃圾回收(gc
), 它的开销不大吗?
Why do garbage collection? Won't it be too expensive?
记录管理已分配对象的生命周期是Go
程序自身完成的。在诸如C
这样的语言中,这种记录是程序员手工完成的,它会消耗大量的程序员时间精力,并且常常是可能稍不注意引起致命问题。即使在像C++
或Rust
之类提供协助机制的语言中,这些机制也会对软件的设计产生显著的影响,通常会增加其编程开销。我们认为替程序员消除这种开销是很有意义的,过去几年垃圾会后技术的进步使我们相信,它可以以足够小的代价实施,并且具有足够低的延迟,因此它可以成为网络化系统的一种可行方法。
并发编程的许多难度都来源于对象生存期的管理问题:当对象在线程之间传递时,要保证它们都被安全释放是一件很麻烦的事。自动垃圾回收使程序员更容易地编写并发程序的代码。当然,在并发环境中实现垃圾回收本身就具有挑战性,但是只迎面挑战它一次总好过让每个程序都去考虑这件事。
最后,撇开并发性不谈,垃圾会后使interface
编程更为简单,因为我们不需要指定如何跨接口地管理内存。
这并不是说Rust
等语言处理这个问题的方式是错误的;我们鼓励这项工作,并很乐于看到它是如何发展的。但是Go
采用了一种更传统的方法,既仅通过垃圾回收机制管理对象的生命周期。
当前Go
使用的是mark-and-sweep
垃圾回收算法。如果机器是多核处理器,则回收器在与主程序并行地运行在不同CPU
核心上。近年来,回收器已经将暂停时间减少到了亚毫秒级的范围,这几乎消除了网络服务器中垃圾会后的主要障碍之一。开发团队会继续改进算法,进一步减少开销和延迟,并探索新的途径。Go
团队的Rick Hudson
在2018年的ISM
主题演讲中报告了迄今为止的进展,并提出了一些未来的方法。
在性能方面,请记住,Go
使程序员能够相当大程度地控制内存布局和分配,这比垃圾回收语言中的典型情况要多得多。一个细心的程序员可以很好地使用该语言,从而大大减少垃圾收集开销;请参阅有关分析,了解一个已运行的示例,包括Go
的演示分析工具。
有疑问加站长微信联系(非本文作者)