写在前面:这本书前前后后花了挺长时间,去年 11 月份就开始读了,中间又断了,直到最近才捡起来看完。

本书讲得内容非常全面,语言也很顺畅,生词非常少,并且内容没有太大难度,看起来比较过瘾,算是全面复习一下 Go 语言。如果你想开始练习阅读英文书,这本将是一个非常好的开始。

下面是阅读过程中记录的一些有用的点,随意看看就好。


  1. Go 有很多优点,其中一点是没有预编译阶段,这使得它的编译速度更快。像 C 语中,以 # 开头的会被预编译器处理。有预编译器的语言有:C, C++, Ada, and PL/SQL。预编译器的一大缺点是它会修改源代码,而人们不知道送到编译器里的最终的代码是什么。

  2. 可以直接在命令行执行 go doc strings.Fields 获取库函数的解释;执行 go get golang.org/x/tools/cmd/godoc 会安装 godoc 工具,注意这两者是不同的。前者是 go 命令,后者则是 godoc 命令。执行 godoc -http :8080 可以启动一个 server,访问 http://localhost:8080/pkg/ 即可看到 Go 的文档。

  3. 执行 go build 会显示生成一个可执行文件,仅仅一个 hello_world 就会达到 2M 大小,这是因为 Go 是静态链接,生成的文件可以直接执行,不需要再动态链接其他文件。而执行 go run 命令,虽然也会生成可执行文件,但是它是隐式的,之后当程序执行完后会被自动删掉。注意,看不见并不等于不存在!

  4. 所有的 UNIX 系统都支持:/dev/stdin/dev/stdout/dev/stderr 这三个特殊的文件名,它们也可以用 0、1、2 号文件描述符来描述。

  5. fmt.Println(), fmt.Print(), and fmt.Printf() 用于打印,fmt.Sprintln(), fmt.Sprint(), fmt.Sprintf() 用于生成字符串,fmt.Fprintln(), fmt.Fprint(), fmt.Fprintf() 用于写文件。

  6. 短赋值符 := 不能用于函数之外,因此全局变量只能用 var 声明。

  7. 下面的代码用于从标准输入读取数据,每读出一行就打印出来:

func main() {
	var f *os.File
	f = os.Stdin
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		fmt.Println(">", scanner.Text())
	}
}

按 ctrl+D 退出循环,因为 ctrl+D 会告知程序没有更多的数据可以读取。

  1. os.Args 可以记录通过命令行输入的参数,并且它的类型是 []string,第一个元素是程序名,之后的为输入参数。例如:
go run a.go 10 1

os.Args[/tmp/go-build059507490/b001/exe/cla 10 1]

  1. 关于 docker 的命令:
# 根据 tag 创建
docker build -t go_hw:v1 .

# 列出所有的 docker images
docker images

# 运行
docker run go_hw:v1

# 删除(-f 强制删除)
docker rmi 5a0d2473aa96 f56365ec0638
  1. 关于 Go 的垃圾回收算法:并发标记清除、非分代、非整理,使用写屏障。

  2. Go 为了降低 GC 的停顿时间,让 GC 和用户程序并发执行。为了让三色标记的结果不受并发执行的程序的影响,在整个标记过程中,要确保一个不变性:黑色集合里的对象保证不会指向白色集合里的对象,注意这并不影响一个白色对象指向黑色对象。我们把用户程序称为 mutator,mutator 运行了一个 writer barrier,每次当堆上有对象的指针(如果是对象的非指针字段变化,不影响)发生了变化,说明此对象可达,就要运行 writer barrier,将它变成灰色。mutator 通过 writer barrier 保证“黑色集合里的对象保证不会指向白色集合里的对象”这一不变性。这会带来性能的损耗,但这是并发执行用户程序和 GC 的代价。

  3. 垃圾回收器会在 channel 不可达时回收它,即使 channel 还未关闭。

  4. time go run xx.go 可以计算运行程序花费的时间。

  5. Please remember that at the end of the day, all programs that work on UNIX machines end up using C system calls to communicate with the UNIX kernel and perform most of their tasks. 所有在 UNIX 系统上运行的程序最终都会通过 C 系统调用来和内核打交道。用其他语言编写程序进行系统调用,方法不外乎两个:一是自己封装,二是依赖 glibc、或者其他的运行库。Go 语言选择了前者,把系统调用都封装到了 syscall 包。封装时也同样得通过汇编实现。

  6. strace ls 查看都有哪些系统调用,-c 可以计数。

  7. 将 .go 文件转化成汇编代码时,可指定操作系统和架构:

