Go CLI Playbook 學習筆記

Posted by Kubeguts on 2021-02-15

以下為學習本課程 Pluralshight: The Go CLI Playbook 所記錄的筆記,了解Golang本身所提供的指令集工具要如何使用

https://app.pluralsight.com/course-player?clipId=38b3a654-cbfd-436c-acb1-df088dfb5a48

Go Command 功能

Go 的基本Command可以提供以下功能

  • Building Application: 把.go檔案編譯成執行檔
  • Testing Application: 測試.go程式
  • Profiling Application: 效能測試
  • Managing Workspaces: 管理開發環境
  • Interacting with Environment: 與環境互動

Go Command的列表

在終端機下 go 指令,就會顯示go command的詳細資訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
➜ go
Go is a tool for managing Go source code.

Usage:

go command [arguments]

The commands are:

build compile packages and dependencies
clean remove object files and cached files
doc show documentation for package or symbol
env print Go environment information
bug start a bug report
fix update packages to use new APIs
fmt gofmt (reformat) package sources
generate generate Go files by processing source
get download and install packages and dependencies
install compile and install packages and dependencies
list list packages
run compile and run Go program
test test packages
tool run specified go tool
version print Go version
vet report likely mistakes in packages

Use "go help [command]" for more information about a command.

Additional help topics:

c calling between Go and C
buildmode build modes
cache build and test caching
filetype file types
gopath GOPATH environment variable
environment environment variables
importpath import path syntax
packages package lists
testflag testing flags
testfunc testing functions

Use "go help [topic]" for more information about that topic.

可透過 go help 可看到特定command的指令

假如要看go test的介紹,那就下這行指令 go help test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
➜ go help test
usage: go test [build/test flags] [packages] [build/test flags & test binary flags]

'Go test' automates testing the packages named by the import paths.
It prints a summary of the test results in the format:

ok archive/tar 0.011s
FAIL archive/zip 0.022s
ok compress/gzip 0.033s
...

followed by detailed output for each failed package.

'Go test' recompiles each package along with any files with names matching
the file pattern "*_test.go".
These additional files can contain test functions, benchmark functions, and
example functions. See 'go help testfunc' for more.
...
...
...

The test binary also accepts flags that control execution of the test; these
flags are also accessible by 'go test'. See 'go help testflag' for details.

For more about build flags, see 'go help build'.
For more about specifying packages, see 'go help packages'.

go env 查看環境變數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> go env

GOARCH="amd64"
GOBIN="/Users/username/go/bin"
GOCACHE="/Users/username/Library/Caches/go-build"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/username/go"
GORACE=""
GOROOT="/usr/local/Cellar/go/1.10.2/libexec"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.10.2/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/3t/8jsg2cz52yn2glq8rk_9y7x40000gn/T/go-build760606218=/tmp/go-build -gno-record-gcc-switches -fno-common"

go run 執行程式

注意,若要運程go run,需要在$GOPATH底下的src目錄進行

運行一個包含main()方法的程式

1
> go run main.go

透過 go run --race來偵測是否會有race condition的狀況發生

若目前有個使用gorouting的程式,若要偵測是否會發生race condition狀況,可以加上-race這個flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
}

若有race condition,go tool會產出報告來解釋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
➜ go run -race race_condition/main.go 
==================
WARNING: DATA RACE
Read at 0x00c00013e010 by goroutine 8:
main.main.func1()
/Users/a10000005588/go/src/race_condition/main.go:13 +0x3c

Previous write at 0x00c00013e010 by main goroutine:
main.main()
/Users/a10000005588/go/src/race_condition/main.go:10 +0x108

Goroutine 8 (running) created at:
main.main()
/Users/a10000005588/go/src/race_condition/main.go:12 +0xe4
==================
==================
WARNING: DATA RACE
Read at 0x00c00013e010 by goroutine 7:
main.main.func1()
/Users/a10000005588/go/src/race_condition/main.go:13 +0x3c

Previous write at 0x00c00013e010 by main goroutine:
main.main()
/Users/a10000005588/go/src/race_condition/main.go:10 +0x108

Goroutine 7 (running) created at:
main.main()
/Users/a10000005588/go/src/race_condition/main.go:12 +0xe4
==================
2
2
2
4
6
6
7
8
8
10
Found 2 data race(s)
exit status 66
1
一般來說在dev模式時,會建議每次運行專案時,都加上-race

