Go问答101

(这是一份非官方Go问答列表。官方版问答列表在这里。)

索引:

编译器错误信息non-name *** on left side of :=意味着什么?

直到目前(Go 1.12), Go中对短变量声明有一个强制性约束
所有位于:=符号左侧的条目都必须是纯标识符,并且其中至少有一个为新变量名称。

这意味着容器元素索引表达式(x[i])、结构体的字段选择器(x.f)、指针解引用(*p)和限定标识符(aPackage.Value)都不能出现在:=符号的左侧。

目前,这还是一个未解决问题(已经和一个相关问题合并)。而且感觉Go核心开发团队并未有在Go 2发布之前解决此问题的打算。

编译器错误信息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官方博客文章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值的内存布局一文以获得详情。

赋值是原子操作吗?

对于标准编译器来说,赋值不是原子操作。

请阅读官方FAQ中的此问答以了解更多。

是否每一个零值在内存中占据的字节都是零?

对于大部分类型,答案是肯定的。不过事实上,这依赖于编译器。 例如,对于标准编译器,对于某些字符串类型的零值,此结论并不十分正确。

比如:
package main

import (
	"unsafe"
	"fmt"
)

func main() {
	var s1 string
	fmt.Println(s1 == "") // true
	fmt.Println(*(*uintptr)(unsafe.Pointer(&s1))) // 0
	var s2 = "abc"[0:0]
	fmt.Println(s2 == "") // true
	fmt.Println(*(*uintptr)(unsafe.Pointer(&s2))) // 4869856
	fmt.Println(s1 == s2) // true
}

反过来,对于标准编译器已经支持的所有架构,如果一个值的所有字节都是零,那么这个值肯定是它的类型的零值。 然而,Go规范并没有保证这一点。我曾听说在某些比较老的处理器上,空指针表示的内存地址并不为零。

标准的Go编译器是否支持函数内联?

是的,标准编译器支持函数内联。编译器会自动的内联短小的并且不调用其它函数的函数。 内联规则可能会在不同编译器版本之间发生变化。

目前(Go 1.12),对于标准编译器,
  • 没有显式的方式来在用户代码中指定哪些函数应该被内联。
  • 尽管编译参数-gcflags "-l"可以阻止任何函数被内联, 但是并没有正式的方式来避免某个特定的用户函数被内联。 目前有以下两种非正式的方法来避免一个函数被内联(这两种方式都有有可能在未来的Go标准编译器版本中失效):
    1. 你可以在函数声明前增加一行//go:noinline 指令来避免这个函数被内联。
    2. 由于包含循环块的函数不会内联,所以你可以再函数里增加一个空循环 for false {}来避免该函数被内联。 (但是这个方法从Go 1.13开始将不再有效。)

如何原子地操作指针值?

例如:
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
}
是的,目前指针的原子操作使用起来非常得繁琐。

如何使用尽可能短的代码行数来获取任意月份的天数?

假设输入的年份是一个自然年,并且输入的月份也是一个自然月(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)操作将使当前协程进入阻塞状态。

终结器(finalizer)可以用做对象的析构函数吗?

在Go程序里,我们可以通过调用runtime.SetFinalizer函数来给一个对象设置一个终结器函数。 一般说来,此终结器函数将在此对象被垃圾回收之前调用。 但是终结器并非被设计为对象的析构函数。 通过runtime.SetFinalizer函数设置的终结器函数并不保证总会被运行。 因此我们不应该依赖于终结器来保证程序的正确性。

终结器的主要用途是为了库包的维护者能够尽可能地避免因为库包使用者不正确地使用库包而带来的危害。 例如,我们知道,当在程序中使用完某个文件后,我们应该将其关闭。 但是有时候因为种种原因,比如经验不足或者粗心大意,导致一些文件在使用完成后并未被关闭,那么和这些文件相关的很多资源只有在此程序退出之后才能得到释放。这属于资源泄漏。 为了尽可能地避免防止资源泄露,os库包的维护者将会在一个os.File对象被被创建的时候为之设置一个终结器。 此终结器函数将关闭此os.File对象。当此os.File对象因为不再被使用而被垃圾回收的时候,此终结器函数将被调用。

请记住,有一些终结器函数永远不会被调用,并且有时候不当的设置终结器函数将会阻止对象被垃圾回收。 关于更多细节请阅读runtime.SetFinalizer函数的文档

调用stringsbytes标准库包里TrimLeftTrimRight函数经常会返回不符预期的结果,这些函数的实现存在bugs吗?

