golang 隨手記 - 基礎知識
寫在前面
go 語言開發者在日常工作或學習中,用到最多的命令可能是go build
、go run
、go install
、go get
...,通過這些命令可以幫我們編譯程序、運行程序、安裝程序和獲取代碼包,然而在執行這些命令的時候,你是否思考過 go 編譯過程中是如何組織我們的源碼文件的?go install 的時候發生了什麼?以及 go get 只是去下載我們依賴的包文件嗎?go 還有哪些實用的命令?這是一篇可以說比較基礎的文章,也可以作爲 go 命令的一個速查手冊,旨在幫助我們更深刻的瞭解 go。
幾個概念
-
命令源碼文件:聲明爲屬於 main 代碼包,並且包含無參數聲明和結果聲明的 main 函數的源碼文件,一般一個項目只有一個命令源碼文件。
-
庫源碼文件:沒有命令源碼的特徵,存在於某個包裏面的普通源碼文件
-
測試源碼文件:以_test.go 爲後綴的源碼文件,且必須包含 Test 或 Beanchmark 爲名稱前綴的函數。
gopath 可能是每個 go 開發者最熟悉不過的東西了,gopath 就是我們的工作區,通過go env
可以查看到我們的 gopath,如 GOPATH="/Users/code/go"
,gopath 就是我們的工作區,gopath 下面一般會建立bin
、src
、pkg
三個文件夾。
- bin:go install 後的可執行文件會放在 bin 目錄裏。但是在 go 的環境變量中有個叫 GOBIN 的,默認情況下 GOBIN 是空的,但是當我們設置了 GOBIN 後,GOPATH/bin 就會沒有意義了,go install 的可執行文件會默認放到 GOBIN 下。
export GOBIN=/tmp //聲明GOBIN目錄
go install $GOPATH/src/code //安裝code程序
/tmp/code 發現程序已經被安裝到了GOBIN下
-
src:我們編寫的源碼文件都在這個文件夾裏。
-
pkg:依賴包安裝後的庫源碼文件存放的位置,即歸檔文件(.a 結尾),也就是程序編譯後生成的靜態庫文件。
常見輔助命令
go 有一些輔助命令可以幫助我們更好的理解 go 的一些執行過程,輔助命令的體現就是 go run/build.. -x
xx.go, -x
就是輔助命令。-x
可以有以下類型:
-
-a
:強行對所有涉及到的代碼包(包括標準庫中的代碼包)進行重新構建,即使它們已經是最新的了。 -
-n
:打印構建期間所用到的其它命令,但是並不真正執行它們,通過 - n 可以觀察構建的過程 -
-race
:用於檢測數據競爭問題,比如 map 併發讀寫... -
-v
:用於打印命令執行過程中涉及的代碼包,包括了我們依賴的代碼包,還可以看到依賴的代碼包關聯的代碼包。 -
-work
:用於打印命令執行時生成和使用的臨時工作目錄名字,且命令執行完後不刪除它。如果不添加此標記,臨時工作目錄會在命令執行完後被刪除。 -
-x
:打印命令執行過程中用到的所有命令,並同時執行他們。 -
-p n
:構建的並行數量(n)。默認情況下並行數量與 CPU 數量相同。 -
-o
:編譯指定輸出到的文件。
小衆輔助命令
-
-asmflags
:此標記可以後跟另外一些標記,如 - D、-I、-S 等。這些後跟的標記用於控制 Go 語言編譯器編譯彙編語言文件時的行爲。 -
-buildmode
:指定編譯模式,使用方式如 - buildmode=default,主要控制編譯器在編譯完成後生成靜態鏈接庫(即. a 文件,也就是我們之前說的歸檔文件)、動態鏈接庫(即. so 文件)或 / 和可執行文件(在 Windows 下是. exe 文件)。 -
-compiler
:指定當前使用的編譯器的名稱,其值可以爲 gc 或 gccgo。其中,gc 編譯器即爲 Go 語言自帶的編輯器,而 gccgo 編譯器則爲 GCC 提供的 Go 語言編譯器。 -
-gccgoflags
:指定需要傳遞給 gccgo 編譯器或鏈接器的標記的列表。 -
-gcflags
:指定需要傳遞給 go tool compile 命令的標記的列表。 -
-installsuffix
:爲了使當前的輸出目錄與默認的編譯輸出目錄分離,可以使用這個標記。此標記的值會作爲結果文件的父目錄名稱的後綴。其實,如果使用了 - race 標記,這個標記會被自動追加且其值會爲 race。如果我們同時使用了 - race 標記和 - installsuffix,那麼在 - installsuffix 標記的值的後面會再被追加_race,並以此來作爲實際使用的後綴。 -
-ldflags
:指定需要傳遞給 go tool link 命令的標記的列表。 -
-linkshared
:用於與 - buildmode=shared 一同使用。後者會使作爲編譯目標的非 main 代碼包都被合併到一個動態鏈接庫文件中,而前者則會在此之上進行鏈接操作。 -
pkgdir
:可以指定一個目錄。編譯器會只從該目錄中加載代碼包的歸檔文件,並會把編譯可能會生成的代碼包歸檔文件放置在該目錄下。 -
-tags
:指定在實際編譯期間需要受理的編譯標籤(也可被稱爲編譯約束)的列表。 -
-toolexec
:可以讓我們去自定義在編譯期間使用一些 Go 語言自帶工具(如 vet、asm 等)的方式。 -
-mod
:使用 - mod [mode] 模式,[mode] 支持 readonly,release,vendor,當執行 go build -mod=vendor 的時候,會在生成可執行文件的同時將項目的依賴包放到主模塊的 vendor 目錄下。 -
-modcacherw
:將下載的 mod 的包以讀寫方式保留在模塊緩存中而不是使其只讀。 -
-trimpath
:刪除二進制文件中的源碼路徑信息,比如當我們的程序 panic 的時候,可能把相關錯誤的源碼路徑打印出來,通過這個可以隱藏。
編譯
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
-
創建臨時目錄:$WORK/b001/
-
查找依賴包信息:$WORK/b001/importcfg.link
-
創建 exe 目錄:$WORK/b001/exe/
-
通過 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
-
exe:就是我們的可執行文件。
-
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 後面支持可選參數
-
-d
:讓命令程序只執行下載動作,而不執行安裝動作。 -
-u
:讓命令利用網絡來更新已有代碼包及其依賴包。默認情況下,該命令只會從網絡上下載本地不存在的代碼包,而不會更新已有的代碼包。 -
-t
:讓命令程序同時下載並安裝指定的代碼包中的測試源碼文件中依賴的代碼包。 -
-insecure
:允許命令程序使用非安全的 scheme(如 HTTP)去下載指定的代碼包。如果你用的代碼倉庫(如公司內部的 Gitlab)沒有 HTTPS 支持,可以添加此標記。請在確定安全的情況下使用它。 -
-v
:顯示執行的命令。
go mod
go 從 1.11 開始支持 go mod 模式,現在相信大家也基本都在使用 go mod,相比 vendor 的好處,使用go mod
之後,你的代碼可以存在在任何位置。
-
GO111MODULE=on 通過這個環境變量來開啓 go mod 模式
-
GOPROXY="https://goproxy.io",推薦大家用這個 proxy 來下載 go 的第三方依賴包。
-
GOPRIVE="git.xx.xx",prive 是私有的倉庫,一些比如公司自己內部倉庫,可以配置下私有倉庫的域名來獲取內部代碼
go mod
相關命令
-
go mod init 模塊名
初始化一個 go 項目 -
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="
}
go mod edit
編輯 go.mod 文件 選項有-json
、-replace
...,可以使用幫助go help mod edit
,比如說如果你要修改某個包,可以直接使用 go mod edit -replace=old[@v]=new[@v]
,一般都是直接編輯go.mod
文件了,這個命令用的不多。
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
這個包,來查看依賴關係:
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
文件的作用就兩個:
-
checksum,防止包被篡改,校驗和
-
記錄包的更新記錄
當我們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
-
-n
:把需要執行的清除命令打印出來,但是不執行,這樣就可以很容易的知道底層是如何運行的。 -
-i
:清除關聯的安裝的包和可運行文件。我們可以通過結合-n
來看看-i
是如何執行的:
//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
產生的編譯文件。
-r
:循環的清除在 import 中引入的包:
// 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 項目依賴很多包,這些依賴的包也會執行刪除一些當前目錄的編譯文件。
-
-x
:打印出來執行的詳細命令,其實就是-n
打印的執行版本。 -
-cache
:刪除所有go build
命令的緩存。
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
-testcache
:刪除當前包所有的測試結果。
當我們使用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
-modcache
:刪除 go.mod 模式下的緩存文件。
當我們啓動 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