Go 编译速度快的原因具体体现在三个方面:

  • Go 要求每个源文件在开头处显式地列出所有依赖的包导入,这样 Go 编译器不必读取和处理整个文件就可以确定其依赖的包列表;
  • Go 要求包之间不能存在循环依赖,这样一个包的依赖关系便形成了一张有向无环图。由于无环,包可以被单独编译,也可以并行编译;
  • 已编译的 Go 包对应的目标文件(file_name.o 或 package_name.a)中不仅记录了该包本身的导出符号信息,还记录了其所依赖包的导出符号信息。这样,Go 编译器在编译某包 P 时,针对 P 依赖的每个包导入(比如:导入包 Q),只需读取一个目标文件即可(比如:Q 包编译成的目标文件,该目标文件中已经包含了 Q 包的依赖包的导出信息),而无需再读取其他文件中的信息了。

使用 import 关键字导入依赖的标准库包或第三方包:

  1. package main
  2. import (
  3. "fmt" // 标准库包导入
  4. "a/b/c" // 第三方包导入
  5. )
  6. func main() {
  7. c.Func1()
  8. fmt.Println("Hello, Go!")
  9. }

很多 Gopher 看到上面代码都会想当然地将 import 后面的 “c”、“fmt” 与 c.Func1()和 fmt.Println() 中的 c 和 fmt 认作为同一个语法元素:包名。但在深入学习 Go 语言后,大家会发现事实并非如此。比如在使用实时分布式消息框架 nsq 提供的官方 client 包时,我们包导入这样来写:

  1. import "github.com/nsqio/go-nsq"

但在使用该包提供的导出函数时,我们使用的不是 go-nsq.xx 而是 nsq.xxx:

  1. q, _ := nsq.NewConsumer("write_test", "ch", config)

1. Go 程序构建过程

Go 程序的构建简单来讲也是由编译(compile)和链接(link)两个阶段组成的。

一个非 main 包在编译后会对应生成一个.a 文件,该文件可以理解为是 Go 包的目标文件(实则是通过 pack 工具($GOROOT/pkg/tool/darwin_amd64/pack)对 .o 文件打包后形成的 .a)。默认情况下在编译过程中 .a 文件生成在临时目录下,除非使用 go install 安装到 $GOPATH/pkg 下(Go 1.11 版本之前),否则你看不到 .a 文件。如果是构建可执行程序,那么 .a 文件会在构建可执行程序的链接阶段起使用。

标准库包的源码文件在$GOROOT/src 下面,而对应的 .a 文件存放在$GOROOT/pkg/darwin_amd64 下(以 MacOS 上为例;如果是 linux,则是 linux_amd64):

  1. // Go 1.13
  2. $tree -FL 1 $GOROOT/pkg/darwin_amd64
  3. ├── archive/
  4. ├── bufio.a
  5. ├── bytes.a
  6. ├── compress/
  7. ├── container/
  8. ├── context.a
  9. ├── crypto/
  10. ├── crypto.a
  11. ├── database/
  12. ├── debug/
  13. ├── encoding/
  14. ├── encoding.a
  15. ├── errors.a
  16. ├── expvar.a
  17. ├── flag.a
  18. ├── fmt.a
  19. ├── go/
  20. ├── hash/
  21. ├── hash.a
  22. ... ...

构建 Go 程序时,编译器会重新编译依赖包的源文件还是直接链接包的.a 文件呢?

  • 在使用第三方包的时候,当第三方包源码存在且对应的 .a 已安装的情况下,编译器链接的仍是根据第三方包最新源码编译出的 .a 文件,而不是之前已经安装到$GOPATH/pkg/darwin_amd64 下面的目标文件。

让 Go 编译器输出详细信息:

  1. $ go build -x -v *.go
  • 对于标准库中的包,编译器直接链接的是$GOROOT/pkg/darwin_amd64 下的.a 文件。

那么如何让编译器能去”感知“到标准库中的最新更新呢?以 fmt.a 为例,有两种方法:

  • 方法 1:删除$GOROOT/pkg/darwin_amd64 下面的 fmt.a,然后重新执行 go install fmt
  • 方法 2:使用 go build 的-a 命令行选项