哈,我们不能保证这些函数的实现绝对没有bug,但是如果这些函数返回的结果是不符你的预期,更有可能的是你的期望是不正确的。

标准包stringsbytes里有多个修剪(trim)函数。 这些函数可以被分类为两组:
  1. TrimTrimLeftTrimRightTrimSpaceTrimFuncTrimLeftFuncTrimRightFunc。 这些函数将修剪首尾所有满足指定(或隐含)条件的utf-8编码的Unicode码点(即rune)。(TrimSpace隐含了修剪各种空格符。) 这些函数将检查每个开头或结尾的rune值,直到遇到一个不满足条件的rune值为止。
  2. TrimPrefixTrimSuffix。 这两个函数会把指定前缀或后缀的子字符串(或子切片)作为一个整体进行修剪。

部分程序员TrimLeftTrimRight函数当作TrimPrefixTrimSuffix函数而误用。 自然地,函数返回的结果很可能不是预期的那样。

例如:
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.Printfmt.Println 的区别是什么?

fmt.Println函数总会在两个相邻的参数之间输出一个空格,然而fmt.Print函数仅当两个相邻的参数(的具体值)都不是字符串类型时才会在它们之间输出一个空格。

另外一个区别是fmt.Println函数会在结尾写入一个换行符,但是fmt.Print函数不会。

函数log.Print 和函数 log.Println 有什么区别吗?

函数log.Printlog.Println的区别与上一个问题里描述的关于函数fmt.Printfmt.Println的第一个区别点类似。

这两个函数都会在结尾输出一个换行符。

函数fmt.Printfmt.Printlnfmt.Printf的实现进行同步了吗?

没有。 如果有同步的需求,请使用log标准库包里的相应函数。 你可以调用log.SetFlags(0)来避免每一个日志行的前缀输出。

内置的printprintln函数与fmtlog标准库包中相应的打印函数有什么区别?

除了上一个问题里提到的区别之外,这三组函数之间还有一些其他区别。
  1. 内置的print/println函数总是写入标准错误。 fmt标准包里的打印函数总是写入标准输出。 log标准包里的打印函数会默认写入标准错误,然而也可以通过log.SetOutput函数来配置。
  2. 内置print/println函数的调用不能接受数组和结构体参数。
  3. 对于组合类型的参数,内置的print/println函数将输出参数的底层值部的地址,而fmtlog标准库包中的打印函数将输出参数的字面值。
  4. 目前(Go 1.12),对于标准编译器,调用内置的print/println函数不会使调用参数引用的值逃逸到堆上,而fmtlog标准库包中的的打印函数将使调用参数引用的值逃逸到堆上。
  5. 如果一个实参有String() stringError() string方法,那么fmtlog标准库包里的打印函数在打印参数时会调用这两个方法,而内置的print/println函数则会忽略参数的这些方法。
  6. 内置的print/println函数不保证在未来的Go版本中继续存在。

标准库包math/randcrypto/rand生成的随机数之间有什么区别?

通过math/rand标准库包生成的伪随机数序列对于给定的种子是确定的。 这样生成的随机数不适用于安全敏感的环境中。 如果处于加密安全目的,我们应该使用crypto/rand标准库包生成的伪随机数序列。

标准库中为什么没有math.Round函数?

math.Round函数是有的,但是只是从Go 1.10开始才有这个函数。 从Go 1.10开始,标准库添加了两个新函数math.Roundmath.RoundToEven

在Go 1.10之前,关于 math.Round函数是否应该被添加进标准包,经历了很长时候的讨论

哪些类型不支持比较?

下列类型不支持比较:
  • 映射(map)
  • 切片
  • 函数
  • 包含不可比较字段的结构体类型
  • 元素类型为不可比较类型的数组类型

不支持比较的类型不能用做映射类型的键值类型。

请注意:
  • 尽管映射,切片和函数值不支持比较,但是它们的值可以与类型不确定的nil标识符比较。
  • 如果两个接口值的动态类型相同且不可比较,那么在运行时比较这两个接口的值会产生一个恐慌。

关于为什么映射,切片和函数不支持比较,请阅读Go的官方FAQ中关于这个问答

为什么两个nil值有时候会不相等?

(Go官方FAQ中的这个答案也回答了这个问题。)

一个接口值可以看作是一个包裹非接口值的盒子。被包裹在一个接口值中的非接口值的类型必须实现了此接口值的类型。 在Go中,很多种类型的类型的零值都是用nil来表示的。 一个什么都没包裹的接口值为一个零值接口值,即nil接口值。 一个包裹着其它非接口类型的nil值的接口值并非什么都没包裹,所以它不是(或者说它不等于)一个nil接口值。

