(这是一份非官方Go问答列表。官方版问答列表在这里。)
non-name *** on left side of :=
意味着什么?unexpected newline, expecting { after if clause
意味着什么?declared and not used
意味着什么?new(T)
是var t T; (&t)
的语法糖吗?all goroutines are asleep - deadlock
意味着什么?time.Sleep(d)
和通道接收<-time.After(d)
操作之间有何区别?strings
和bytes
标准库包里TrimLeft
和TrimRight
函数经常会返回不符预期的结果,这些函数的实现存在bugs吗?fmt.Print
和fmt.Println
的区别是什么?log.Print
和函数log.Println
有什么区别吗?fmt.Print
、fmt.Println
和fmt.Printf
的实现进行同步了吗?print
和println
函数与fmt
和log
标准库包中相应的打印函数有什么区别?math/rand
和crypto/rand
生成的随机数之间有什么区别?math.Round
函数?non-name *** on left side of :=
意味着什么?
:=
符号左侧的条目都必须是纯标识符,并且其中至少有一个为新变量名称。
这意味着容器元素索引表达式(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
}
将会被编译器解释成
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编译器和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
}
另请见官方FAQ中的此问答。
new(T)
是var t T; (&t)
的语法糖吗?
虽然这两者在实现上会有一些微妙的差别,取决于编译器的具体实现,但是我们基本上可以认为这两者是等价的。
即,通过new
函数分配的内存块可以在栈上,也可以在堆上。
all goroutines are asleep - deadlock
意味着什么?
用词asleep在这里其实并不准确,实际上它的意思是处于阻塞状态。
因为一个处于阻塞状态的协程只能被另一个协程解除阻塞,如果程序中所有的协程都进入了阻塞状态,则它们将永远都处于阻塞状态。 这意味着程序死锁了。一个正常运行的程序永远不应该死锁,一个死锁的程序肯定是由于逻辑实现上的bug造成的。 因此官方Go标准运行时将在一个程序死锁时令其崩溃退出。
传递给sync/atomic
标准库包中的64位函数的地址必须是64位对齐的,否则调用这些函数将在运行时导致恐慌产生。
对于标准编译器和gccgo编译器,在64位架构下,64位整数的地址将保证总是64位对齐的。 所以它们总是可以被安全地原子访问。 但在32位架构下,64位整数的地址仅保证是32位对齐的。 所以原子访问某些64位整数可能会导致恐慌。 但是,有一些方法可以保证一些64位整数总是可以被安全地原子访问。 请阅读关于Go值的内存布局一文以获得详情。
对于标准编译器来说,赋值不是原子操作。
请阅读官方FAQ中的此问答以了解更多。
对于大部分类型,答案是肯定的。不过事实上,这依赖于编译器。 例如,对于标准编译器,对于字符串或者浮点数类型的某些零值,此结论并不十分正确。
比如:package main
import (
"unsafe"
"fmt"
)
func main() {
// case 1:
var s = "abc"[0:0]
fmt.Println(s == "") // true
var addr = *(*uintptr)(unsafe.Pointer(&s))
fmt.Println(addr) //
// case 2:
var x = 0.0
var y = -x
fmt.Println(y == 0) // true
var n = *(*uintptr)(unsafe.Pointer(&y))
fmt.Println(n) // 9223372036854775808
}
反过来,对于标准编译器已经支持的所有架构,如果一个值的所有字节都是零,那么这个值肯定是它的类型的零值。 然而,Go规范并没有保证这一点。我曾听说在某些比较老的处理器上,空指针表示的内存地址并不为零。
是的,标准编译器支持函数内联。编译器会自动内联一些满足某些条件的短小函数。这些内联条件可能会在不同编译器版本之间发生变化。
-gcflags "-l"
可以阻止任何函数被内联,
但是并没有一个正式的方式来避免某个特定的用户函数被内联。
目前我们可以在函数声明前增加一行//go:noinline
指令来避免这个函数被内联。
但是此方式不保证永久有效。
在Go程序里,我们可以通过调用runtime.SetFinalizer
函数来给一个对象设置一个终结器函数。
一般说来,此终结器函数将在此对象被垃圾回收之前调用。
但是终结器并非被设计为对象的析构函数。
通过runtime.SetFinalizer
函数设置的终结器函数并不保证总会被运行。
因此我们不应该依赖于终结器来保证程序的正确性。
终结器的主要用途是为了库包的维护者能够尽可能地避免因为库包使用者不正确地使用库包而带来的危害。
例如,我们知道,当在程序中使用完某个文件后,我们应该将其关闭。
但是有时候因为种种原因,比如经验不足或者粗心大意,导致一些文件在使用完成后并未被关闭,那么和这些文件相关的很多资源只有在此程序退出之后才能得到释放。这属于资源泄漏。
为了尽可能地避免防止资源泄露,os
库包的维护者将会在一个os.File
对象被被创建的时候为之设置一个终结器。
此终结器函数将关闭此os.File
对象。当此os.File
对象因为不再被使用而被垃圾回收的时候,此终结器函数将被调用。
请记住,有一些终结器函数永远不会被调用,并且有时候不当的设置终结器函数将会阻止对象被垃圾回收。 关于更多细节,请阅读runtime.SetFinalizer函数的文档。
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日。
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
标准库包中的打印函数将输出接口参数的动态值的字面形式。
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
函数是否应该被添加进标准包,经历了很长时候的讨论。
不支持比较的类型不能用做映射类型的键值类型。
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
}
[]T1
和[]T2
没有共享相同底层类型,即使不同的类型T1
和T2
共享相同的底层类型?
(不久前,Go官方FAQ也增加了一个相似的问题。)
在Go语言中,仅当两个切片类型共享相同的底层类型时,其中一个切片类型才可以转换成另一个切片的类型而不需要使用unsafe
机制。
一个无名组合类型的底层类型是此组合类型本身。
所以即便两个不同的类型T1
和T2
共享相同的底层类型,类型[]T1
和[]T2
也依然是不同的类型,因此它们的底层类型也是不同的。这意味着其中一个的值不能转换为另一个。
[]T1
和[]T2
不同的原因是:
[]T1
和[]T2
的值相互转换的需求在实践中并不常见。
同样的原因也适用于其它组合类型。
例如:类型map[T]T1
和 map[T]T2
同样不共享相同的底层类型,即便T1
和 T2
共享相同的底层类型。
[]T1
的值时候有可能通过使用unsafe
机制转换成[]T2
的,但是一般不建议这么做:
package main
import (
"fmt"
"unsafe"
)
func main() {
type MyInt int
var a = []int{7, 8, 9}
var b = *(*[]MyInt)(unsafe.Pointer(&a))
b[0]= 123
fmt.Println(a) // [123 8 9]
fmt.Println(b) // [123 8 9]
fmt.Printf("%T \n", a) // []int
fmt.Printf("%T \n", b) // []main.MyInt
}
&T{}
在Go里是一个语法糖,它是tmp := T{}; (&tmp)
的简写形式。
所以&T{}
是合法的并不代表字面量T{}
是可寻址的。
在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
的方法集的超集,但是反之却不然?
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 1.23),没有值满足第三种定义。 但是零尺寸类型的变量值可以被视为事实上的(可被取地址的)不变量。
具名常量值满足第一种定义。
方法和包级函数可以被视为声明的不可变值。它们满足第二种定义。 字符串的字节元素和映射条目中的元素值同样满足第二种定义。
在Go中没有办法声明其它不可变值。
set
容器类型?
map[Tkey]struct{}
经常被用做一个集合类型。
[]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中的字符串一文。
iota
是什么意思?
iota
用在常量声明中。
在每一个常量声明组中,其值在该常量声明组的第N个常量规范中的值为N
。
closed
函数用来检查通道是否已经关闭?
原因是此函数的实用性非常有限。 此类函数调用的返回结果不能总是反映输入通道实参的最新状态。 所以依靠此函数的返回结果来做决定不是一个好主意。
如果你确实需要这种函数,你可以不怎么费功夫地自己写一个。
请阅读如何优雅地关闭通道一文来了解如何编写一个closed
函数以及如何避免使用这样的函数。
是的,在Go中这是绝对安全的。
支持栈的Go编译器将会对每个局部变量进行逃逸分析。 对于官方标准编译器来说,如果一个值可以在编译时刻被断定它在运行时刻仅会在一个协程中被使用,则此值将被开辟在(此协程的)栈上;否则此值将被开辟在堆上。 请阅读内存块一文了解更多。
在Go社区中,gopher表示Go程序员。 这个昵称可能是源自于Go语言采用了一个卡通小地鼠(gopher)做为吉祥物。 顺便说一下,这个卡通小地鼠是由Renee French设计的。 Renee French是Go项目首任负责人Rob Pike的妻子。
Go101.org网站内容包括Go编程各种相关知识(比如Go基础、Go优化、Go细节、Go实战、Go测验、Go工具等)。后续将不断有新的内容加入。敬请收藏关注期待。
本丛书微信公众号(联系方式一)名称为"Go 101"。二维码在网站首页。此公众号将时不时地发表一些Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。
《Go语言101》系列丛书项目目前托管在Github上(联系方式二)。欢迎各位在此项目中通过提交bug和PR的方式来改进完善《Go语言101》丛书中的各篇文章。我们可以在项目目录下运行go run .
来浏览和确认各种改动。
本书的twitter帐号为@Golang_101(联系方式三)。玩推的Go友可以适当关注。
你或许对本书作者老貘开发的一些App感兴趣。
sync
标准库包sync/atomic
标准库包