2. 究竟是路径名还是包名

通过前面的实验,我们了解到编译器在编译过程中必然要使用的是编译单元(一个包)所依赖的包的源码。而编译器要找到依赖包的源码文件就需要知道依赖包的源码路径。这个路径由两部分组成:基础搜索路径包导入路径

基础搜索路径是一个全局的设置,下面是关于基础搜索路径的规则描述:

  • 所有包(无论标准库包还是第三方包)的源码基础搜索路径都包括 $GOROOT/src;
  • 在上述基础搜索路径的基础上,不同版本的 Go 包含的其他基础搜索路径有不同:
    • Go 1.11版本之前,包的源码基础搜索路径还包括 $GOPATH/src;
    • Go 1.11-Go 1.12 版本,包的源码基础搜索路径有三种模式:
      • 经典 gopath 模式下(GO111MODULE=off):$GOPATH/src ;
      • module-aware 模式下(GO111MODULE=on):$GOPATH/pkg/mod ;
      • auto 模式下(GO111MODULE=auto):在$GOPATH/src 路径下,与 gopath 模式相同;在$GOPATH/src 路径外且包含 go.mod,与 module-aware 模式相同。
    • Go 1.13 版本,包的源码基础搜索路径有两种模式:
      • 经典 gopath 模式下(GO111MODULE=off):$GOPATH/src;
      • module-aware 模式下(GO111MODULE=on/auto):$GOPATH/pkg/mod;
    • 未来的 Go 版本将只有 module-aware 模式,即只在 module 缓存的目录下搜索包的源码。

而搜索路径的第二部分就是位于每个包源码文件头部的包导入路径。基础搜索路径与包导入路径结合在一起,Go 编译器便可确定一个包的所有依赖包的源码路径的集合,这个集合构成了 Go 编译器的源码搜索路径空间。我们举个例子:

  1. // p1.go
  2. package p1
  3. import (
  4. "fmt"
  5. "time"
  6. "github.com/bigwhite/effective-go-book"
  7. "golang.org/x/text"
  8. "a/b/c"
  9. "./e/f/g"
  10. )
  11. ... ...

我们以 Go 1.11 版本之前的 GOPATH 模式为例,编译器在编译上述 p1 包时,会构建自己的源码搜索路径空间,该空间对应的搜索路径集合包括:

  1. - $GOROOT/src/fmt/
  2. - $GOROOT/src/time/
  3. - $GOROOT/src/github.com/bigwhite/effective-go-book/
  4. - $GOROOT/src/golang.org/x/text/
  5. - $GOROOT/src/a/b/c/
  6. - $GOPATH/src/github.com/bigwhite/effective-go-book/
  7. - $GOPATH/src/golang.org/x/text/
  8. - $GOPATH/src/a/b/c/
  9. - $CWD/e/f/g

源文件头部的包导入语句 import 后面的部分就是一个路径,路径的最后一个分段也不是包名。
**

  • 包导入路径的最后一段目录名最好与包名一致
  • 当包名与包导入路径中的最后一个目录名不同时,最好用下面语法将包名显式放入包导入语句
  1. // app2/main.go
  2. package main
  3. import (
  4. mypkg2 "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg2"
  5. )
  6. func main() {
  7. mypkg2.Func1()
  8. }

3. 同一源码文件的依赖包在同一源码搜索路径空间下包名冲突的问题

  1. // cmd/app3
  2. package main
  3. import (
  4. "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
  5. "github.com/bigwhite/effective-go-book/chapter3-demo2/pkg/pkg1"
  6. )
  7. func main() {
  8. pkg1.Func1()
  9. }

我们编译一下 cmd/app3:

  1. $go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app3
  2. # github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app3
  3. ./main.go:5:2: pkg1 redeclared as imported package name
  4. previous declaration at ./main.go:4:2

用为包导入路径下的包起别名的方法:

  1. package main
  2. import (
  3. pkg1 "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
  4. mypkg1 "github.com/bigwhite/effective-go-book/chapter3-demo2/pkg/pkg1"
  5. )
  6. func main() {
  7. pkg1.Func1()
  8. mypkg1.Func1()
  9. }