go build 編譯程式與其依賴

go build會把專案編譯成執行檔

若目前有個在cmds資料夾內的專案叫做 hello 並引用了helloService這個依賴內的方法

cmds/hello.go

1
2
3
4
5
6
7
package main

import "helloService"

func main() {
helloService.SayHello()
}

helloService/msg.go

1
2
3
4
5
6
7
8
package hello

import "fmt"

// SayHello represents a greeting to developer.
func SayHello() {
fmt.Println("Hello gofer! I came from `cmds/hello` package.")
}

那在 /cmds/hello專案中執行 go build,會在當前目錄產生hello執行檔

可直接執行該執行檔

使用 go build -o定義編譯後檔案名稱

假如要把編譯檔案叫做hello_exe.a

1
go build -o hello_exe.a

使用 go build -i 編譯專案的依賴

若對專案進行編譯時,加上-i參數,會同時將import的依賴 (ex:helloService)也會編譯該依賴們並安裝在 $GOPATH/pkg底下

參考

Go Official Docs: Command go
https://golang.org/cmd/go/#Compile packages and dependencies

go install 編譯專案成為package

會將專案 (main) 進行編譯成bin檔案並放置在$GOPATH/bin底下

會將依賴 (package) 進行編譯並安裝在$GOPATH/pkg底下

1
如果已經有存在的依賴或是binary檔案、那go install就不會有作用

go install -n:顯示會進行的動作,但不會真正執行

假如我們在對剛剛的helloService這個依賴,進行go install -n

會看到不會做什麼動作,因為已經有安裝該依賴在pkg底下

將該依賴移除,再次執行一遍,會看到go tool執行了什麼動作

go install -x:顯示會進行的動作,但會真正執行

其他flag

  • -p n: 指定用多少個cpu來進行install, 預設是可取得的數量

產生Shared Libraries,使用-buildmode flag

透過 go build -buildmode 或是 go install -buildmode
可以像C或C++那樣產生Shared Libraries,供動態載入

透過go help buildmode可查看有提供哪些模式

1
2
3
預設-buildmode會使用 archive:將檔案編譯成 .a

使用 -buildmode=shared:將檔案編譯成可動態載入的shared lib,或是在runtime時可連結

假如再用剛剛的範例,一個cmds/hello.go中引用了 helloService這個package

使用 -buildmode=shared 產生供go動態連結的Shared Libraries

若要將某個package編譯成shared library,

舉個例子,把helloService 編譯成shared library並安裝在pkg資料夾中

1
➜ go install -buildmode=shared helloService

注意: Macos不支援 -buildmode=shared
-buildmode=shared not supported on darwin/amd64

接著編譯主程式,並指定其執行檔可以動態載入(-linkshared) shared library

1
➜ go build -linkshared cmds/hello

執行編譯後的執行檔 ./hello ,可看到其結果

不過若將pkg中,剛剛go install的helloService的shared library給刪除的話,再次執行 /.hello 那就會發生無法執行狀況。

使用 -buildmode=c-archive 產生供C language "靜態"連結的Shared Libraries

如果想要將pakcage編譯成C可以載入的shared library的話,需要對pakcage的method,加上 //export functionName

1
2
3
4
5
6
7
8
9
package helloService

import "fmt"
import "C"

//export SayHello
func SayHello() {
fmt.Println("Hello gofer! I came from `cmds/hello` package.")
}

若使用 -buildmode=c-archive的話,則會產生出 .h.a的shared library

注意: Macos運行-buildmode=c-archive 會無法產生 .h檔案 go版本: go1.15 darwin/amd64

接著就可以在C語言內引入用Go build好的share library

1
2
3
4
5
6
#include "hello.a"

int main(void) {
SayHello();
return 0;
}

執行之

1
cc hello.c ./hello

使用 -buildmode=c-shared 產生供C language "動態"連結的Shared Libraries

同用go產生出shared library

使用 -buildmode=plugin 將所有import的packages,打包成go plugin

可以把專案需要import的package,打包成plugin

舉個例子,若現在我們要將plugin這一個pakcage打包成plugin

1
2
3
4
5
6
7
package main

import "fmt"

func ThingToDo() {
fmt.Println("Executing action")
}