当对一个nil接口值和一个nil非接口值进行比较时(假设它们可以比较),此nil非接口值将先被转换为nil接口值的类型,然后再进行比较; 此转换的结果为一个包裹了此nil非接口值的一个副本的接口值,此接口值不是(或者说它不等于)一个nil接口值,所以此比较不相等。

关于更详细的解释请阅读接口关于Go中的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没有共享相同底层类型,即使不同的类型T1T2共享相同的底层类型?

(不久前,Go官方FAQ也增加了一个相似的问题。)

在Go语言中,仅当两个切片类型共享相同的底层类型时,其中一个切片类型才可以转换成另一个切片的类型而不需要使用unsafe机制

一个非定义组合类型的底层类型是此组合类型本身。 所以即便两个不同的类型T1T2共享相同的底层类型,类型[]T1[]T2也依然是不同的类型,因此它们的底层类型也是不同的。这意味着其中一个的值不能转换为另一个。

底层类型[]T1[]T2不同的原因是:

同样的原因也适用于其它组合类型。 例如:类型map[T]T1map[T]T2同样不共享相同的底层类型,即便T1T2共享相同的底层类型。

类型[]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
}

哪些值可以被取地址,哪些值不可以被取地址?

以下的值是不可以寻址的:
  • 字符串的字节元素
  • 映射元素
  • 接口值的动态值(类型断言的结果)
  • 常量值
  • 字面值
  • 声明的包级别函数
  • 方法(用做函数值)
  • 中间结果值
    • 函数调用
    • 显式值转换
    • 各种操作,不包含指针解引用(dereference)操作,但是包含:
      • 数据通道接收操作
      • 子字符串操作
      • 子切片操作
      • 加法、减法、乘法、以及除法等等。
请注意:&T{}在Go里是一个语法糖,它是tmp := T{}; (&tmp)的简写形式。 所以&T{}是合法的并不代表字面值T{}是可寻址的。

以下的值是可寻址的,因此可以被取地址:
  • 变量
  • 可寻址的结构体的字段
  • 可寻址的数组的元素
  • 任意切片的元素(无论是可寻址切片或不可寻址切片)
  • 指针解引用(dereference)操作

为什么映射元素不可被取地址?

如果映射元素可以被取地址,则每个映射元素的地址在它的生命期内必须保持不变。 这阻碍了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里如何声明不可变量?

如下是三种不可变值的定义:
  1. 没有地址的值(所以它们不可以寻址)。
  2. 有地址但是因为种种原因在语法上不可以寻址的值。
  3. 可寻址但不允许在语法上被修改的值。

在Go语言中,直到现在(Go 1.12),没有值满足第三种定义。

有名常量值满足第一种定义。

方法和声明的函数可以被视为声明的不可变值。 它们满足第二种定义。字符串的字节元素同样满足第二种定义。

在Go中没有办法声明其它不可变值。

为什么没有内置的set容器类型?

集合(set)可以看作是不关心元素值的映射。 在Go语言里,map[Tkey]struct{}经常被用做一个集合类型。

什么是byte?什么是rune? 如何将[]byte[]rune类型的值转换为字符串?

在Go语言里,byteuint8类型的一个别名。 换言之,byteuint8是相同的类型。 runeint32属于同样类似的关系。

一个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是希腊字母表中的第九个字母。 在Go语言中,iota用在常量声明中。 在每一个常量声明组中,其值在该常量声明组的第N个常量规范中的值为N

为什么没有一个内置的closed函数用来检查数据通道是否已经关闭?

原因是此函数的实用性非常有限。 此类函数调用的返回结果不能总是反映输入数据通道实参的最新状态。 所以依靠此函数的返回结果来做决定不是一个好主意。

如果你确实需要这种函数,你可以不怎么费功夫地自己写一个。 请阅读如何优雅地关闭数据通道一文来了解如何编写一个closed函数以及如何避免使用这样的函数。

函数返回局部变量的指针是否安全?

是的,在Go中这是绝对安全的。

支持栈的Go编译器将会对每个局部变量进行逃逸分析。 如果编译器发现某个局部变量开辟在栈上不是绝对安全的,则此局部变量将被开辟在堆上。 请阅读内存块一文了解更多。

单词gopher在Go社区中表示什么?

在Go社区中,gopher表示Go程序员。 这个昵称可能是源自于Go语言采用了一个卡通小地鼠(gopher)做为吉祥物。 顺便说一下,这个卡通小地鼠是由Renee French设计的。 Renee French是Go项目首任负责人Rob Pike的妻子。

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