编译器和运行时
编译错误 non-name *** on left side of := 什么意思
到现在为止(Go 1.10), 对于短变量声明有一个强制性规则:
:= 左侧的所有条目必须是纯标识符, 并且至少有一个是新变量名.
这意味着容器元素(x[i]
), 结构体字段(x.f
), 指针解引用(*
)以及限制性标识符不能出现在 :=
的左侧. 目前, 针对这个问题有一个公开 issue. 看起来 Go 作者们想把这个问题留到 Go 2.0.
编译错误 unexpected newline, expecting { … 什么意思
在 Go 中, 我们不能在随意的位置折断一行代码. 具体细节可以阅读 Go 换行规则. 通过该规则, 一般来说, 在大括号之前不允许换行.
比如下面的代码:
if true |
将解释成:
if true; |
Go 编译器会对每一个开放的大括号给出一个错误. 为了避免这些错误, 我们应该重写代码如下:
if true { |
编译错误 declared and not used 什么意思
对于标准 Go 编译器, 每个被声明为本地代码块的变量应该被至少用作右值(right-hand side)一次.
所以如下的代码将无法编译:
func f(x bool) { |
Go 运行时是否会维持 map 的遍历顺序
不会. Go 1 规范说 map 的遍历顺序无法保证. 对于标准 Go 编译器, map 的遍历顺序有些随机. 如果你需要 map 的有序遍历, 你需要自己维持这个顺序. 阅读 Go maps 实战获取更多信息.
Go 编译器是否会对结构体类型做填充以保证字段对齐
至少对于标准 Go 编译器和 gccgo, 答案是肯定的. 填充是操作系统和编译器相关的.
比如:
type T1 struct { |
T1.b
内存中的地址值在 AMD64 系统必须的是 8 字节对齐的, i386 系统是 4 字节对齐的. 这就是为什么 T1.a
在 AMD64 系统被填充 7 字节, 在 i386 系统被填充 3 字节.
Go 规范对类型对齐做一些保证. 规则之一是一个结构体类型的对齐是它字段类型最大的那个的对齐. 所以 T1
也是 AMD64 系统 8 字节对齐, i386 系统 4 字节对齐(和 T1.b
类型一样, int64
), 并且标准的 Go 编译器将确保类型值的大小是该类型的对齐保证的倍数, 这就是为什么 T1.c
在 AMD64 系统被填充 6 字节, 在 i386 系统被填充 2 字节.
一个结构体中的字段顺序会影响填充, 并且填充会影响后续结构体的大小. 在 AMD64 系统, T1
的大小是 24, 但 T2
的大小是 16.
Go 编译器不会重排结构体字段来减小结构体大小. 这样做会引起一些不可预知的结果. 然而, 程序员可以通过手动重排字段来减少填充.
为什么一个结构体中结尾的零大小类型的字段有时会影响结构体的大小
(这不是 Go 规范的规则. 它是一个编译器的具体实现. 这里的编译器是标准编译器. 具体细节查看 issue#9401)
在当前的标准 Go 运行时实现中, 如果一个内存块至少被一个活跃的指针引用, 那么这个内存块将不会被视作垃圾并且不会被回收. 一个可寻址的结构体值的所有字段都可以寻址. 如果一个非零大小的结构体的结尾字段的大小是零, 那么对结构体值结尾字段的寻址将会返回一个地址, 该地址在结构体值分配的内存块之外. 返回的地址可能:
- 指向其他分配的内存块. 如果一个活跃的指针存储了该地址, 它会阻止垃圾回收器回收其他分配的内存块的回收, 这将引起内存泄漏.
- 指向一个未分配内存区. 这会导致垃圾回收器重新收集未分配内存并且可能会引起程序崩溃.
为了避免这些问题, Go 标准编译器将确保对于非零大小结构体结尾字段的寻址永远不会返回一个结构体之外分配内存块的地址. Go 标准编译器通过必要时在结尾非零字段前填充一些字节来实现.
如果一个结构体类型所有字段的类型都是零大小的(所以该结构体也是零大小的), 那么没有必要对该结构体填充字节, 因为 Go 标准编译器会对零大小的类型特殊对待.
比如:
package main |
new(T) 是 var t T; (&t) 的语法糖吗
一般来说, 你可以这样认为. 通过 new
分配的内存块要么在栈上要么在堆上. 在这两者之间会有一些微妙的差异, 取决于编译器实现.
运行时错误 all goroutines are asleep - deadlock 什么意思
单词 asleep (睡着了)在这里是不准确的, 实际上这意味着处于阻塞状态. 一个阻塞 goroutine 只能被其他 goroutine 唤醒, 如果程序里所有的 goroutine 都进入阻塞状态, 那么所有 goroutine 都将永远处于阻塞状态. 这意味着程序发生了死锁. 一个正常运行的程序是不希望死锁的, 所以标准 Go 运行时让程序崩溃并退出.
被确保 64 位对齐的变量和结构体字段能原子访问吗
传给 sync/atomic
的 64 位函数的地址必须是 64 位对齐的, 否则调用这些函数会引起运行时崩溃.
对于运行在 64 位系统的 Go 程序, 64 位值确保为 64 位对齐. 所以他们可以原子访问.
对于运行在 32 位系统的 Go 程序, 没有这种普遍的保证. 但是以下的 64 位字可以确保能够原子访问:
- 已分配 64 位字.
- 已分配结构体的第一个字段是 64 位字.
- 已分配数组的第一个元素的第一个字是 64 位.
这里, 一个已分配值意味着该值的地址是该值所在内存块的开始地址. 换句话说, 该值在内存块所在的开始位置.
一个合格的 Go 编译器应该承诺一个数组或者切片的所有元素也能够原子访问, 如果一个数组或者切片的一个元素可以原子访问并且元素的类型是一个 64 位字类型, 尽管官方文档并不保证这个.
比如:
package main |
赋值是原子操作吗
对于标准 Go 编译器来说不是, 尽管赋值的大小是一个字. 访问官方问题查看更多细节.
零值在内存中是由零字节序列组成的吗
对于大多数类型, 这是对的. 实际上, 这是编译器相关的. 比如, 对于标准 Go 运行时, 对于字符串的一些零值这是错的.
证据:
package main |
相反, 对于标准 Go 编译器当前支持的所有处理器和架构来说, 如果一个值的所有字节都是零, 那么该值必定是这个类型的零值. 然而, Go 规范并不保证. 我也听在一些老旧的处理器上, 空指针在内存中并不是零.
标准 Go 编译器支持函数内联吗
是的, 标准 Go 编译器支持函数内联. 编译器会自动内联短叶函数. 叶函数是那些不包含函数调用的函数. 特别的内联规则会随版本而变化.
当前(Go 1.10), 对于标准 Go 编译器,
- 没有明确的方式指定用户中的哪个函数应该内联.
- 尽管
gcflags "-l"
编译选项能阻止任何函数内联, 没有正式的方式避免用户特定的函数内联. 有一些真正非正式的方式(所有这些可能随 Go 标准编译器版本可能变得不可用):
- 你可以在一个函数声明前添加一行指令
//go:noinline
来避免函数内联. - 你可以添加一行
for false {}
在一个包含循环块的函数来避免内联.
标准库
如何原子操作指针值
比如:
import ( |
是的, 现在使用指针原子函数更简洁了.
怎么用更少的代码获得任何月的天数
假设输入的年份是一个自然年并且输入月也是一个自然月(一月是 1).
days := time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day() |
对于 Go 时间 API, 通常月份起止是 [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 |
time.Sleep(d) 函数调用和 <- time.After(d) channel 接收操作有何不同
这两个都会特定的时间内暂停当前的 goroutine 执行. 不同是 time.Sleep(d)
函数调用会使当前的 goroutine 进入睡眠状态(不是一个 goroutine 状态), 但仍然会保持运行, 相反, <-time.After(d)
会使当前的 goroutine 进入阻塞状态.
我能用 finalizers 作为对象解构器吗
在 Go 程序里, 我们可以通过 runtime.SetFinalizer
函数对一个对象设置一个 finalizer 函数. 对象被垃圾回收之前 finalizer 函数会被调用. 但是 finalizer 从来不是被设计为用作对象的解构器. 被 runtime.SetFinalizer
设置的 finalizers 不保证可以运行. 所以对于程序的正确性你不应该依赖 finalizers.
finalizers 的主要意图是为程序库开发人员为补救用户使用不当做出的额外努力. 比如, 在一个程序中, 如果我们使用 os.Open
打开了多个文件但是忘了使用后关闭他们, 那么程序会持有很多文件描述符直到程序退出. 这就是资源泄漏. 为避免程序持有太多文件描述符, os
包的维护者会为每个创建的 os.File
对象设置一个 finalizer. finalizer 会关闭存储在 os.File
对象中的文件描述符. 正如上面提到的, 这些 finalizers 不保证会被调用. 他们仅仅被用来尽量减少资源泄漏.
有时, 为了避免一个对象的 finalizer 被调用太早, 我们可以使用 runtime.KeepAlive 函数避免对象被垃圾回收的太早.
请注意, 一些 finalizers 从来不会被调用, 一些 finalizers 设置不当会阻止一些对象被垃圾回收. 请阅读 runtime.SetFinalizer 函数文档获取更多细节.
调用 strings 和 bytes 标准库中的 TrimLeft 和 TrimRight 经常返回不期望的结果, 是实现的 BUG 吗
哈哈, 可能实现有 BUG, 但是现在没法确定. 如果返回的结果是不期望的, 那么更可能是你的期望不正确.
在 strings
和 bytes
标准库有很多 trim 函数. 这些函数可以归纳为两组:
Trim
,TrimLeft
,TrimRight
,TrimSpace
,TrimFunc
,TrimLeftFunc
,TrimRightFunc
. 这些函数会裁剪掉所有满足或默认条件的开头和结尾的 UTF-8 编码的 Unicode 码点(a.k.a runes)(TrimSpace
默认裁掉各种类型的空格). 开头和结尾的每个 rune 都会被检查直到有不满足特定和默认条件的那个.TrimPrefix
,TrimSuffix
. 这两个函数会裁剪特定或者默认的完整前缀和后缀子串.
一些程序员第一次使用 trim 函数的时候会误用 TrimLeft
和 TrimRight
函数为 TrimPrefix
和 TrimSuffix
. 当然返回结果是非常可能不是预期的.
比如:
package main |
fmt.Print, fmt.Println 和 fmt.Printf 是否是同步的
不是, 这些函数不是同步的. 如果需要同步, 请使用 log
标准库对应的函数. 你可以使用 log.SetFlags(0)
来移除每行日志的前缀.
fmt.Print 和 fmt.Println 有何不同
fmt.Println
总会在相邻的两个参数插入一个空格, fmt.Print
将在仅当两个相邻的参数类型不是字符串的时候会在两个参数间插入一个空格.
另一个不同是 fmt.Println
会在最后写入一个换行符, 但 fmt.Print
不会.
log.Print 和 log.Println 有没有不同
log.Print
和 log.Println
的不同点和上面 fmt
的两个函数差不多, 但是这两个函数都会在最后插入一个换行符.
内建的 print/println 和对应的 fmt 和 log 标准库的 print 函数有何不同
这两类函数有几个不同点:
- 内建的
print/println
函数会写入标准错误.fmt
的 print 函数会写入标准输出.log
的 print 函数也会默认写入标准错误, 然而可以通过log.SetOutput
函数配置. - 内建的
print/println
函数不能传入数组和结构体参数. - 对于一个复合类型参数, 内建的
print/println
会写入参数底层值部分的地址, 而fmt
和log
的 print 函数会尝试写入参数值的字面量. - 目前(Go 1.10), 对于标准 Go 编译器, 对内建的
print/println
的函数的调用不会对调用的参数的引用值逃逸到堆, 而fmt
和log
的 print 函数会. - 如果一个参数有
String() string
和Error() string
方法,fmt
和log
的 print 函数写参数的时候会尝试调用这些方法, 而内建的print/println
函数会忽略参数的方法. - 内建的
print/println
函数不保证存在于以后的 Go 版本中.
通过 math/rand 和 crypto/rand 标准库产生随机数有什么不同
math/rand
标准库对于一个给定种子(seed)产生的伪随机数是确定的. 产生的随机数不适合安全敏感的场景. 对于密码学安全的目的, 我们应该使用 crypto/rand
产生的伪随机数.
为什么没有 math.Round 函数
有 math.Round
函数, 如果你用的是 Go 1.10. 两个新函数, math.Round
和 math.RoundToEven
已被加入到 Go 1.10.
在 Go 1.10 之前, 对于 math.Round
函数是否应该被加入到标准库有很长时间的讨论. 最后提案被采纳.
类型系统
什么类型不支持比较
以下类型不支持比较:
- 映射(map)
- 切片(slice)
- 函数(function)
- 包含不可比较字段的结构体类型
- 包含不可比较元素的数组类型
不可比较类型不能用作 map 类型的 key 类型.
请注意,
- 尽管 map, slice 和 function 类型不支持比较, 他们的值可以和裸
nil
标识符比较. - 比较两个接口值 会在运行时发生崩溃如果这两个接口值的动态类型一致并且不可比较.
关于为什么 map, slice 以及 function 类型不支持比较, 可以阅读官方 Go FAQ 的这个回答.
为什么两个 nil 值有时不相等
(官方 Go FAQ 的这个回答也可以回答这个问题.)
回答这个问题之前, 让我们看 Go 中的一些事实:
nil
可以用作很多类型的(pointers, slices, maps, channels, functions 以及 interfaces)值.nil
值也可能没有类型(无类型).- 一个非接口类型
T
的值, 如果T
实现了I
接口, 那么T
可以转为I
. 转化之后一个非接口值的副本会存储在接口值. - 一个接口值存储一个非接口值. 非接口值被称作接口值的动态值. 动态值的类型信息也存储在接口值. 动态值的类型被称做接口值的动态类型.
- 无类型
nil
值可以转化为 pointers, slices, maps, channels, functions 以及 interfaces 中的任何类型. - 当一个无类型
nil
值存储在一个接口值时, 该接口值无动态类型. - 如果两个非接口值可比较(编译和运行时), 他们必须相等.
- 如果一个接口值和一个非接口值可比较(编译和运行时), 该非接口值将在比较前转化为接口值的类型. 所以比较一个接口值和非接口值等同于比较两个接口值.
- 如果两个接口值可以比较(编译和运行时), 他们是相等的仅当他们有相同的的动态类型并且他们的动态类型相等.
从这些事实中我们可以得出:
- 两个动态值都是
nil
的两个接口, 如果他们的动态类型不同(或者其中一个有动态类型另一个没有), 那么他们不相等. - 如果接口值的动态类型不是非接口
nil
值的类型, 一个动态值是nil
的接口值和和一个非接口nil
值是不相等的.
阅读Go 中的接口 和 Go 中的 nils 来获取更多解释.
比如:
package main |
为什么尽管两个不同类型 T1 和 T2 共享相同的底层类型, []T1 和 []T2 依然不共享相同的底层类型
(好像不久前官方 Go FAQ 增加了一个相似的问题.)
在 Go 中, 一个 slice 类型的值可以不使用 unsafe
机制转化为另一个 slice 类型仅仅当两个 slice 类型共享相同的底层类型(这篇文章列出了所有值的转化规则的列表).
一个无命名复合类型的底层类型是复合类型自己. 所以尽管两个不同类型 T1
和 T2
共享相同的底层类型, 类型 []T1
和 []T2
仍旧是不同的类型, 所以他们的底层类型也是不同的, 这也意味着他们中一个的值不能转化为另一个的.
[]T1
和 []T2
底层类型不同的原因是:
[]T1
和[]T2
值之间相互转化的要求在实践中并不普遍.- 这样可以让底层类型跟踪规则更简单.
这些原因对其他的复合类型依然合法. 比如类型 map[T]T1
和 map[T]T2
也不共享相同的底层类型尽管 T1
和 T2
共享相同的底层类型.
通过 unsafe
机制将 []T1
类型的值转化为 []T2
类型是可能的, 但一般这是不推荐的:
package main |
哪些值可以哪些值不可以寻址
以下的值不可以寻址:
- strings 的 bytes
- map 元素
- 接口值的动态值
- 常量值
- 字面量值
- 包级函数
- 方法(作为函数值使用**
- 中间值
- 函数调用
- 明确的值转换
- 各种各样的操作, 指针解引用除外:
- channel 接收操作
- sub-string 操作
- sub-slice 操作
- 加法, 减法, 乘法, 除法, 等.
请注意, Go 中有一个语法糖
&T{}
. 它是tmp:= T{}; (&tmp)
的缩写形式. 所以&T{}
是合法的并不意味着T{}
可寻址.
以下值是可寻址的:
- 变量
- 可寻址的结构体的字段
- 可寻址的数组的元素
- 任何 slice 的元素(不管 slice 本身是不是可寻址的)
- 指针解引用操作
为什么 map 元素不可寻址
第一个原因是, 在 Go 中, 对于一个 map m
和一个 key k
, 读操作 m[k]
总是合法的, 即便 m
是 nil
或者 m
不包含 key k
. 对于这两种情况, m[k]
总是 map 元素类型的零值. 任何类型的零值应该是不可变的. 不可变值不可以寻址, 否则他们的值可以被修改. m[k]
是不是一个零值只有运行时才知道. 所以为了安全和一致, Go 编译器总是认为 m[k]
不可寻址.
另一个原因是使 map 元素可以寻址意味着一个 map 元素的地址在它的生命周期内不可以改变. 这阻止了 Go 编译器使用更有效的算法实现 map. 对于标准 Go 编译器, map 元素的内部地址运行时可能会发生改变.
为什么非 nil slice 总是可寻址的, 即便 slice 本身不可寻址
slice 的内部类型是个这样的结构体:
struct { |
每个 slice 间接地的引用一个元素序列. 尽管一个非 nil slice 不可寻址, 它的内部元素序列必须是可寻址的. 事实上获取一个 slice 元素的地址就是获取该内部数组的元素的地址. 这就是为什么不可寻址的非 nil slice 元素可寻址.
基于同样的原因, 不可寻址的字符串的 substring 操作总是可以编译通过(通过标准 Go 编译器). 然而我不确定这是否是 Go 规范所保证的.
对于非指针非接口定义的类型 T, 为什么 *T 的方法集总是 T 方法集的超集, 反之则不然
在 Go 中, 为了方便:
- 类型
T
的一个值可以调用*T
定义的方法集, 当且仅当T
的值是可寻址的.
编译器在调用指针方法前会自动获取T
值的地址. 因为不是所有类型T
的值是可寻址的, 也不是所有的T
的值有能力调用定义在*T
的方法.
这种便利不仅是语法糖也是一个固有的规则. - 类型
*T
的值总是可以调用定义在 类型T
的方法. 因为解引用一个指针值总是合法的.
这种便利不仅是语法糖也是一个固有的规则.
所以 *T
的方法集总是 T
方法集的超集, 而不是相反, 是很合理的.
事实上, 你可以每个定义在类型 T
上的方法, 一个同名同签名的隐式方法自动定义在了类型 *T
:
func (t T) MethodX(param0 ParamType0, ...) (result0 ResultType0, ...) { |
如果一个方法定义在类型 *T
, 不会有同名同签名的方法定义在 T
. 这是另一个解释为什么 *T的方法集总是
T` 方法集的超集, 而不是相反.
(请阅读这个官方 Go FAQ 回答来获取更多解释.)
我们可以为哪些类型实现方法
在 Go 中, 我们仅能在以下情况下为任意的类型 T
和 *T
实现方法, 当 T
:
- 必须是已定义的类型;
- 不能是一个外部包的类型(包括内建类型);
- 不能是一个指针类型;
- 不能是一个接口类型.
Go 中如何定义不可变值
有三种不可变值的定义:
- 没有地址的值(所以不可寻址).
- 有地址但是不可寻址的值(他们的地址在语法上无法获取).
- 可以寻址的值但是他们的值无法在语法上予以修改.
在 Go 中, 直到现在(Go 1.10), 没有值能够满足第三点定义. 换句话说, 第三点不支持.
部分满足第一种定义的值在 Go 中叫常量(constants). 在 Go 中, 仅布尔, 数字和字符串值可以声明为常量.
方法和包级函数也可以视作不可变的值. 他们满足第二点.
在 Go 中没有办法定义其他自定义不可变直.
为什么没有内建 set 类型
Sets 只是不关心元素值的 maps. 在 Go 中, map[Tkey]struct{}
经常被用作 set 类型.
什么是 byte, 什么是 rune, 怎样把 []byte 和 []rune 转为字符串
在 Go 中, byte
是类型 uint8
的别名. 换句话说 byte
和 uint8
是相同的标识符类型. 对于 rune
和 int32
也是如此.
一个 rune
经常被用来存储一个 Unicode 断点.
[]byte
和 []rune
值可以直接但是明确地转为 string
, 反之亦然:
package main |
理解字符串的更多东西, 可以阅读 Go 中的字符串.
其他
iota 什么意思
Iota 是希腊字母的第 9 个字母. 在 Go 中, iota
被用作数字常量的声明. 在任何常量声明组中, iota
的初始值是 0, 那么它对接下来的每一行都会增加 1.
为什么没有一个 closed 函数用来检查一个 channel 是否已经关闭
原因是这种函数的用处是非常局限的. 对这种函数的调用的返回结果可能不会反映输入 channel 的最新状态. 所以依赖于返回结果来做决定不是一个好注意.
如果你确实需要这样一个函数, 可以自己毫不费力的实现一个. 阅读这篇文章来获取如何写这样一个 closed
函数以及如何避免使用它.
一个函数返回本地变量的指针安全吗
是的, 这在 Go 中是完全安全的.
支持栈的 Go 编译器会做逃逸分析. 对于标准 Go 编译器, 如果逃逸分析器认为一个内存块确定只在当前的函数调用时使用, 那么它会分配内存块到栈上, 否则内存块会分配到堆上.
单词 gopher 在 Go 社区有什么意义
在 Go 社区, 一个 gopher(地鼠) 意味着一个 Go 程序员. 这个绰号来源于 Go 语言采用一个卡通地鼠作为吉祥物的事实. 随便说一句, 卡通地鼠由 Renee French 设计, 她是 Go 项目领导者 Rob Pike 的妻子.
有疑问加站长微信联系(非本文作者)