科技改变生活 · 科技引领未来
写在前面go语言开发者在日常工作或学习中,用到最多的命令可能是gobuild、gorun、goinstall、goget...,通过这些命令可以帮我们编译程序、运行程序、安装程序和获取代码包,然而在执行这些命令的时候,你是否思考过go编译过
写在前面
go语言开发者在日常工作或学习中,用到最多的命令可能是go build、go run、go install、go get...,通过这些命令可以帮我们编译程序、运行程序、安装程序和获取代码包,然而在执行这些命令的时候,你是否思考过go编译过程中是如何组织我们的源码文件的?go install的时候发生了什么?以及go get只是去下载我们依赖的包文件吗?go还有哪些实用的命令?这是一篇可以说比较基础的文章,也可以作为go命令的一个速查手册,旨在帮助我们更深刻的了解go。
几个概念
gopath可能是每个go开发者最熟悉不过的东西了,gopath就是我们的工作区,通过go env可以查看到我们的gopath,如 GOPATH="/Users/code/go",gopath就是我们的工作区,gopath下面一般会建立bin、src、pkg三个文件夹。
export GOBIN=/tmp //声明GOBIN目录 go install $GOPATH/src/code //安装code程序 /tmp/code 发现程序已经被安装到了GOBIN下
常见辅助命令
go有一些辅助命令可以帮助我们更好的理解go的一些执行过程,辅助命令的体现就是go run/build.. -x xx.go, -x就是辅助命令。-x可以有以下类型:
小众辅助命令
编译
go run
我们通过go run -n main.go来看下构建的过程
mkdir -p $WORK/b001/ cat >$WORK/b001/importcfg.link << 'EOF' # internal packagefile command-line-arguments=/Users/gopher/Library/Caches/go-build/f5/f58206d3722b7787f6341e5013f38f3887e44138ffbdafbb07b67da377632762-d packagefile code/utils=/Users/gopher/Library/Caches/go-build/89/8931b87411d98f32ea6222ad605f3367700be896828286782db3e51de73217ff-d packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a ... EOF mkdir -p $WORK/b001/exe/ cd . /usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/main -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=ttUV5epeZDG7q3yVlG2A/ek8iSJ8rjD-RiujnYKAd/hNomEMXuvdA3I1ks6XOE/ttUV5epeZDG7q3yVlG2A -extld=clang /Users/goper/Library/Caches/go-build/f5/f58206d3722b7787f6341e5013f38f3887e44138ffbdafbb07b67da377632762-d $WORK/b001/exe/main
通过 go run -work main.go,我们来看看临时文件夹:
go run -work main.go WORK=/var/folders/s4/2cpbmp4s1_j4y3zv2s08m9q40000gn/T/go-build281107053
切到临时目录:
└── b001 ├── exe │ └── main └── importcfg.link
默认情况下,go run命令运行完后会删除临时文件夹。
go build
go build用于编译我们的程序,默认编译后的文件存放在当前的文件夹中,如果指定-o那么就可以移动到指定的文件中。我们通过go build -n main.go来看下build的过程:
# # command-line-arguments # mkdir -p $WORK/b001/ cat >$WORK/b001/_gomod_.go << 'EOF' # internal package main import _ "unsafe" //go:linkname __debug_modinfo__ runtime.modinfo var __debug_modinfo__ = "0wxaffx92tbx02Axe1xc1axe6xd6x18xe6pathtcommand-line-argumentsnmodtcodet(devel)tnxf92C1x86x18 rx00x82Bx10Ax16xd8xf2" EOF cat >$WORK/b001/importcfg << 'EOF' # internal # import config packagefile code/utils=/Users/sunkang/Library/Caches/go-build/89/8931b87411d98f32ea6222ad605f3367700be896828286782db3e51de73217ff-d packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a packagefile runtime=/Users/sunkang/Library/Caches/go-build/b4/b44856e241a6bb3baf596eb19e4566e956a490ef403c1ed31ba8f014542fcf81-d EOF cd /Users/gopher/go/src/code /usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -lang=go1.15 -complete -buildid Fjkl2yr7MirhGqbO0lrl/Fjkl2yr7MirhGqbO0lrl -goversion go1.15.3 -D _/Users/sunkang/go/src/code -importcfg $WORK/b001/importcfg -pack -c=4 ./main.go $WORK/b001/_gomod_.go /usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cat >$WORK/b001/importcfg.link << 'EOF' # internal packagefile command-line-arguments=$WORK/b001/_pkg_.a packagefile code/utils=/Users/sunkang/Library/Caches/go-build/89/8931b87411d98f32ea6222ad605f3367700be896828286782db3e51de73217ff-d packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a ... EOF mkdir -p $WORK/b001/exe/ cd . /usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=s0BcVGdGaAeuHGFL7teJ/Fjkl2yr7MirhGqbO0lrl/Fjkl2yr7MirhGqbO0lrl/s0BcVGdGaAeuHGFL7teJ -extld=clang $WORK/b001/_pkg_.a /usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out main
整体流程和go run差不多,唯一不同的是在compile和link之后,生成的可执行文件会移动到当前文件夹中,并不是随着临时文件夹一起消亡。
go install
go install用于编译并安装指定的代码包及它们的依赖包,当指定的代码包的依赖包还没编译安装的时候,会先去安装依赖包。与go build不同的是,go install会把编译后的安装包放在指定的文件夹中。安装的代码包会在当前工作区的pkg目录下,即.a的归档文件,当我们没有设置GOBIN时,安装的命令源码文件会存放在当前工作区的bin目录下,当我们设置了GOBIN时,则会放在GOBIN下。 假设现在项目是这样的:
├── go.mod ├── main.go └── utils └── utils.go
main.go就是我们的入口文件,即命令源码文件,utils是我们的依赖包,即库源码文件。 当我们只在当前目录执行go install -n main.go后:
mkdir -p $WORK/b001/ cat >$WORK/b001/importcfg.link << 'EOF' # internal ... EOF mkdir -p $WORK/b001/exe/ cd . /usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=yfU8nbngCa6KbgUlJLKa/Fjkl2yr7MirhGqbO0lrl/khh18opCAdXA909bR95q/yfU8nbngCa6KbgUlJLKa -extld=clang /Users/sunkang/Library/Caches/go-build/ed/ed5868e69c99c66b8bf4b399e989ea410063b44143b546fd3f4a98f758d73a47-d /usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal mkdir -p /Users/gopher/go/bin/ mv $WORK/b001/exe/a.out /Users/gopher/go/bin/main
可以发现最后一行与go build的不同的是,它把可执行文件移动到了GOPATH/bin中。当我们切到依赖包utils文件中执行 go install -n后:
mkdir -p $WORK/b001/ mkdir -p /Users/gopher/go/pkg/darwin_amd64/code/ mv /Users/gopher/Library/Caches/go-build/89/8931b87411d98f32ea6222ad605f3367700be896828286782db3e51de73217ff-d /Users/gopher/go/pkg/darwin_amd64/code/utils.a
可以发现会把依赖包安装到 GOPATH/pkg下,并且命名为.a结尾的归档文件。darwin_amd64是$GOOS_$GOARCH的拼装,即操作系统和处理器架构,code就是我们的项目名。
/Users/gopher/go/pkg/darwin_amd64/code └── utils.a
所以go install大概流程是这样的:
go get
通过go get 命令我们可以下载并安装一个依赖包,go get下载的源码文件会放在GOPATH中的第一个工作区。由于go在1.11开始支持go mod,所以当我们开启go mod后,通过go get获取的代码会下载在GOPAT/pkg/mod中,go get后面支持可选参数
go mod
go从1.11开始支持go mod模式,现在相信大家也基本都在使用go mod,相比vendor的好处,使用go mod之后,你的代码可以存在在任何位置。
go mod相关命令
//go mod download -json { "Path": "github.com/go-basic/uuid", "Version": "v1.0.0", "Info": "/Users/gopher/go/pkg/mod/cache/download/github.com/go-basic/uuid/@v/v1.0.0.info", "GoMod": "/Users/gopher/go/pkg/mod/cache/download/github.com/go-basic/uuid/@v/v1.0.0.mod", "Zip": "/Users/gopher/go/pkg/mod/cache/download/github.com/go-basic/uuid/@v/v1.0.0.zip", "Dir": "/Users/gopher/go/pkg/mod/github.com/go-basic/uuid@v1.0.0", "Sum": "h1:Faqtetcr8uwOzR2qp8RSpkahQiv4+BnJhrpuXPOo63M=", "GoModSum": "h1:yVtVnsXcmaLc9F4Zw7hTV7R0+vtuQw00mdXi+F6tqco=" }
编辑go.mod文件 选项有-json、-replace...,可以使用帮助go help mod edit,比如说如果你要修改某个包,可以直接使用 go mod edit -replace=old[@v]=new[@v],一般都是直接编辑go.mod文件了,这个命令用的不多。
以文本模式打印依赖的包,比如我的go.mod是
module code go 1.15 require ( github.com/gin-gonic/gin v1.7.4 // indirect github.com/go-basic/uuid v1.0.0 // indirect )
这时执行go mod graph
//go mod graph code github.com/gin-gonic/gin@v1.7.4 code github.com/go-basic/uuid@v1.0.0 github.com/gin-gonic/gin@v1.7.4 github.com/gin-contrib/sse@v0.1.0 github.com/gin-gonic/gin@v1.7.4 github.com/go-playground/validator/v10@v10.4.1 github.com/gin-gonic/gin@v1.7.4 github.com/golang/protobuf@v1.3.3 github.com/gin-gonic/gin@v1.7.4 github.com/json-iterator/go@v1.1.9 github.com/gin-gonic/gin@v1.7.4 github.com/mattn/go-isatty@v0.0.12 github.com/gin-gonic/gin@v1.7.4 ... golang.org/x/text@v0.3.2 golang.org/x/tools@v0.0.0-20180917221912-90fa682c2a6e
发现还是看不出依赖关系,这里推荐大家使用 go get -u github.com/PaulXu-cn/go-mod-graph-chart/gmchart这个包,来查看依赖关系:
添加丢失或移出不需要的模块。当前我的go.mod里面有个uuid的包,但是我的代码并没有引用。
module code go 1.15 require github.com/go-basic/uuid v1.0.0 // indirect
执行go mod tidy:
module code go 1.15
会发现帮我移除了不需要的包。
6.go mod verify验证依赖是否正确。7. go mod why解释为什么需要包和模块,比如执行: go mod why github.com/go-basic/uuid,然后输出:
# github.com/go-basic/uuid code/utils github.com/go-basic/uuid
我的理解是 code/utils这个包有用到github.com/go-basic/uuid。
go.sum
go.sum文件的作用就两个:
当我们go get某个包的时候,会先下载到本地$GOPATH/pkg/mod/cache/download中,下载下来后会有一个名为vx.x.x.zip的压缩包,以及vx.x.x.ziphash的文件,vx.x.x.ziphash内容就是vx.x.x.zip经过hash的值,比如: h1:jwqTeEM3x6L9xDXrCxN0Hbg7vdGfPBOTIkr0+/LYZDA=% 以uuid包为例子:当我们go get github.com/go-basic/uuid后,除了会在go.mod里追加一条require命令后,还会在go.sum里面写入两条记录:
github.com/go-basic/uuid v1.0.0 h1:Faqtetcr8uwOzR2qp8RSpkahQiv4+BnJhrpuXPOo63M= github.com/go-basic/uuid v1.0.0/go.mod h1:yVtVnsXcmaLc9F4Zw7hTV7R0+vtuQw00mdXi+F6tqco=
第一条hash就是我们上面提到的zip压缩包的hash值,第二条hash是如果我们的依赖包中有go.mod,那么就是这条go.mod的hash值。在准备把两条hash值记录更新到go.sum中的时候,为了确保依赖包的真实可靠性,go在下载完依赖包后,会通过go的环境变量GOSUMDB="sum.golang.org"指向的服务器去检查依赖包的hash值,如果查询的hash值和本地的hash值不一样,那么就拒绝向下执行,也不会更新go.sum。
go clean
//go clean -i -n cd /Users/gopher/go/job // 当前项目 rm -f job job.exe job.test job.test.exe main main.exe rm -f /Users/gopher/go/bin/job
先切到我们的项目中去,然后尝试删除当前目录下的一些编译文件如.exe、.test等,最后去尝试删除$GOPATH/bin/job这个因为go install产生的编译文件。
// go clean -r -n cd /Users/gopher/go/job rm -f job job.exe job.test job.test.exe main main.exe cd /usr/local/go/src/fmt rm -f fmt.test fmt.test.exe cd /usr/local/go/src/errors rm -f errors.test errors.test.exe cd /usr/local/go/src/internal/reflectlite rm -f reflectlite.test reflectlite.test.exe ....
job项目依赖很多包,这些依赖的包也会执行删除一些当前目录的编译文件。
go build的过程是产生一些缓存的,这些缓存是存在go的环境变量GOCACHE中的 -cache就是删除相关的缓存的:
//go clean -n -cache rm -r /Users/gopher/Library/Caches/go-build/00 /Users/gopher/Library/Caches/go-build/01 /Users/gopher/Library/Caches/go-build/02 /Users/gopher/Library/Caches/go-build/03 /Users/gopher/Library/Caches/go-build/04 ... rm -r /Users/gopher/Library/Caches/go-build/ff
当我们使用go test .来跑某个路径下面的测试用例时,会编译并测试路径下每个测试文件,并且会缓存测试结果,以避免不必要的重复测试,当缓存成功后,第二次跑test 会发现有个cached标识。
第一次 go test . ok job 0.431s 第二次 go test . ok job (cached)
这时候通过go clean -testcache 就是删除对应的测试缓存。
go clean -testcache go test . ok job 0.459s
当我们启动go.mod模式来组织我们的go代码时,下载的依赖包会放在$GOPATH/pkg/mod中,通过 go clean -modcache就是删除$GOPATH/pkg/mod下的所有文件。
//go clean -n -modcache rm -rf /Users/gopher/go/pkg/mod
结语
写本文的目的主要是因为自己知识的匮乏,自己用go也两年了,然而对go的一些命令、go的编译过程还是存在模糊感的,比如项目一直使用的go.mod,go.mod是如何管理我们依赖的代码包的,go.sum是什么?为什么需要go.sum?go build的过程发生了什么?另一方面本文也列举了基本日常开发够用的命令,这样当自己需要查找命令不用去网上百度了,可以作为一个速查手册。也许我们日常工作任务不太需要我们对底层知识的了解,但是保持好奇心也是一个程序员快速进步的一种方式。原理、底层这些很枯燥的东西,如果能啃下来,会发现很多问题都想通了。就好比练武功,武功高强的人,会发现他们的内功心法很强大,当内功心法强大了,学什么武功也就快了。
欢迎大家关注公众号《假装懂编程》,我将持续输出网络、数据库、go、缓存、架构、面试、程序人生相关文章。
刘悦远