透過以下指令,做出一個plugin,檔名叫做 plugin 副檔名為.so

1
➜ go build -buildmode=plugin -o=plugin.so plugin/plugin.go

接著我們定義了一個小專案,來使用plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"flag"
"log"
"plugin"
)

func main() {
path := flag.String("plugin", "", "Plugin to execute")

flag.Parse()
// 如果執行go時沒有帶 -plugin指令
if *path == "" {
log.Fatal("Path to plugin must be provided")
}

// 讀取plugin
p, err := plugin.Open(*path)
if err != nil {
log.Fatal(err)
}

// 尋找plugin是否有ThingToDo這個方法
f, err := p.Lookup("ThingToDo")
if err != nil {
log.Fatal(err)
}

thingToDo, ok := f.(func())
if !ok {
log.Fatal("Could not find function 'ThingToDo' in plugin")
}
thingToDo()
log.Println("Did the thing")
}

接著運行下面指令使用plugin

1
➜ go run action/action.go -plugin=./plugin.so

如果plugin存在的話,就可以執行 plugin.so內的ThingToDo方法

執行結果

參考

Golang的构建模式 - Chen Jiehua
https://chenjiehua.me/golang/golang-buildmode.html

go test 撰寫單元測試Unit Testing Programs

透過 go test 可測試golang撰寫的程式的Input與Output是否正確

測試需注意的規則

由於go本身對測試有著嚴謹的規則,故可以避免像其他語言那樣有各種不同測試的框架,以統一測試的樣式

其規則如下:

  • 若有定義測試的檔案名稱,需要以 _test.go最結尾命名
1
mylib_test.go
  • 若需要被測試的方法,請以 Test_ 為開頭
1
func Test_MyFunction() {}

執行測試

若有個專案叫做 testing_example,其目錄架構如下

testing_example/mylib_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package mylib

import (
"testing"
)

func Test_BasicChecks(t *testing.T) {
t.Run("Go can add", func(t *testing.T) {
if 1+1 != 2 {
t.Fail()
}
})
t.Run("Go can concatenate strings", func(t *testing.T) {
if "Hello, "+"Go" != "Hello, Go" {
t.Fail()
}
})
}

其子package也有定義testing file ``testing_example/childlib/childlib_test.go`

1
2
3
4
5
6
7
8
9
10
11
package childlib

import (
"testing"
)

func Test_MoreBasics(t *testing.T) {
if 10-5 != 5 {
t.Error("Failed to subtract correctly")
}
}

執行測試的範疇

若要對專案執行Unit Test:

1
2
3
~/go/src via v1.15 
go test testing_example
ok testing_example 0.014s

但會發現child package的測試沒有執行到,所以若要對package內所有子
package都進行測試,那需要加上 /... 做遞迴尋找的動作

1
2
3
4
~/go/src via 🐹 v1.15 
➜ go test testing_example/...
ok testing_example (cached)
ok testing_example/childlib 0.015s

測試可以夾帶的參數 flags

透過 go help testflag 可以查看執行測試時可以客製化的選擇

列舉一些常使用的:

  • -cpu: 指定測試時使用的CPU數量,預設是會使用 GOMAXPROCS參數的值
  • -parallel: 測試時可以並行處理,但只有指定Function才會被並行處理
1
2
3
4
5
6
7
8
func Test_BasicChecks(t *testing.T) {
t.Parallel() // 進行並行測試
t.Run("Go can add", func(t *testing.T) {
if 1+1 != 2 {
t.Fail()
}
})
}
  • -list: 可透過正則表達式過濾出測試方法名稱,再測試時會列出符合的測試方法給

假如要列出有包含 Basics的測試方法名稱:

1
2
3
4
5
~/go/src via v1.15 
➜ go test -list Basics testing_example/...
ok testing_example 0.022s
Test_MoreBasics // 列出
ok testing_example/childlib 0.023s
  • -run: 跟List很像,但只會執行符合正則表達式的測試方法
    • 使用時機:若開發出子專案的測試方法,透過-run指定該子專案的scope執行測試就好,就可以省去不少時間
1
2
3
4
~/go/src via v1.15 
➜ go test -run Basics testing_example/...
ok testing_example 0.024s [no tests to run]
ok testing_example/childlib 0.022s
  • -timeout d: 超過多久就停止測試,預設為10分鐘
  • -v: 測試時將詳細結果列出來
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~/go/src via v1.15 
➜ go test -v testing_example/...
=== RUN Test_BasicChecks
=== PAUSE Test_BasicChecks
=== CONT Test_BasicChecks
=== RUN Test_BasicChecks/Go_can_add
=== RUN Test_BasicChecks/Go_can_concatenate_strings
--- PASS: Test_BasicChecks (0.00s)
--- PASS: Test_BasicChecks/Go_can_add (0.00s)
--- PASS: Test_BasicChecks/Go_can_concatenate_strings (0.00s)
PASS
ok testing_example 0.017s
=== RUN Test_MoreBasics
=== PAUSE Test_MoreBasics
=== CONT Test_MoreBasics
--- PASS: Test_MoreBasics (0.00s)
PASS
ok testing_example/childlib 0.025s
  • -count: 指定執行測試的次數
  • -cover: 顯示測試的涵蓋範圍資訊

測試的涵蓋 Code Coverage

可以透過 go test -cover 來查看目前測試項目涵蓋了多少開發的內容

以下為測試的項目內容:

mylib.go

1
2
3
4
5
6
7
8
9
package mylib

func adder(l, r int) int {
return l + r
}

func subractor(l, r int) int {
return l - r
}

mylib_test.go

1
2
3
4
5
6
7
8
9
10
11
12
package mylib

import (
"testing"
)

// 在這邊只有測試adder方法,沒有subractor方法
func TestAdder(t *testing.T) {
if adder(2, 5) != 7 {
t.Fail()
}
}

可以看到上述的範例只測試一個方法

1
2
3
~/go/src via v1.15 
go test -cover testing_example_1
ok testing_example_1 0.013s coverage: 50.0% of statements

透過go test -coverpkg 指定測試程式引用的package

再用上述的範例,我們打算測試專案 testing_example_1中的 fmttesting_example_1本身自己這兩個pacakge:

1
2
3
4
5
~/go/src via v1.15 
go test -coverpkg testing_example_1, fmt testing_example_1
warning: no packages being tested depend on matches for pattern
ok fmt 0.089s coverage: 0.0% of statements in testing_example_1,
ok testing_example_1 0.019s coverage: 50.0% of statements in testing_example_1,

透過go test -coverprofile 產出測試的報告

1
2
3
~/go/src via v1.15 
➜ go test -coverprofile cover.out testing_example_1
ok testing_example_1 0.012s coverage: 50.0% of statements

同時會產生 cover.out這個測試報告檔案,內容如下

1
2
3
mode: set
testing_example_1/mylib.go:3.26,5.2 1 1
testing_example_1/mylib.go:7.30,9.2 1 0

透過 go tool 查看測試報告

可以透過 go tool cover -html= 測試報告檔案名稱 來產生出以網頁方式瀏覽的報告

若要解析產生出來的報告 cover.out

go tool cover -html=cover.out

可以產出Web呈現,測試的報告


如果要看出更詳細的測試報告的話,例如想看到測試方法被執行幾次

1
2
3
~/go/src via v1.15
go test -covermode count -coverprofile cover.out testing_example_1
ok testing_example_1 0.018s coverage: 50.0% of statements

一樣會產生出 cover.out檔案,這時再透過 go tool cover -html


進行壓力測試 Benchmark Testing

golang的test工具亦提供benchmark的測試,可以看出該方法需要耗費多少CPU資源

規則

需要定義方法名稱前綴為 Benchmark 例如:

1
2
3
4
5
func BenchmarkAdder(b *testing.B) {
for i := 0; i < b.N; i++ {
adder(5, 7)
}
}

指令: go test -bench

若要對testing_example_2這個專案下的檔案:

mylib.go

1
2
3
4
5
package mylib

func adder(l, r int) int {
return l + r
}

mylib_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package mylib

import (
"testing"
)

func TestAdder(t *testing.T) {
if adder(2, 5) != 7 {
t.Fail()
}
}

func BenchmarkAdder(b *testing.B) {
for i := 0; i < b.N; i++ {
adder(5, 7)
}
}

這時透過 go test -bench Adder testing_example_2,對testing_example_2這專案內有包含 Adder的壓測方法進行壓力測試

1
2
3
4
5
6
7
8
~/go/src via v1.15
go test -bench Adder testing_example_2
goos: darwin
goarch: amd64
pkg: testing_example_2
BenchmarkAdder-4 1000000000 0.374 ns/op
PASS
ok testing_example_2 0.433s

BenchmarkAdder 這隻方法執行了1000000000次, 每次花了 0.374 ns

或是也可以不指定方法Benchmark名稱 go test -bench . testing_example_2

指定要花多少時間做benchmark測試: go test -benchtime

可指定壓測測多少時間,可以擴展壓測的次數

舉例,只壓測0.001秒,那可看到只會測出 2864049次

1
2
3
4
5
6
7
8
~/go/src via v1.15 
go test -benchtime 0.001s -bench . testing_example_2
goos: darwin
goarch: amd64
pkg: testing_example_2
BenchmarkAdder-4 2864049 0.485 ns/op
PASS
ok testing_example_2 0.024s
1
2
3
4

#### 檢視壓力測試花多少記憶體: `go test -bench . -benchmem`

透過 `-bemchmem`這個flag,壓測資訊會多了 `B/op` 與 `allocs/op` 來檢視該方法壓測時花了多少記憶體資源

~/go/src via v1.15
➜ go test -bench . -benchmem testing_example_2
goos: darwin
goarch: amd64
pkg: testing_example_2
BenchmarkAdder-4 1000000000 0.501 ns/op 0 B/op 0 allocs/op
PASS
ok testing_example_2 0.586s

1
2
3
4
5
6
7


### 使用go tool pprof (performance profiling tool) 產生測試報告

產出報告之前,需要先安裝 `graphviz`這個套件,這樣go tool才能夠繪製流程圖供我們查看

Linux/Macos

sudo apt-get install graphyiz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

> 更多有關graphyiz資訊請到: http://graphviz.org
>

#### 產生程式記憶體花費報告 `go test -memprofile`

一樣用testing_example_2這個專案下的檔案做測試

![](pics/14.png)
![](https://i.imgur.com/FPmr2HU.png)


`mylib.go`
```go=
package mylib

func adder(l, r int) int {
return l + r
}

mylib_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package mylib

import (
"testing"
)

func TestAdder(t *testing.T) {
if adder(2, 5) != 7 {
t.Fail()
}
}

func BenchmarkAdder(b *testing.B) {
for i := 0; i < b.N; i++ {
adder(5, 7)
}
}

透過 go test -memprofile mem.out testing_example_2 來產出叫做 mem.out的測試報告

測試結果

1
2
3
~/go/src via v1.15 
➜ go test -memprofile mem.out testing_example_2
ok testing_example_2 0.018s

以及會產生兩個檔案 mem.outtesting_example_2.test,後者主要提供給 go tool pprof檢視


接著透過 go tool pprof檢視其報告 testing_example_2.test,並指定用-web 網頁的方式瀏覽

1
sudo go tool pprof -web testing_example_2.test mem.out

不過由於範例檔案規模太小,go tool無法捕捉到有用的資訊,所以抓不到

這時可以透過 go test -mmprofilerate n來改變讀取報告的頻率,當執行n個指令時,就產出報告

接著再查看一次產出的報告

1
sudo go tool pprof -web testing_example_2.test mem.out

可以看到每個方法詳細的記憶體消耗資訊圖

產生CPU消費報告 go test -cpuprofile

一樣可以產生每個方法或package使用多少的CPU關聯圖

1
2
3
~/go/src via v1.15 
➜ go test -cpuprofile cpu.out testing_example_2
ok testing_example_2 0.224s

產生出cpu.outtesting_example_2.test,然後後者也是透過 pprof檢視

不過也是一樣沒看到豐富的測試資訊,因為整個測試只有執行一遍,導致pprof無法抓到有用的資訊

這時可以透過 -count 可以測試多次以取得足夠的CPU使用量報告

go test -cpuprofile cpu.out -count 1000000 testing_example_2

在查看一遍就會有方法呼叫的CPU消耗流程圖可以檢視了

產生程式足跡報告 go test -trace

透過-trace這個flag, 可以產出許多檢視程式的執行報告

一樣透過 go test -trace trace.out testing_example_2來產生叫做 trace.out的報告

go tool trace trace.out 檢視內容

點開View trace可看到如下

參考資源