42

Go中高频问题的FAQ 节选

 5 years ago
source link: https://studygolang.com/articles/19213?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

bVbqlll?w=550&h=289

本文翻译自 官方FAQ

该链接可能需要科学上网 orz

其中一些专有名词为了防止翻译引起的歧义,索性保留英文:)

Usage

Go 程序能和 C/C++ 程序链接在一起吗 ?

Do Go programs link with C/C++ programs?

可以的,单这并不是很自然的做法,并且需要特别的接口软件。此外,将 CGo 链接在一起会丧失原本 Go 可以提供的安全地内存和栈的管理。有时为了解决一个问题,我们一定要使用 C 库,但这么做总会引入一些风险,毕竟链接后就不是纯 Go 程序了,所以千万当心!

如果您确实需要将 CGo 配合使用,那么如何进行取决于 Go 编译器的实现。当前 Go 团推支持三种编译器: gc (默认), gccgo (使用 gcc 作为后端), 还有一个以 LLVM 框架还不太成熟的 gollvm

gc 使用与 C 语言不同的调用约定( calling conventions )和链接器,因此不能直接从 C 程序直接调用,反之亦然。 CGO 程序提供了一种“外部函数接口”的机制,允许从 Go 代码安全地调用 C 库。 SWIG 将此机制扩展到 C++ 库。

您也可以在 gccgogollvm 中使用 cgoSWIG 。由于它们使用的是传统的 API ,所以您还可以非常小心地将这些编译器的代码直接与 GCC/LLVM 编译的 CC++ 程序链接。只是,这么做需要您非常熟习语言的调用约定,同时还要注意从 Go 调用 CC++ 时是又堆栈限制的。

Go 支持什么 IDE ?

What IDEs does Go support?

Go 项目没有自带 IDE 。但是 Go 的语言和库的设计使得分析源代码十分容易。因此,大多数著名的编辑器和 IDE 支持 Go ,要么是直接使用,要么是提供插件支持。

这里罗列了一些支持 Go 的著名 IDE 和编辑器: emacsvimvscodeatomeclipsesublimeintellij 等等

Design

Goruntime 的概念的吗 ?

Does Go have a runtime ?

是的! runtime 库作为一个扩展库, 是每个 Go 程序的组成部分。 runtime 库实现了如垃圾回收、并发控制、栈管理等一系列 Go 语言的特性。尽管与语言本身的关系更紧密, Goruntime 其实和 C 语言程序中经常使用 libc 库地位差不多。

需要强调的是, Goruntime 没有虚拟机的概念(这一点不同于 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 的内置 mapslice ,加上空 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 ,而不去修改元素和删除元素,那么在并发情况下就无须同步互斥操作。

为了帮助程序员正确地使用 mapGo 实现了一个特殊的检查,当 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 之间是独立的,它们并没有层次关系。

我们可以用这种思想构建出类似 Unixpipe 这样的东西: fmt.fprintf 可以将信息打印到输出到任何地方,而不仅是文件; bufio 包可以与文件I/O完全分离; image 包可以产生出压缩文件。这三者的实现的思想都来源于 io.Writer 这个 interface 中的中 Write 方法。这些都是表面上能看到的东西, Gointerface 对程序结构有深刻的影响。

初学者需要时间去习惯 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 。例如,如果我们将 int3 存储在一个 interface 中,那么得到 interface 变量就可以表示为( T = intV = 3 )。值 V 也称为 interface 的动态值,因为在程序运行期间给定的 interface 变量可能存储不同的值 V (和相应的类型 T )。

只有当 VT 都未设置时( T = nilV 未设置),接口的值才为 nil 。如果我们在一个 interface 变量中存储一个类型为 *intnil 指针,那么不管指针的值是多少,内部类型都将是:( T = *intV = nil )。因此,即使内部指针值 Vnil ,这样的接口值也将为 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 = *myErrorV = 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 语言中的 intint64 是两种不同的类型(即使 int 本身是 64 位)。 int 类型是通用的;如果你关心一个整数的位数,那么最好的办法就是显示地声明使用它的类型。

Go 语言中的常量是如何工作的 ?

How do constants work in Go?

尽管 Go 对不同数值类型的变量之间的转换非常严格,但对常量却比较灵活。像 233.14159math.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 之间的比较相等操作,所以这些类型可以作为 mapkey ,而 slice 依然不可以。

为什么 map , slicechannel 是引用类型,而 array 是值类型 ?

Why are maps, slices, and channels references while arrays are values ?

这个问题牵涉到许多历史。早期, mapchannel 都限定为指针,不能声明为非指针实例。另外,我们在决定 array 如何工作的问题上挣扎了许久。最终我们认为,严格地区分指针和值会使得语言变得难以使用,所以我们把 mapchannel 转变为引用类型,通过共享数据结构解决了这个问题。这一变化的确增加了一些复杂性,但极大地增加了可用性。要知道, Go 语言的设计目标就是成为一种更高效、更舒适的语言。

Pointers and Allocation

函数什么时候以值进行参数传递 ?

When are function parameters passed by value?

C 家族的所有语言一样, Go 都是以值进行传递的。也就是说,一个函数总是得到正在传递的对象的一个副本,就像有一个赋值语句将值赋给参数一样。例如,将 int 值传递给函数将生成 int 的副本,传递指针值将生成指针的副本,但不复制指向的数据。

mapslice 变量的值的行为类似于指针(引用类型):它们是包含指向底层 mapslice 数据的指针的描述符。复制 mapslice 变量的值不会复制它指向的数据。复制 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 必须是指针。( slicemap 是引用类型,因此它们的情况稍微特殊点,但要是更改方法会更改 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 初始化 slicemap 以及 channel 等结构。

欢迎点击 relevant section of Effective Go 了解更多细节

在64位的机器上,一个int占多大 ?

What is the size of an int on a 64 bit machine?

intuint 占用的空间与平台相关,但在给定的平台上是这二者相同的。为了保证可移植性,对占用空间有依赖的代码应该使用显式大小的类型,如 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 的方法集包含所有类型为 TT 的接收器的方法,这意味着方法集 *TT 的超集。

造成这种区别的原因是如果一个包含指针 *Tinterface 变量作为方法的 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

ab 都是指针。这样声明更清晰。另外,短声明方式和完整变量声明方式的顺序也是一样的。

var a uint64 = 1

和下面的语句有相同的效果

a := uint64(1)

代码的解析也因为这种独立的类型声明方式得到了简化;像 funcchan 等关键字可以使代码逻辑保持清晰。

为什么 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 自动格式化的,所以必须选定某种样式。当然这种风格可能不同于你在 CJava 中使用的,但是 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 的演示分析工具。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK