一探鹅厂研发小哥的 GO语言工具链学习笔记
导语:Golang 语言的优势非常吸引初期团队作为主要语言选型,并且 Golang 自己带有完整的工具链。但是一些公司内的项目管理和 Golang 部分设计初衷存在一些背离,因此期望能够深入 Golang 的工具链使其和谐。本文并非编译原理探究,主要是解决工具链应用场景。
0x01 Perpare
Environment variables
- GOOS:程序构建环境的目标操作系统。
- GOARCH:程序构建环境的目标计算架构。
- GOHOSTOS:程序运行环境的目标操作系统。
- GOHOSTARCH:程序运行环境的目标计算架构。
- GOROOT:Golang 安装根目录。
- GOPATH:Golang 工作区目录,可以包含多个目录,每一个工作区目录之下包含三个子文件夹。
workspace // GOPATH |-- bin // 默认的可执行文件安装目录 |-- pkg // 默认的库归档文件安装目录 |-- src // 源代码目录
- GOBIN:Golang 可执行文件安装目录。(:warning:不建议 PATH 包含该目录)
- 如果设置了 GOBIN,编译后的可执行文件将不会安装到 GOPATH 下的 bin 目录。
- 如果 GOPATH 包含了多个工作区目录,必须设置 GOBIN。
Commands
- compiler
- go build:compile packages and dependencies
- go install:compile and install packages and dependencies、
- go clean:remove object files
- go run:compile and run Go program
- go test:test packages
- packager
- go get:download and install packages and dependencies
- go mod: module maintenance
- go list:list packages
- analyzer
- go vet:run go tool vet on packages
- go tool pprof
0x02 Build Procedure
run && build && install
通过 go ${subcommand} -x 观察编译过程,三个子命令都是先创建临时工作区目录,使用 $GOROOT/libexec/pkg/tool/$GOOS_$GOARCH/complie(go tool complie) 工具编译生成名为 command-line-arguments 库文件,然后再通过 $GOROOT/libexec/pkg/tool/$GOOS_$GOARCH/link(go tool link) 工具链接生成目标可执行文件。
- go run
- 仅针对 package main 的 Go 文件,且不支持 testing Go 文件。
- 生成的相关库文件与可执行文件仅保留在临时工作区目录,并且会直接执行目标可执行文件。
- go build
- 针对 package main 的 Go 文件会生成可执行文件,并且将可执行文件复制到当前目录(不是源代码所在目录)。
- 针对其他 Go 文件只会做检查性的编译,不会链接生成可执行文件,也不会将库文件复制到当前目录。
- go install
- 针对 package main 的 Go 文件会生成可执行文件,并且将相关库文件以包目录结构复制到 $GOPATH/pkg/ 目录,可执行文件将直接复制到 $GOPATH/bin 或 $GOBIN 目录。
- 针对其他 Go 文件会将相关库文件以包目录结构复制到 $GOPATH/pkg/ 目录。
学习笔记
- Golang 已经提供了非常完整的工具链支持,还可以使用 go tool complie 和 go tool link定制个性化的编译和链接过程。
- 其中 go build、go run 等子命令还支持 -o 选项可以指定生成的可执行文件的输出目录,不完全依赖 GOPATH 和 GOBIN。
- Golang 也提供了编译动态连接库的模式,可以满足特定场景的需求。
- Golang 通过 GOOS 与 GOARCH 两个环境变量可以指定交叉编译目标系统和平台,前提是当前安装的 Golang 工具链已经包含目标平台交叉编译工具链。
0x03 Package Maneragement
GOPATH && go get
- GOPATH 是工作区目录,编译过程中会按照 GOPATH 中路径的顺序寻找对应的依赖包。
- go get 会根据包地址将包代码下载到 GOPATH 中首个路径的 src 目录。
这是最初版本的 Golang 包管理,可以通过 GOPATH 管理业务代码与依赖库目录。
Godeps && vendor
- 2013 年,Google 提供了 godep 工具,扫描项目代码生成 Godeps.json 依赖关系文件,并将项目的依赖库源代码复制保存在 Godeps/_workspace 目录(后期也使用 vendor 目录),并将该目录作为 GOPATH 解决包依赖问题。
- 2016 年,Google 发布 Golang 1.6 版本,增加了 vendor 目录作为 GOPATH 的默认路径,社区提供了多种依赖管理方案,以 glide 和 govendor 为主要代表。
Google 官方开始介入 Golang 包管理乱象,但是解决方案并未能够得到普遍共识。
go modules / vgo
- vgo 只是 go modules 在正式发布之前的代号,Google 在 Golang 1.11 版本提供了新的包管理官方解决方案。
- go mod 工具通过维护 go.mod 文件管理包依赖,并支持版本管理。
- go mod 工具不再完全依赖 GOPATH,同时提供 GOPATH 兼容模式。
go modules 的一般使用过程。
学习笔记
Golang 提供了官方的的包管理方案,可以从这些方案的发展过程中窥探到包管理的需求。
- 简单的包获取方式:go get 很好的满足的包获取,同时也提供包别名方案。
- 完整的包管理工具:godep 和 go mod 工具对于已获取的包能够很好的组织工程项目。
- 便捷的包依赖分析:Godeps.json 和 go.mod 文件与相应工具支持包依赖分析。
- 有效的包版本控制:go mod 已经支持包版本控制。
0x04 Thirdparty Dependences
Protobuf
- protoc 生成 Go 文件,需要使用 protoc-gen-go 插件。如果需要支持 service 生成,需要使用 grpc 子插件。
- Go 语言独立的包依赖,所以需要在 .proto 文件指定 go_package 选项。
学习笔记
- protoc-gen-go 插件通过 --go_out=$key=$value{,...}:$outpath 形式接收插件参数。
- plugins:子插件列表,例如 grpc 子插件。
- import_prefix:包引用前缀。
- import_path:包引用路径。
- paths:包引入路径类型,import 与 source_relative 两种类型,默认 source_relative。
- annotate_code:注释代码开关。
- Protobuf FileDescriptor Options
- go_package:指定生成 Golang 包的包名。
注意 Protobuf 包与 Golang 包相互关系,可以通过指定 Protobuf 文件中的 go_package 以及指定 protoc-gen-go 工具相关参数组织合理的工程目录。
cgo
- 堆栈切换存在性能隐患。
- 因为 C 语言的绑定,无法支持交叉编译。
- Golang 相关调试工具将会无法深入调试。
学习笔记
不要轻易使用 cgo 技术,但是对于公司内部的公共组件使用可能这是唯一的解决方式,如果使用需要注意主机操作系统与系统库版本。
0x05 Bazel && Blade
大型工程项目都会选择相应的构建系统来编译整个工程,Google Bazel 与 typhoon/blade 都是非常好的构建系统选型,现阶段都对 Golang 构建有相应的支持。
Bazel 已经很好的支持了 Golang 工程构建,并且还支持引入外部源,由于自己的工程并没有深入使用 Bazel 系统,所以此处并不作深入的分析。
Blade 现在对于 Golang 工程构建比较初级。
- 需要在 go_config 中配置 go_home 配置作为 GOPATH,并且不能指定多个 GOPATH,也不接收通过环境变量直接指定的 GOPATH。
- Blade 实现 Golang 相关编译是通过调用 go build、go install、go test 三个子命令,也并未对交叉编译有明确的支持。
- 公司内的网络策略也不支持在代码编译机器获取外部包依赖,同时内部没有也不可能搭建 Golang 包镜像,所以 Blade 并未实现外源支持。
- Protobuf Golang 编译目标需要明确指定 go_package,强约束工程目录组织结构。
题外话,为什么需要一个构建系统?如果使用构建系统,为什么选择 Blade 而非 Bazel?
- 项目通过多语言实现,使用构建系统可以统一构建关系。配合协议描述语言生成代码工具,可以打通多语言交流壁垒。
- Blade 是内部开源项目,部分功能满足公司内部特定场景需求。Bazel 适合类似 Google 统一代码库的超大型工程。两者差异化不大,所以选择了小而美,更适合公司内部使用的 Blade。
0xFF Final
通过对 Golang 工具链的学习了解,来尝试解决一下现阶段面临的问题。
- 如何解决工程依赖的外部包?
- 对于依赖的外部包只能保存在代码库,因为持续编译集成环境无法访问外网。相当于对外部依赖库进行 Snapshot。
- 现阶段工程使用的是 Blade 构建系统,不能支持多个 GOPATH 指定,所以所有外部依赖包均保存在 go_home 所指定的目录。
- git.code.oa.com 开源的 Golang 依赖包将会以 tencent.com 域名引用。
- go modules 不是强制使用,但是为了项目标准化建议符合规范。
- 如何构建当前 Golang 工程?
- Protobuf Golang 目标构建分两个过程,首先是通过 protoc 工具指定 GOPATH 为输出目录生成 .pb.go 文件,其次是通过 Golang 编译工具链编译生成静态链接库。
- Golang 工具链编译过程会编译依赖库,链接可执行文件的过程链接依赖库文件,并不需要 Blade 构建系统显式指定。但是,依赖的 proto_library 需要显式指定,因为 Golang 编译过程并不会生成 Protobuf 文件对应的 Go 文件。
- 项目内实现的 go_library 要求强制显式声明,便于 proto_library 和 gen_rule 等依赖的传递。
Reference
1.《初探 Go 的编译命令执行过程》https://halfrost.com/go_command/
2.《GO 命令教程》https://github.com/hyper0x/go_command_tutorial
3.《Go 包管理的前世今生》https://www.infoq.cn/article/history-go-package-management
4.《初窥Go module》https://tonybai.com/2018/07/15/hello-go-module/