# 两者等价
GOOS=darwin GOARCH=amd64 go tool compile -S goEnv.go
GOOS=darwin GOARCH=amd64 go build -gcflags -S goEnv.go

GOOS 和 GOARCH 可选项为:The list of valid GOOS values includes android, darwin, dragonfly, freebsd, linux, nacl, netbsd, openbsd, plan9, solaris, windows, and zos. On the other hand, the list of valid GOARCH values includes 386, amd64, amd64p32, arm, armbe, arm64, arm64be, ppc64, ppc64le, mips, mipsle, mips64, mips64le, mips64p32, mips64p32le, ppc, s390, s390x, sparc, and sparc64.

  1. go build -x defer.go 展示 build 过程。

  2. 数组可以用 “:” 变成切片:array4[0:] 或 array4[:],copy 函数只接收切片作为参数。

  3. 什么时候使用指针:1. 可以 share data,尤其是在函数之间;2. 区别某个变量是未设置还是真的零值。

  4. 关于 strings 有很多有意思的方法,例如 Repeat, Fields 等等,在这里可以看到很多。

  5. Go container 包有 heap/list/ring 这几个组件。

  6. math/rand 可用于生成伪随机数;更安全的生成随机数:crypto/rand

  7. 关于可变参数的函数(A variadic function),...Type 称为 pack operator,而 Slice... 则被称为 unpack operator。一个可变参数的函数只能使用一次 pack operator。

  8. 安装一个本地包:

$ mkdir ~/go/src/aPackage
$ cp aPackage.go ~/go/src/aPackage/
$ go install aPackage
$ cd ~/go/pkg/darwin_amd64/
$ ls -l aPackage.a
-rw-r--r-- 1 mtsouk staff 4980 Dec 22 06:12 aPackage.a

编译一个本地包:

$ go tool compile aPackage.go
$ ls -l aPackage.*
[email protected] 1 mtsouk staff 201 Jan 10 22:08 aPackage.go -rw-r--r-- 1 mtsouk staff 16316 Mar 4 20:01 aPackage.o
  1. 关于 Go 版本,例如 v1.2.3,v1/v2/v3 通常是不兼容的,1 表示大版本,2 表示 feature,3 表示 fix。

  2. 如何和 gomod 工作:

$ go mod init
go: creating new go.mod: module github.com/mactsouk/myModule 
$ touch myModule.go
$ vi myModule.go
$ git add .
$ git commit -a -m "Initial version 1.0.0"
$ git push
$ git tag v1.0.0
$ git push -q origin v1.0.0
$ go list
github.com/mactsouk/myModule
$ go list -m
github.com/mactsouk/myModule
  1. 创建 v2 版本:
git commit -a -m "using v2.0.0"
git tag v2.0.0
git push --tags origin v2
git --no-pager branch -a
  1. 使用 go mod vendor 命令来将依赖放到 vendor 文件夹里:
go mod init useV1V2
go mod vendor
  1. 查找哪些 go 源文件使用了 syscall:
grep \"syscall\" `find /usr/local/go/src -name "*.go"`
  1. 要记住的是在绝大部分程序里不需要使用反射,所以我们得弄清楚为什么反射是必须的以及什么时候需要使用反射。反射在实现 fmt, text/template, html/template 时是必须的。例如在 fmt 包里,反射可以让你不需要明确处理所有的类型,你当然可以明确处理你知道的所有类型,但你仍然不可能处理 All possible types。

  2. 什么时候用反射:Therefore, you might need to use reflection when you want to be as generic as possible or when you want to make sure that you will be able to deal with data types that do not exist at the time of writing your code but might exist in the future. Additionally, reflection is handy when working with values of types that do not implement a common interface.

  3. 反射不好的三点:a. 大量的反射会造成程序代码难以理解和维护。一个可行的解决方法是清晰的文档注释,但众所周知,程序员是最不愿意写文档的人;b. 相比正常的数据结构,反射是动态地“决定”数据结构,因此会更慢。这些动态代码也会使得一些代码工具更难执行重构和分析;c. 反射的错误在 build 期间不会被捕获,很多都是在运行期间直接 crash 整个程序。而且这经常是在程序正常运行数月甚至是数年之后才会爆发。一个可行的办法是大量的测试,但这也不太可能覆盖完全,并且会让代码库更加庞大。

  4. Go 不是一门面向对象的语言,但它可以模拟面向对象语言的某些功能。

  5. flag.var 可以解析用逗号分隔的多个值。

  6. wc 命令的结果有三列,分别表示行数、word 数,以及字节数。平时用的最多的是 wc -l,表示行数;wc -w 表示 wrod 数;wc -c 表示字节数。

  7. 如何输出一个文件的权限,上代码:

package main

import (
	"fmt"
	"os"
)

func main() {
	arguments := os.Args
	if len(arguments) == 1 {
		fmt.Printf("usage: permissions filename\n")
		return
	}

	filename := arguments[1]
	info, _ := os.Stat(filename)
	mode := info.Mode()
	fmt.Println(filename, "mode is", mode.String()[1:10])
}
  1. crtl+C 向进程发送 SIGINT 信号。Unix 里的信号其实都是软中断,用来异步处理“事件”,信号可以通过 name 和 number 来识别。进程不可能处理所有类型的信号,有些信号不能被 caught,不能被 blocked,例如 SIGKILL、SIGSTOP 不能被 caught,不能被 blocked,也不能被 ignored。因为它们给内核和 root 用户提供了特权,可以停止运行某些进程。一般我们建议用信号的 name 来操作,例如 kill -s INT pid。有个例外的是 SIGKILL,它对应的 number 是 9,例如我们经常执行 kill -9 pid 来杀死某个进程,它等价于 kill -s KILL pid

  2. 最常用来发送信号的方式用 kill 命令,默认发送的是 SIGTERM 信号。kill -l 命令可以列出所有支持的信号。

  3. go run -race xx.go 可以显示有竞争冲突的代码。

  4. diff pipeline.go plNoRace.go --color 显示两个文件的 diff。

  5. Go 语言的并发模型是 fork-join 型的。使用 go 关键字启动子协程工作,使用 sync.Wait 和 channel 来收集结果。

  6. 可通过设置环境变量来改变 runtime.GOMAXPROCS(0) 的输出值:export GOMAXPROCS=800;

  7. sync.RWMutex 结构体里包含 sync.Mutex,即“读写锁”是在“锁”的基本上实现的。只有当所的读锁都 Unlock 了,写锁才能被 Lock。

  8. time go run xx.go 可以显示执行时间,包括 real, sys, user 的执行时间。

  9. 如果子 context 取消了,父 context 没有收到消息,那么在父 context 取消前就发生了内存泄露。

For garbage collection to work correctly, the parent goroutine needs to keep a reference to each child goroutine. If a child goroutine ends without the parent knowing about it, then a memory leak occurs until the parent is canceled as well.

  1. TAOCP——《计算机程序设计艺术》的作者高德纳(Donald Ervin Knuth)老爷子的一句经典的话:

“The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.”

告诉我们不要老想着性能优化,在真的出现问题、出现瓶颈的时候再来考虑。

还有 Erlang 的作者之一 Joe Armstrong:

“Make it work, then make it beautiful, then if you really, really have to, make it fast. 90 percent of the time, if you make it beautiful, it will already be fast. So really, just make it beautiful!”

这告诉我们性能优化并不是主要工作,我们不要花费大量精力在这上面。

  1. 做优化的前提是程序没有 bug,所以如果你在程序的第一版就来优化是有问题的,因为 v1 版本可能经常有 bug。

  2. 交叉编译命令:env GOOS=linux GOARCH=386 go build xCompile.go。指定操作系统、指令集。

  3. 通过 bytes 包的例子,可以看懂 godoc 和源码里的 comments 的对应关系。pkg/bytes 文档里有很多代码样例,还可以 run 一下,但其实这些样例是写死在源码里的,就在 src/bytes/example_test.go 文件里。一开始没发现这个文件,我直接拿样例代码全局搜,一下就找到了。例如,源码写的如下两个 example:

// src
func ExampleToTitleSpecial() {
	str := []byte("ahoj vývojári golang")
	totitle := bytes.ToTitleSpecial(unicode.AzeriCase, str)
	fmt.Println("Original : " + string(str))
	fmt.Println("ToTitle : " + string(totitle))
	// Output:
	// Original : ahoj vývojári golang
	// ToTitle : AHOJ VÝVOJÁRİ GOLANG
}

pkg 上,就对应这样:

go pkg example