golang 隨手記 - 基礎知識

寫在前面

go 語言開發者在日常工作或學習中,用到最多的命令可能是go buildgo rungo installgo get...,通過這些命令可以幫我們編譯程序、運行程序、安裝程序和獲取代碼包,然而在執行這些命令的時候,你是否思考過 go 編譯過程中是如何組織我們的源碼文件的?go install 的時候發生了什麼?以及 go get 只是去下載我們依賴的包文件嗎?go 還有哪些實用的命令?這是一篇可以說比較基礎的文章,也可以作爲 go 命令的一個速查手冊,旨在幫助我們更深刻的瞭解 go。

幾個概念

gopath 可能是每個 go 開發者最熟悉不過的東西了,gopath 就是我們的工作區,通過go env可以查看到我們的 gopath,如 GOPATH="/Users/code/go",gopath 就是我們的工作區,gopath 下面一般會建立binsrcpkg三個文件夾。

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
  1. 創建臨時目錄:$WORK/b001/

  2. 查找依賴包信息:$WORK/b001/importcfg.link

  3. 創建 exe 目錄:$WORK/b001/exe/

  4. 通過 go 的工具 tool 的 link 來鏈接庫文件(importcfg.link)生成可執行文件 $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__ = "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\nmod\tcode\t(devel)\t\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
        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相關命令

  1. go mod init 模塊名 初始化一個 go 項目

  2. go mod download 下載 modules 到本地 cache,即$GOPATH/pkg/mod/cache/download中,執行go mod download -json可以以 json 格式輸出信息:

//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="
}
  1. go mod edit

編輯 go.mod 文件 選項有-json-replace...,可以使用幫助go help mod edit,比如說如果你要修改某個包,可以直接使用 go mod edit -replace=old[@v]=new[@v],一般都是直接編輯go.mod文件了,這個命令用的不多。

  1. go mod graph

以文本模式打印依賴的包,比如我的 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這個包,來查看依賴關係:

  1. go mod tidy

添加丟失或移出不需要的模塊。
當前我的 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文件的作用就兩個:

  1. checksum,防止包被篡改,校驗和

  2. 記錄包的更新記錄

當我們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 的過程發生了什麼?另一方面本文也列舉了基本日常開發夠用的命令,這樣當自己需要查找命令不用去網上百度了,可以作爲一個速查手冊。
也許我們日常工作任務不太需要我們對底層知識的瞭解,但是保持好奇心也是一個程序員快速進步的一種方式。原理、底層這些很枯燥的東西,如果能啃下來,會發現很多問題都想通了。就好比練武功,武功高強的人,會發現他們的內功心法很強大,當內功心法強大了,學什麼武功也就快了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/s83pPXlau48mFiX6iJeCCg