Go问答101
(这是一份非官方Go问答列表。官方版问答列表在这里。)
直到目前(Go 1.17), Go中对短变量声明有一个强制性约束:
所有位于:=
符号左侧的条目都必须是纯,并且其中至少有一个为新变量名称。
这意味着容器元素索引表达式(x[i]
)、结构体的字段选择器(x.f
)、指针解引用(*p
)和限定标识符(aPackage.Value
)都不能出现在:=
符号的左侧。
目前,这还是一个未解决问题(已经和合并)。而且感觉Go核心开发团队目前并未有立即解决此问题的打算。
编译器错误信息unexpected newline, expecting { ...
意味着什么?
在编写Go代码时,我们不能随意断行。 请阅读代码断行规则一文以了解Go代码断行规则。 一般来说,根据这些规则,在左括号之前断行是不合法的。
例如,下列代码片段
将会被编译器解释成
if true;
{
}
for i := 0; i < 10; i++;
{
}
var _ = []int;
{
1, 2, 3;
}
Go编译器将为每个左大括号{
起始的代码行报告一个语法错误。 为避免这些报错,我们需要将上述代码重写为下面这样:
if true {
}
for i := 0; i < 10; i++ {
}
var _ = []int {
1, 2, 3,
}
编译器错误信息declared and not used
意味着什么?
对于标准编译器,在局部代码块中声明的每一个变量必须被至少一次用做r-value(right-hand-side value,右值)。
因此,下列代码将编译失败,因为y
只被用做目标值(目标值都为左值)。
func f(x bool) {
var y = 1 // y被声明了但没有被用做右值
if x {
y = 2 // 这里,y被用做左值
}
}
Go运行时是否维护映射条目的遍历顺序?
不。明确提到映射元素的迭代顺序时未定义的。 所以对于同一个映射值,它的一个遍历过程和下一个遍历过程中的元素呈现次序不保证是相同的。 对于标准编译器,映射元素的遍历顺序是随机的。 如果你需要固定的映射元素遍历顺序,那么你就需要自己来维护这个顺序。 更多信息请阅读Go官方博客文章Go maps in action。
但是请注意:从Go 1.12开始,标准库包中的各个打印函数的结果中,映射条目总是排了序的。
Go编译器是否会进行字节填充以确保结构体字段的地址对齐?
至少对于标准的Go编译器和gccgo,答案是肯定的。 具体需要填充多少个字节取决于操作系统和编译器实现。 请阅读关于Go值的内存布局一文获取详情。
Go编译器将不会重新排列结构体的字段来最小化结构体值的尺寸。 因为这样做会导致意想不到的结果。 但是,根据需要,程序员可以手工重新排序字段来实现填充最小化。
为什么一个结构体类型的最后一个字段类型的尺寸为零时会影响此结构体的尺寸?
一个可寻址的结构值的所有字段都可以被取地址。 如果非零尺寸的结构体值的最后一个字段的尺寸是零,那么取此最后一个字段的地址将会返回一个越出了为此结构体值分配的内存块的地址。 这个返回的地址可能指向另一个被分配的内存块。 在目前的官方Go标准运行时的实现中,如果一个内存块被至少一个依然活跃的指针引用,那么这个内存块将不会被视作垃圾因而肯定不会被回收。 所以只要有一个活跃的指针存储着此非零尺寸的结构体值的最后一个字段的越界地址,它将阻止垃圾收集器回收另一个内存块,从而可能导致内存泄漏。
为避免上述问题,标准的Go编译器会确保取一个非零尺寸的结构体值的最后一个字段的地址时,绝对不会返回越出分配给此结构体值的内存块的地址。 Go标准编译器通过在需要时在结构体最后的零尺寸字段之后填充一些字节来实现这一点。
如果一个结构体的全部字段的类型都是零尺寸的(因此整个结构体也是零尺寸的),那么就不需要再填充字节,因为标准编译器会专门处理零尺寸的内存块。
一个例子:
package main
import (
"unsafe"
"fmt"
)
func main() {
type T1 struct {
a struct{}
x int64
}
fmt.Println(unsafe.Sizeof(T1{})) // 8
type T2 struct {
x int64
a struct{}
}
fmt.Println(unsafe.Sizeof(T2{})) // 16
new(T)
是var t T; (&t)
的语法糖吗?
虽然这两者在实现上会有一些微妙的差别,取决于编译器的具体实现,但是我们基本上可以认为这两者是等价的。 即,通过new
函数分配的内存块可以在栈上,也可以在堆上。
运行时错误信息all goroutines are asleep - deadlock
意味着什么?
用词asleep在这里其实并不准确,实际上它的意思是处于阻塞状态。
因为一个处于阻塞状态的协程只能被另一个协程解除阻塞,如果程序中所有的协程都进入了阻塞状态,则它们将永远都处于阻塞状态。 这意味着程序死锁了。一个正常运行的程序永远不应该死锁,一个死锁的程序肯定是由于逻辑实现上的bug造成的。 因此官方Go标准运行时将在一个程序死锁时令其崩溃退出。
64位整数值的地址是否能保证总是64位对齐的,以便可以被安全地原子访问?
传递给sync/atomic
标准库包中的64位函数的地址必须是64位对齐的,否则调用这些函数将在运行时导致恐慌产生。
对于标准编译器和gccgo编译器,在64位架构下,64位整数的地址将保证总是64位对齐的。 所以它们总是可以被安全地原子访问。 但在32位架构下,64位整数的地址仅保证是32位对齐的。 所以原子访问某些64位整数可能会导致恐慌。 但是,有一些方法可以保证一些64位整数总是可以被安全地原子访问。 请阅读一文以获得详情。
赋值是原子操作吗?
对于标准编译器来说,赋值不是原子操作。
请阅读以了解更多。
是否每一个零值在内存中占据的字节都是零?
对于大部分类型,答案是肯定的。不过事实上,这依赖于编译器。 例如,对于标准编译器,对于某些字符串类型的零值,此结论并不十分正确。
比如:
反过来,对于标准编译器已经支持的所有架构,如果一个值的所有字节都是零,那么这个值肯定是它的类型的零值。 然而,Go规范并没有保证这一点。我曾听说在某些比较老的处理器上,空指针表示的内存地址并不为零。
标准的Go编译器是否支持函数内联?
是的,标准编译器支持函数内联。编译器会自动内联一些满足某些条件的短小函数。这些内联条件可能会在不同编译器版本之间发生变化。
目前(Go 1.17),对于标准编译器,
- 没有显式的方式来在用户代码中指定哪些函数应该被内联。
- 尽管编译参数
-gcflags "-l"
可以阻止任何函数被内联, 但是并没有一个正式的方式来避免某个特定的用户函数被内联。 目前我们可以在函数声明前增加一行//go:noinline
指令来避免这个函数被内联。 但是此方式不保证永久有效。
终结器的主要用途是为了库包的维护者能够尽可能地避免因为库包使用者不正确地使用库包而带来的危害。 例如,我们知道,当在程序中使用完某个文件后,我们应该将其关闭。 但是有时候因为种种原因,比如经验不足或者粗心大意,导致一些文件在使用完成后并未被关闭,那么和这些文件相关的很多资源只有在此程序退出之后才能得到释放。这属于资源泄漏。 为了尽可能地避免防止资源泄露,os
库包的维护者将会在一个os.File
对象被被创建的时候为之设置一个终结器。 此终结器函数将关闭此os.File
对象。当此os.File
对象因为不再被使用而被垃圾回收的时候,此终结器函数将被调用。
请记住,有一些终结器函数永远不会被调用,并且有时候不当的设置终结器函数将会阻止对象被垃圾回收。 关于更多细节,请阅读。
如何使用尽可能短的代码行数来获取任意月份的天数?
假设输入的年份是一个自然年,并且输入的月份也是一个自然月(1代表1月)。
days := time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
对于Go中的time
标准库包,正常月份的去值范围为[1, 12]
,并且每个月的起始日是1
。 所以,y
年的m
月的起始时间就是time.Date(y, m, 1, 0, 0, 0, 0, time.UTC)
。
传递给time.Date
函数的实参可以超出它们的正常范围,此函数将这些实参进行规范化。 例如,1月32日会被转换成2月1日。
以下是一些Go语言里的日期使用示例:
package main
import (
"time"
"fmt"
)
func main() {
// 2017-02-01 00:00:00 +0000 UTC
fmt.Println(time.Date(2017, 1, 32, 0, 0, 0, 0, time.UTC))
// 2017-01-31 23:59:59.999999999 +0000 UTC
fmt.Println(time.Date(2017, 1, 32, 0, 0, 0, -1, time.UTC))
// 2017-01-31 00:00:00 +0000 UTC
fmt.Println(time.Date(2017, 2, 0, 0, 0, 0, 0, time.UTC))
// 2016-12-31 00:00:00 +0000 UTC
fmt.Println(time.Date(2016, 13, 0, 0, 0, 0, 0, time.UTC))
// 2017-02-01 00:00:00 +0000 UTC
fmt.Println(time.Date(2016, 13, 32, 0, 0, 0, 0, time.UTC))
}
函数调用time.Sleep(d)
和通道接收<-time.After(d)
操作之间有何区别?
两者都会将当前的goroutine执行暂停一段时间。 区别在于time.Sleep(d)
函数调用将使当前的协程进入睡眠子状态,但是当前协程的(主)状态依然为运行状态; 而通道接收<-time.After(d)
操作将使当前协程进入阻塞状态。
调用strings
和bytes
标准库包里TrimLeft
和TrimRight
函数经常会返回不符预期的结果,这些函数的实现存在bugs吗?
哈,我们不能保证这些函数的实现绝对没有bug,但是如果这些函数返回的结果是不符你的预期,更有可能的是你的期望是不正确的。
标准包strings
和bytes
里有多个修剪(trim)函数。 这些函数可以被分类为两组:
Trim
、TrimLeft
、TrimRight
、TrimSpace
、TrimFunc
、TrimLeftFunc
和TrimRightFunc
。 这些函数将修剪首尾所有满足指定(或隐含)条件的utf-8编码的Unicode码点(即rune)。(TrimSpace
隐含了修剪各种空格符。) 这些函数将检查每个开头或结尾的rune值,直到遇到一个不满足条件的rune值为止。TrimPrefix
和TrimSuffix
。 这两个函数会把指定前缀或后缀的子字符串(或子切片)作为一个整体进行修剪。
部分会把TrimLeft
和TrimRight
函数当作TrimPrefix
和TrimSuffix
函数而。 自然地,函数返回的结果很可能不是预期的那样。
例如:
package main
import (
"fmt"
"strings"
)
func main() {
var s = "abaay森z众xbbab"
o := fmt.Println
o(strings.TrimPrefix(s, "ab")) // aay森z众xbbab
o(strings.TrimSuffix(s, "ab")) // abaay森z众xbb
o(strings.TrimLeft(s, "ab")) // y森z众xbbab
o(strings.TrimRight(s, "ab")) // abaay森z众x
o(strings.Trim(s, "ab")) // y森z众x
o(strings.TrimFunc(s, func(r rune) bool {
return r < 128 // trim all ascii chars
})) // 森z众
}
函数fmt.Print
和fmt.Println
的区别是什么?
fmt.Println
函数总会在两个相邻的参数之间输出一个空格,然而fmt.Print
函数仅当两个相邻的参数(的具体值)都不是字符串类型时才会在它们之间输出一个空格。
另外一个区别是fmt.Println
函数会在结尾写入一个换行符,但是fmt.Print
函数不会。
函数log.Print
和函数 log.Println
有什么区别吗?
函数log.Print
与log.Println
的区别与上一个问题里描述的关于函数fmt.Print
和fmt.Println
的第一个区别点类似。
这两个函数都会在结尾输出一个换行符。
函数fmt.Print
、fmt.Println
和fmt.Printf
的实现进行同步了吗?
没有。 如果有同步的需求,请使用log
标准库包里的相应函数。 你可以调用log.SetFlags(0)
来避免每一个日志行的前缀输出。
内置的print
和println
函数与fmt
和log
标准库包中相应的打印函数有什么区别?
除了上一个问题里提到的区别之外,这三组函数之间还有一些其他区别。
- 内置的
print
/println
函数总是写入标准错误。fmt
标准包里的打印函数总是写入标准输出。log
标准包里的打印函数会默认写入标准错误,然而也可以通过log.SetOutput
函数来配置。 - 内置
print
/println
函数的调用不能接受数组和结构体参数。 - 对于组合类型的参数,内置的
print
/println
函数将输出参数的底层值部的地址,而fmt
和log
标准库包中的打印函数将输出接口参数的动态值的字面形式。 - 目前(Go 1.17),对于标准编译器,调用内置的
print
/println
函数不会使调用参数引用的值逃逸到堆上,而fmt
和log
标准库包中的打印函数将使调用参数引用的值逃逸到堆上。 - 如果一个实参有
String() string
或Error() string
方法,那么fmt
和log
标准库包里的打印函数在打印参数时会调用这两个方法,而内置的print
/println
函数则会忽略参数的这些方法。 - 内置的
print
/println
函数不保证在未来的Go版本中继续存在。
标准库包math/rand
和crypto/rand
生成的随机数之间有什么区别?
通过math/rand
标准库包生成的伪随机数序列对于给定的种子是确定的。 这样生成的随机数不适用于安全敏感的环境中。 如果处于加密安全目的,我们应该使用crypto/rand
标准库包生成的伪随机数序列。
标准库中为什么没有math.Round
函数?
math.Round
函数是有的,但是只是从Go 1.10开始才有这个函数。 从Go 1.10开始,标准库添加了两个新函数math.Round
和math.RoundToEven
。
在Go 1.10之前,关于 math.Round
函数是否应该被添加进标准包,经历了很长时候的讨论。
哪些类型不支持比较?
下列类型不支持比较:
- 映射(map)
- 切片
- 函数
- 包含不可比较字段的结构体类型
- 元素类型为不可比较类型的数组类型
不支持比较的类型不能用做映射类型的键值类型。
请注意:
- 尽管映射,切片和函数值不支持比较,但是它们的值可以与类型不确定的
nil
标识符比较。 - 如果两个接口值的动态类型相同且不可比较,那么在运行时比较这两个接口的值会产生一个恐慌。
关于为什么映射,切片和函数不支持比较,请阅读Go的官方FAQ中。
为什么两个nil
值有时候会不相等?
(Go官方FAQ中的也回答了这个问题。)
一个接口值可以看作是一个包裹非接口值的盒子。被包裹在一个接口值中的非接口值的类型必须实现了此接口值的类型。 在Go中,很多种类型的类型的零值都是用nil
来表示的。 一个什么都没包裹的接口值为一个零值接口值,即nil接口值。 一个包裹着其它非接口类型的nil值的接口值并非什么都没包裹,所以它不是(或者说它不等于)一个nil接口值。
当对一个nil接口值和一个nil非接口值进行比较时(假设它们可以比较),此nil非接口值将先被转换为nil接口值的类型,然后再进行比较; 此转换的结果为一个包裹了此nil非接口值的一个副本的接口值,此接口值不是(或者说它不等于)一个nil接口值,所以此比较不相等。
关于更详细的解释请阅读接口和两篇文章。
一个示例:
package main
import "fmt"
func main() {
var pi *int = nil
var pb *bool = nil
var x interface{} = pi
var y interface{} = pb
var z interface{} = nil
fmt.Println(x == y) // false
fmt.Println(x == nil) // false
fmt.Println(y == nil) // false
fmt.Println(x == z) // false
fmt.Println(y == z) // false
(不久前,Go官方FAQ也增加了。)
在Go语言中,仅当两个切片类型共享相同的底层类型时,其中一个切片类型才可以转换成另一个切片的类型而不需要使用。
底层类型[]T1
和[]T2
不同的原因是:
- 把
[]T1
和[]T2
的值相互转换的需求在实践中并不常见。 - 使得底层类型的溯源规则更加简单。
同样的原因也适用于其它组合类型。 例如:类型map[T]T1
和 map[T]T2
同样不共享相同的底层类型,即便T1
和 T2
共享相同的底层类型。
类型[]T1
的值时候有可能通过使用unsafe
机制转换成[]T2
的,但是一般不建议这么做:
哪些值可以被取地址,哪些值不可以被取地址?
以下的值是不可以寻址的:
- 字符串的字节元素
- 映射元素
- 接口值的动态值(类型断言的结果)
- 常量(包括有名常量和字面量)
- 声明的包级别函数
- 中间结果值
- 函数调用
- 显式值转换
- 各种操作,不包含指针解引用(dereference)操作,但是包含:
- 通道接收操作
- 子字符串操作
- 子切片操作
- 加法、减法、乘法、以及除法等等。
请注意:&T{}
在Go里是一个语法糖,它是tmp := T{}; (&tmp)
的简写形式。 所以&T{}
是合法的并不代表字面量T{}
是可寻址的。
以下的值是可寻址的,因此可以被取地址:
- 变量
- 可寻址的结构体的字段
- 可寻址的数组的元素
- 任意切片的元素(无论是可寻址切片或不可寻址切片)
- 指针解引用(dereference)操作
为什么映射元素不可被取地址?
在Go中,映射的设计保证一个映射值在内存允许的情况下可以加入任意个条目。 另外为了防止一个映射中为其条目开辟的内存段支离破碎,官方标准编译器使用了哈希表来实现映射。 并且为了保证元素索引的效率,一个映射值的底层哈希表只为其中的所有条目维护一段连续的内存段。 因此,一个映射值随着其中的条目数量逐渐增加时,其维护的连续的内存段需要不断重新开辟来增容,并把原来内存段上的条目全部复制到新开辟的内存段上。 另外,即使一个映射值维护的内存段没有增容,某些哈希表实现也可能在当前内存段中移动其中的条目。 总之,映射中的元素的地址会因为各种原因而改变。 如果映射元素可以被取地址,则Go运行时(runtime)必须在元素地址改变的时候修改所有存储了元素地址的指针值。 这极大得增加了Go编译器和运行时的实现难度,并且严重影响了程序运行效率。 因此,目前,Go中禁止取映射元素的地址。
映射元素不可被取地址的另一个原因是表达式aMap[key]
可能返回一个存储于aMap
中的元素,也可能返回一个不存储于其中的元素零值。 这意味着表达式aMap[key]
在(&aMap[key]).Modify()
调用执行之后可能仍然被估值为元素零值。 这将使很多人感到困惑,因此在Go中禁止取映射元素的地址。
为什么非空切片的元素总是可被取地址,即便对于不可寻址的切片也是如此?
切片的内部类型是一个结构体,类似于
struct {
elements unsafe.Pointer // 引用着一个元素序列
length int
capacity int
}
每一个切片间接引用一个元素序列。 尽管一个非空切片是不可取地址的,它的内部元素序列需要开辟在内存中的某处因而必须是可取地址的。 取一个切片的元素地址事实上是取内部元素序列上的元素地址。 因此,不可寻址的非空切片的元素也是可以被取地址的。
对任意的非指针和非接口定义类型T
,为什么类型*T
的方法集总是类型T
的方法集的超集,但是反之却不然?
在Go语言中,为了方便,对于一个非指针和非接口定义类型T
,
- 一个
T
类型的值可以调用为*T
类型声明的方法,但是仅当此T
的值是可寻址的情况下。 编译器在调用指针属主方法前,会自动取此T
值的地址。 因为不是任何T
值都是可寻址的,所以并非任何T
值都能够调用为类型*T
声明的方法。 这种便利只是一个语法糖,而不是一种固有的规则。 - 一个
*T
类型的值可以调用为类型T
声明的方法。 这是因为解引用指针总是合法的。 这种便利不仅仅是一个语法糖,它也是一种固有的规则。
所以很合理的, *T
的方法集总是T
方法集的超集,但反之不然。
事实上,你可以认为对于每一个为类型T
声明的方法,编译器都会为类型*T
自动隐式声明一个同名和同签名的方法。 详见一文。
func (t T) MethodX(v0 ParamType0, ...) (ResultType0, ...) {
...
}
// 编译器将会为*T隐式声明一个如下的方法。
func (pt *T) MethodX(v0 ParamType0, ...) (ResultType0, ...) {
return (*pt).MethodX(v0, ...)
}
更多解释请阅读Go官方FAQ中的这个问答。
我们可以为哪些类型声明方法?
请阅读方法一文获取答案。
在Go里如何声明不可变量?
如下是三种不可变值的定义:
- 没有地址的值(所以它们不可以寻址)。
- 有地址但是因为种种原因在语法上不可以寻址的值。
- 可寻址但不允许在语法上被修改的值。
在Go语言中,直到现在(Go 1.17),没有值满足第三种定义。
有名常量值满足第一种定义。
方法和声明的函数可以被视为声明的不可变值。 它们满足第二种定义。字符串的字节元素同样满足第二种定义。
在Go中没有办法声明其它不可变值。
为什么没有内置的set
容器类型?
集合(set)可以看作是不关心元素值的映射。 在Go语言里,map[Tkey]struct{}
经常被用做一个集合类型。
什么是byte?什么是rune? 如何将[]byte
和[]rune
类型的值转换为字符串?
在Go语言里,byte
是uint8
类型的一个别名。 换言之,byte
和 uint8
是相同的类型。 rune
和int32
属于同样类似的关系。
一个rune
值通常被用来存储一个Unicode码点。
[]byte
和[]rune
类型的值可以被显式地直接转换成字符串,反之亦然。
package main
import "fmt"
func main() {
var s0 = "Go"
var bs = []byte(s0)
var s1 = string(bs)
var rs = []rune(s0)
var s2 = string(rs)
fmt.Println(s0 == s1) // true
fmt.Println(s0 == s2) // true
}
更多关于字符串的信息,请阅读Go中的字符串一文。
如何原子地操作指针值?
例如:
import (
"unsafe"
"sync/atomic"
)
type T int // just a demo
var p *T
func demo(newP *T) {
// 加载(读取)
var _ = (*T)(atomic.LoadPointer(
(*unsafe.Pointer)(unsafe.Pointer(&p)),
))
// 存储(修改)
atomic.StorePointer(
(*unsafe.Pointer)(unsafe.Pointer(&p)),
unsafe.Pointer(newP),
)
// 交换
var oldP = (*T)(atomic.SwapPointer(
(*unsafe.Pointer)(unsafe.Pointer(&p)),
unsafe.Pointer(newP),
))
// 比较并交换
var swapped = atomic.CompareAndSwapPointer(
(*unsafe.Pointer)(unsafe.Pointer(&p)),
unsafe.Pointer(oldP),
unsafe.Pointer(newP),
)
_ = swapped
}
是的,目前指针的原子操作使用起来非常得繁琐。
iota
是什么意思?
Iota是希腊字母表中的第九个字母。 在Go语言中,iota
用在常量声明中。 在每一个常量声明组中,其值在该常量声明组的第N个常量规范中的值为N
。
为什么没有一个内置的closed
函数用来检查通道是否已经关闭?
原因是此函数的实用性非常有限。 此类函数调用的返回结果不能总是反映输入通道实参的最新状态。 所以依靠此函数的返回结果来做决定不是一个好主意。
如果你确实需要这种函数,你可以不怎么费功夫地自己写一个。 请阅读如何优雅地关闭通道一文来了解如何编写一个函数以及如何避免使用这样的函数。
是的,在Go中这是绝对安全的。
单词gopher在Go社区中表示什么?
在Go社区中,gopher表示Go程序员。 这个昵称可能是源自于Go语言采用了做为吉祥物。 顺便说一下,这个卡通小地鼠是由Renee French设计的。 Renee French是Go项目首任负责人Rob Pike的妻子。