一、質(zhì)量保證
1. 單元測試
單元測試是保證代碼質(zhì)量的好方法,但單元測試也不是萬能的,使用它可以降低 Bug 率,但也不要完全依賴。除了單元測試外,還可以輔以 Code Review、人工測試等手段更好地保證代碼質(zhì)量。
1.1 定義
顧名思義,單元測試強(qiáng)調(diào)的是對單元進(jìn)行測試。在開發(fā)中,一個(gè)單元可以是一個(gè)函數(shù)、一個(gè)模塊等。一般情況下,要測試的單元應(yīng)該是一個(gè)完整的最小單元,比如 Go 語言的函數(shù)。
單元測試由開發(fā)者自己編寫,也就是誰改動了代碼,誰就要編寫相應(yīng)的單元測試代碼以驗(yàn)證本次改動的正確性。
1.2 Go 語言的單元測試
雖然每種編程語言里單元測試的概念是一樣的,但它們對單元測試的設(shè)計(jì)不一樣。Go 語言也有自己的單元測試規(guī)范。
下例通過遞歸的方式實(shí)現(xiàn)了斐波那契數(shù)列的計(jì)算。
func Fibonacci(n int) int {
if n < 0 {
return 0
}
if n == 0 {
return 0
}
if n == 1 {
return 1
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
該 Fibonacci 函數(shù)在main.go文件中,那么對 Fibonacci 函數(shù)進(jìn)行單元測試的代碼需要放在同一目錄的main_test.go中,測試代碼如下:
func TestFibonacci(t *testing.T) {
//預(yù)先定義的一組斐波那契數(shù)列作為測試用例
fsMap := map[int]int{}
fsMap[0] = 0
fsMap[1] = 1
fsMap[2] = 1
fsMap[3] = 2
fsMap[4] = 3
fsMap[5] = 5
fsMap[6] = 8
fsMap[7] = 13
fsMap[8] = 21
fsMap[9] = 34
for k, v := range fsMap {
fib := Fibonacci(k)
if v == fib {
t.Logf("結(jié)果正確:n為%d,值為%d", k, fib)
} else {
t.Errorf("結(jié)果錯(cuò)誤:期望%d,但是計(jì)算的值是%d", v, fib)
}
}
}
在這個(gè)單元測試中,通過 map 預(yù)定義了一組測試用例,然后通過 Fibonacci 函數(shù)計(jì)算結(jié)果。同預(yù)定義的結(jié)果進(jìn)行比較,如果相等,則說明 Fibonacci 函數(shù)計(jì)算正確,不相等則說明計(jì)算錯(cuò)誤。
然后即可運(yùn)行如下命令,進(jìn)行單元測試:
? go test -v .
這行命令會運(yùn)行當(dāng)前目錄下的所有單元測試,因?yàn)橹粚懥艘粋€(gè)單元測試,所以可以看到結(jié)果如下所示:
? go test -v .
=== RUN TestFibonacci
main_test.go:21: 結(jié)果正確:n為0,值為0
main_test.go:21: 結(jié)果正確:n為1,值為1
main_test.go:21: 結(jié)果正確:n為6,值為8
main_test.go:21: 結(jié)果正確:n為8,值為21
main_test.go:21: 結(jié)果正確:n為9,值為34
main_test.go:21: 結(jié)果正確:n為2,值為1
main_test.go:21: 結(jié)果正確:n為3,值為2
main_test.go:21: 結(jié)果正確:n為4,值為3
main_test.go:21: 結(jié)果正確:n為5,值為5
main_test.go:21: 結(jié)果正確:n為7,值為13
--- PASS: TestFibonacci (0.00s)
PASS
ok test (cached)
在打印的測試結(jié)果中可以看到 PASS 標(biāo)記,說明單元測試通過,而且還可以看到在單元測試中寫的日志。
Go 語言測試框架可以讓開發(fā)者很容易地進(jìn)行單元測試,但是需要遵循五點(diǎn)規(guī)則:
- 含有單元測試代碼的 go 文件必須以 _test.go 結(jié)尾,Go 語言測試工具只認(rèn)符合這個(gè)規(guī)則的文件。
- 單元測試文件名 _test.go 前面的部分最好是被測試的函數(shù)所在的 go 文件的文件名,比如以上示例中單元測試文件叫 main_test.go,因?yàn)闇y試的 Fibonacci 函數(shù)在 main.go 文件里。
- 單元測試的函數(shù)名必須以 Test 開頭,是可導(dǎo)出的、公開的函數(shù)。
- 測試函數(shù)的簽名必須接收一個(gè)指向 testing.T 類型的指針,并且不能返回任何值。
- 函數(shù)名最好是 Test + 要測試的函數(shù)名,比如例子中是 TestFibonacci,表示測試的是 Fibonacci 這個(gè)函數(shù)。
單元測試的重點(diǎn)在于熟悉業(yè)務(wù)代碼的邏輯、場景等,以便盡可能地全面測試,保障代碼質(zhì)量。
1.3 單元測試覆蓋率
Go 語言提供了非常方便的命令來查看單元測試覆蓋率。還是以 Fibonacci 函數(shù)的單元測試為例,通過一行命令即可查看它的單元測試覆蓋率。
? go test -v --coverprofile=res.cover .
這行命令包括 --coverprofile 這個(gè) Flag,它可以得到一個(gè)單元測試覆蓋率文件,運(yùn)行這行命令還可以同時(shí)看到測試覆蓋率。Fibonacci 函數(shù)的測試覆蓋率如下:
PASS
coverage: 85.7% of statements
ok test 0.367s coverage: 85.7% of statements
可以看到,測試覆蓋率為 85.7%。從這個(gè)數(shù)字來看,F(xiàn)ibonacci 函數(shù)應(yīng)該沒有被全面地測試,這時(shí)候就需要查看詳細(xì)的單元測試覆蓋率報(bào)告了。
運(yùn)行如下命令,可以得到一個(gè) HTML 格式的單元測試覆蓋率報(bào)告:
? go tool cover -html=res.cover -o=res.html
命令運(yùn)行后,會在當(dāng)前目錄下生成一個(gè) html 文件,內(nèi)容如下:
紅色標(biāo)記的部分是沒有測試到的,綠色標(biāo)記的部分是已經(jīng)測試到的。這就是單元測試覆蓋率報(bào)告的好處,通過它可以很容易地檢測自己寫的單元測試是否完全覆蓋。
2. 基準(zhǔn)測試
2.1 定義
基準(zhǔn)測試(Benchmark)是一項(xiàng)用于測量和評估軟件性能指標(biāo)的方法,主要用于評估代碼的性能。
2.2 Go 語言的基準(zhǔn)測試
Go 語言的基準(zhǔn)測試和單元測試規(guī)則基本一樣,只是測試函數(shù)的命名規(guī)則不一樣。
Fibonacci 函數(shù)的基準(zhǔn)測試代碼如下:
func BenchmarkFibonacci(b *testing.B){
for i:=0;i<b.N;i++{
Fibonacci(10)
}
}
Go 語言基準(zhǔn)測試和單元測試的不同點(diǎn)如下:
- 基準(zhǔn)測試函數(shù)必須以 Benchmark 開頭,必須是可導(dǎo)出的;
- 函數(shù)的簽名必須接收一個(gè)指向 testing.B 類型的指針,并且不能返回任何值;
- 最后的 for 循環(huán)很重要,被測試的代碼要放到循環(huán)里;
- b.N 是基準(zhǔn)測試框架提供的,表示循環(huán)的次數(shù),因?yàn)樾枰磸?fù)調(diào)用測試的代碼,才可以評估性能。
可以通過如下命令來測試 Fibonacci 函數(shù)的性能:
? go test -bench=. .
goos: darwin
goarch: amd64
pkg: test
BenchmarkFibonacci-8 3461616 343 ns/op
PASS
ok test 2.230s
運(yùn)行基準(zhǔn)測試也要使用 go test 命令,不過要加上 -bench 這個(gè) Flag,它接受一個(gè)表達(dá)式作為參數(shù),以匹配基準(zhǔn)測試的函數(shù),"."表示運(yùn)行所有基準(zhǔn)測試。
輸出的結(jié)果中函數(shù)后面的 -8 表示運(yùn)行基準(zhǔn)測試時(shí)對應(yīng)的 GOMAXPROCS 的值。接著的 3461616 表示運(yùn)行 for 循環(huán)的次數(shù),也就是調(diào)用被測試代碼的次數(shù),最后的 343 ns/op 表示每次需要花費(fèi) 343 納秒。
基準(zhǔn)測試的時(shí)間默認(rèn)是 1 秒,也就是 1 秒調(diào)用 3461616 次、每次調(diào)用花費(fèi) 343 納秒。如果想讓測試運(yùn)行的時(shí)間更長,可以通過 -benchtime 指定,比如 3 秒,代碼如下所示:
go test -bench=. -benchtime=3s .
2.3 計(jì)時(shí)方法
進(jìn)行基準(zhǔn)測試之前會做一些準(zhǔn)備,比如構(gòu)建測試數(shù)據(jù)等,這些準(zhǔn)備也需要消耗時(shí)間,所以需要把這部分時(shí)間排除在外。這就需要通過 ResetTimer 方法重置計(jì)時(shí)器,示例代碼如下:
func BenchmarkFibonacci(b *testing.B) {
n := 10
b.ResetTimer() //重置計(jì)時(shí)器
for i := 0; i < b.N; i++ {
Fibonacci(n)
}
}
這樣可以避免因?yàn)闇?zhǔn)備數(shù)據(jù)耗時(shí)造成的干擾。
除了 ResetTimer 方法外,還有 StartTimer 和 StopTimer 方法,可以靈活地控制什么時(shí)候開始計(jì)時(shí)、什么時(shí)候停止計(jì)時(shí)。
2.4 內(nèi)存統(tǒng)計(jì)
在基準(zhǔn)測試時(shí),還可以統(tǒng)計(jì)每次操作分配內(nèi)存的次數(shù),以及每次操作分配的字節(jié)數(shù),這兩個(gè)指標(biāo)可以作為優(yōu)化代碼的參考。要開啟內(nèi)存統(tǒng)計(jì)可以通過 ReportAllocs() 方法:
func BenchmarkFibonacci(b *testing.B) {
n := 10
b.ReportAllocs() //開啟內(nèi)存統(tǒng)計(jì)
b.ResetTimer() //重置計(jì)時(shí)器
for i := 0; i < b.N; i++ {
Fibonacci(n)
}
}
再運(yùn)行基準(zhǔn)測試,就可以看到如下結(jié)果:
? go test -bench=. .
goos: darwin
goarch: amd64
pkg: test
BenchmarkFibonacci-8 2486265 486 ns/op 0 B/op 0 allocs/op
PASS
ok test 2.533s
可以看到相比原來的基準(zhǔn)測試多了兩個(gè)指標(biāo),分別是 0 B/op 和 0 allocs/op。前者表示每次操作分配了多少字節(jié)的內(nèi)存,后者表示每次操作分配內(nèi)存的次數(shù)。這兩個(gè)指標(biāo)可以作為代碼優(yōu)化的參考,盡可能地越小越好。
以上兩個(gè)指標(biāo)不是越小越好,因?yàn)橛袝r(shí)候代碼實(shí)現(xiàn)需要空間換時(shí)間,所以要根據(jù)自己的具體業(yè)務(wù)而定,做到在滿足業(yè)務(wù)的情況下越小越好。
在運(yùn)行 go test 命令時(shí),也可以使用 -benchmem 這個(gè) Flag 進(jìn)行內(nèi)存統(tǒng)計(jì)。如下所示:
go test -bench=. -benchmem .
這種通過 -benchmem 查看內(nèi)存的方法適用于所有的基準(zhǔn)測試用例。
2.5 并發(fā)基準(zhǔn)測試
除了普通的基準(zhǔn)測試外,Go 語言還支持并發(fā)基準(zhǔn)測試,可以測試在多個(gè) goroutine 并發(fā)下代碼的性能。以 Fibonacci 為例,它的并發(fā)基準(zhǔn)測試代碼如下:
func BenchmarkFibonacciRunParallel(b *testing.B) {
n := 10
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Fibonacci(n)
}
})
}
可以看到,Go 語言通過 RunParallel 方法運(yùn)行并發(fā)基準(zhǔn)測試。RunParallel 方法會創(chuàng)建多個(gè) goroutine,并將 b.N 分配給這些 goroutine 執(zhí)行。
2.6 基準(zhǔn)測試實(shí)戰(zhàn)
以 Fibonacci 函數(shù)為例,根據(jù)前述的基準(zhǔn)測試,會發(fā)現(xiàn)它并沒有分配新的內(nèi)存,也就是說 Fibonacci 函數(shù)慢并不是因?yàn)閮?nèi)存,排除掉這個(gè)原因,就可以歸結(jié)為所寫的算法問題了。
在遞歸運(yùn)算中,一定會有重復(fù)計(jì)算,這是影響遞歸的主要因素。解決重復(fù)計(jì)算可以使用緩存,把已經(jīng)計(jì)算好的結(jié)果保存起來,就可以重復(fù)使用了。
修改后的 Fibonacci 函數(shù)的代碼如下:
//緩存已經(jīng)計(jì)算的結(jié)果
var cache = map[int]int{}
func Fibonacci(n int) int {
if v, ok := cache[n]; ok {
return v
}
result := 0
switch {
case n < 0:
result = 0
case n == 0:
result = 0
case n == 1:
result = 1
default:
result = Fibonacci(n-1) + Fibonacci(n-2)
}
cache[n] = result
return result
}
改造后再來運(yùn)行基準(zhǔn)測試,結(jié)果如下所示:
BenchmarkFibonacci-8 97823403 11.7 ns/op
可以看到,結(jié)果為 11.7 納秒,相比優(yōu)化前的 343 納秒,性能足足提高了 28 倍。
3. 特別注意
go test 以及 go tool cover 等相關(guān)命令通過如下使用 “=” 給參數(shù)賦值的方式僅適用于Mac和Linux,但不適用于Windows。
示例:
go tool cover -html=res.cover -o=res.html
上述命令在Windows中運(yùn)行會生成如下錯(cuò)誤信息:
too many arguments
For usage information, run "go tool cover -help"
正確的使用方式是:
go tool cover -html res.cover -o res.html
這適用于所有平臺。
請注意,這種不良的使用模式遍布整個(gè) go 生態(tài)系統(tǒng)。
二、性能優(yōu)化
在項(xiàng)目開發(fā)中,保證代碼質(zhì)量和性能的手段不只有單元測試和基準(zhǔn)測試,還有代碼規(guī)范檢查和性能優(yōu)化。
- 代碼規(guī)范檢查是對單元測試的一種補(bǔ)充,它可以從非業(yè)務(wù)的層面檢查代碼是否還有優(yōu)化的空間,比如變量是否被使用、是否是死代碼等等。
- 性能優(yōu)化是通過基準(zhǔn)測試來衡量的,這樣才知道優(yōu)化部分是否真的提升了程序的性能。
1. 代碼規(guī)范檢查
1.1 定義
代碼規(guī)范檢查,顧名思義,是從 Go 語言層面出發(fā),依據(jù) Go 語言的規(guī)范對代碼進(jìn)行的靜態(tài)掃描檢查,這種檢查和業(yè)務(wù)無關(guān)。
比如定義了個(gè)常量從未使用過,雖然對代碼運(yùn)行并沒有造成什么影響,但是這個(gè)常量是可以刪除的。再比如調(diào)用了一個(gè)函數(shù),該函數(shù)返回了一個(gè) error,但是并沒有對該 error 做判斷,這種情況下,程序也可以正常編譯運(yùn)行。但是代碼寫得不嚴(yán)謹(jǐn),因?yàn)榉祷氐?error 被忽略了。
除了上述這兩種情況,還有拼寫問題、死代碼、代碼簡化檢測、命名中帶下劃線、冗余代碼等,都可以使用代碼規(guī)范檢查檢測出來。
1.2 golangci-lint
要想對代碼進(jìn)行檢查,則需要對代碼進(jìn)行掃描,靜態(tài)分析寫的代碼是否存在規(guī)范問題。
靜態(tài)代碼分析是不會運(yùn)行代碼的。
可用于 Go 語言代碼分析的工具有很多,比如 golint、gofmt、misspell 等,如果一一引用配置,就會比較煩瑣,所以通常不會單獨(dú)地使用它們,而是使用 golangci-lint。
golangci-lint 是一個(gè)集成工具,它集成了很多靜態(tài)代碼分析工具。通過配置這一工具,可以很靈活地啟用需要的代碼規(guī)范檢查。
1.2.1 安裝
如果要使用 golangci-lint,首先需要安裝。 golangci-lint 有以下幾種安裝方式:
(1)Binaries(在Linux 和 Windows 環(huán)境下,建議通過如下方式進(jìn)行安裝)
# binary will be $(go env GOPATH)/bin/golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.3
(2)Install from Source
因?yàn)?golangci-lint 本身就是 Go 語言編寫的,所以可以從源代碼安裝。
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.3
(3)brew安裝(僅限MacOS)
brew install golangci-lint
brew upgrade golangci-lint
安裝完成后,在終端輸入如下命令,檢測是否安裝成功。
? golangci-lint version
golangci-lint has version v1.53.3 built with go1.20.5
1.2.2 使用
可以運(yùn)行如下命令運(yùn)行g(shù)olangci-lint:
golangci-lint run
# 等價(jià)于
golangci-lint run ./...
也可以指定要分析的目錄和文件:
golangci-lint run dir1 dir2/... dir3/file1.go
目錄不會遞歸分析其子目錄中的文件。要想遞歸地分析該目錄下所有文件,需要在路徑后附加
\...
1.2.3 golangci-lint 配置
golangci-lint 的配置比較靈活可以自定義要啟用哪些 linter。golangci-lint 默認(rèn)啟用的 linter,包括這些:
errcheck - Errcheck是一個(gè)用于檢查Go代碼中未檢查的errors的程序。在某些情況下,這些未經(jīng)檢查的errors可能是嚴(yán)重的錯(cuò)誤
gosimple - 用于Go源代碼的Linter,專門用于簡化代碼
govet - Vet檢查Go源代碼并報(bào)告可疑的結(jié)構(gòu),例如Printf調(diào)用的參數(shù)與格式字符串不一致
ineffassign - 檢測何時(shí)不使用對現(xiàn)有變量的賦值
staticcheck - 它是來自staticcheck的一組規(guī)則。它與staticcheck二進(jìn)制文件不同。staticcheck的作者不支持也不贊成在golangci-lint中使用staticcheck作為庫
unused - 檢查Go代碼中未使用的常量、變量、函數(shù)和類型
golangci-lint 支持的更多 linter,可以在終端中輸入 golangci-lint linters 命令查看,并且可以看到每個(gè) linter 的說明。
如果要修改默認(rèn)啟用的 linter,就需要對 golangci-lint 進(jìn)行配置,即在項(xiàng)目根目錄下新建一個(gè)名字為 .golangci.yml 的文件,這就是 golangci-lint 的配置文件。在運(yùn)行代碼規(guī)范檢查的時(shí)候,golangci-lint 會自動使用它。假設(shè)只啟用 unused 檢查,可以這樣配置:
linters:
disable-all: true
enable:
- unused
在團(tuán)隊(duì)多人協(xié)作開發(fā)中,需要使用一個(gè)固定的 golangci-lint 版本,這樣大家就可以基于同樣的標(biāo)準(zhǔn)檢查代碼。要配置 golangci-lint 使用的版本可以在配置文件中添加如下代碼:
service:
golangci-lint-version: 1.53.3 # use the fixed version to not introduce new linters unexpectedly
此外,還可以針對每個(gè)啟用的 linter 各自進(jìn)行配置,比如要設(shè)置 misspell 這個(gè) linter 拼寫檢測的語言為 US,可以使用如下代碼設(shè)置:
linters-settings:
misspell:
# Correct spellings using locale preferences for US or UK.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
# Default is to use a neutral variety of English.
locale: US
# Default: []
ignore-words:
- someword
關(guān)于 golangci-lint 的更多配置可以參考官方文檔,這里給出一個(gè)常用的配置,代碼如下:
linters:
disable-all: true
enable:
- errcheck
- goconst
- goimports
- gosimple
- govet
- ineffassign
- misspell
- staticcheck
- unused
service:
golangci-lint-version: 1.53.3 # use the fixed version to not introduce new linters unexpectedly
1.2.4 集成 golangci-lint 到 CI
代碼檢查一定要集成到 CI 流程中,這樣開發(fā)者提交代碼的時(shí)候,CI 就會自動檢查代碼,及時(shí)發(fā)現(xiàn)問題并進(jìn)行修正。
不管使用 Jenkins,還是 Gitlab CI,或者 Github Action,都可以通過Makefile的方式運(yùn)行 golangci-lint??梢栽陧?xiàng)目根目錄下創(chuàng)建一個(gè) Makefile 文件,并添加如下代碼:
getdeps:
@mkdir -p ${GOPATH}/bin
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.3
)
lint:
@echo "Running $@ check"
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
verifiers: getdeps lint
之后把如下命令添加到 CI 中了,就可以自動安裝 golangci-lint 并檢查代碼。
make verifiers
2. 性能優(yōu)化
性能優(yōu)化的目的是讓程序更好、更快地運(yùn)行,但是它不是必要的。所以在程序開始的時(shí)候不必刻意追求性能優(yōu)化,先大膽地寫代碼就好了,寫正確的代碼是性能優(yōu)化的前提。
是否進(jìn)行性能優(yōu)化取決于兩點(diǎn):業(yè)務(wù)需求和自我驅(qū)動。所以不要刻意地去做性能優(yōu)化,尤其是不要提前做,先保證代碼正確并上線,然后再根據(jù)業(yè)務(wù)需要,決定是否進(jìn)行優(yōu)化以及花多少時(shí)間優(yōu)化。自我驅(qū)動其實(shí)是一種編碼能力的體現(xiàn),比如有經(jīng)驗(yàn)的開發(fā)者在編碼的時(shí)候,潛意識地就避免了逃逸,減少了內(nèi)存拷貝,在高并發(fā)的場景中設(shè)計(jì)了低延遲的架構(gòu)。
2.1 堆內(nèi)存和棧內(nèi)存
在比較 C 語言中,內(nèi)存分配是手動申請的,內(nèi)存釋放也需要手動完成。
- 手動控制有一個(gè)很大的好處就是需要多少就申請多少,可以最大化地利用內(nèi)存;
- 但是這種方式也有一個(gè)明顯的缺點(diǎn),就是如果忘記釋放內(nèi)存,就會導(dǎo)致內(nèi)存泄漏。
為了讓程序員更好地專注于業(yè)務(wù)代碼的實(shí)現(xiàn),Go 語言增加了垃圾回收機(jī)制,自動地回收不再使用的內(nèi)存。
Go 語言有兩部分內(nèi)存空間:棧內(nèi)存和堆內(nèi)存。
- 棧內(nèi)存由編譯器自動分配和釋放,開發(fā)者無法控制。棧內(nèi)存一般存儲函數(shù)中的局部變量、參數(shù)等,函數(shù)創(chuàng)建的時(shí)候,這些內(nèi)存會被自動創(chuàng)建;函數(shù)返回的時(shí)候,這些內(nèi)存會被自動釋放。
- 堆內(nèi)存的生命周期比棧內(nèi)存要長,如果函數(shù)返回的值還會在其他地方使用,那么這個(gè)值就會被編譯器自動分配到堆上。堆內(nèi)存相比棧內(nèi)存來說,不能自動被編譯器釋放,只能通過垃圾回收器才能釋放,所以棧內(nèi)存效率會很高。
2.2 逃逸分析
既然棧內(nèi)存的效率更高,肯定是優(yōu)先使用棧內(nèi)存。判斷 Go 語言將一個(gè)變量分配到堆上還是棧上,需要逃逸分析。
示例:
func newString() *string{
s:=new(string)
*s = "小明"
return s
}
在這個(gè)示例中:
- 通過 new 函數(shù)申請了一塊內(nèi)存;
- 然后把它賦值給了指針變量 s;
- 最后通過 return 關(guān)鍵字返回。
通過逃逸分析來看下是否發(fā)生了逃逸,命令如下:
? go build -gcflags="-m -l" ./main.go
# command-line-arguments
./main.go:16:8: new(string) escapes to heap
在這一命令中,-m 表示輸出優(yōu)化信息(可以打印出逃逸分析信息),-l 表示禁止內(nèi)聯(lián)優(yōu)化,可以更好地觀察逃逸。從以上輸出結(jié)果可以看到,發(fā)生了逃逸,也就是說指針作為函數(shù)返回值的時(shí)候,一定會發(fā)生逃逸。
gcflags參數(shù)可以用于指定編譯器的參數(shù),可以用于控制代碼生成行為、優(yōu)化等。
逃逸到堆內(nèi)存的變量不能馬上被回收,只能通過垃圾回收標(biāo)記清除,增加了垃圾回收的壓力,所以要盡可能地避免逃逸,讓變量分配在棧內(nèi)存上,這樣函數(shù)返回時(shí)就可以回收資源,提升效率。
對上述代碼進(jìn)行避免逃逸的優(yōu)化,優(yōu)化后的函數(shù)代碼如下:
func newString() string{
s:=new(string)
*s = "小明"
return *s
}
再次通過命令查看以上代碼的逃逸分析,命令如下:
? go build -gcflags="-m -l" ./main.go
# command-line-arguments
./main.go:14:8: new(string) does not escape
雖然還是聲明了指針變量 s,但是函數(shù)返回的并不是指針,所以沒有發(fā)生逃逸。
關(guān)于指針作為函數(shù)返回逃逸的例子,有時(shí)不直接使用指針也會發(fā)生逃逸,示例代碼如下:
fmt.Println("小明")
運(yùn)行逃逸分析會看到如下結(jié)果:
? go build -gcflags="-m -l" ./main.go
# command-line-arguments
./main.go:13:13: ... argument does not escape
./main.go:13:14: "小明" escapes to heap
./main.go:17:8: new(string) does not escape
可以看到,「小明」這個(gè)字符串逃逸到了堆上,這是因?yàn)椤感∶鳌惯@個(gè)字符串被已經(jīng)逃逸的指針變量引用,所以它也跟著逃逸了,引用代碼如下:
func (p *pp) printArg(arg interface{}, verb rune) {
p.arg = arg
//省略其他無關(guān)代碼
}
所以被已經(jīng)逃逸的指針引用的變量也會發(fā)生逃逸。
Go 語言中有 3 個(gè)比較特殊的類型,它們是 slice、map 和 chan,被這三種類型引用的指針也會發(fā)生逃逸,示例如下:
func main() {
m:=map[int]*string{}
s:="小明"
m[0] = &s
}
同樣運(yùn)行逃逸分析,結(jié)果如下:
? gotour go build -gcflags="-m -l" ./main.go
# command-line-arguments
./main.go:16:2: moved to heap: s
./main.go:15:20: map[int]*string literal does not escape
從這一結(jié)果可以看到,變量 m 沒有逃逸,反而被變量 m 引用的變量 s 逃逸到了堆上。所以被map、slice 和 chan 這三種類型引用的指針一定會發(fā)生逃逸的。
逃逸分析是判斷變量是分配在堆上還是棧上的一種方法,在實(shí)際的項(xiàng)目中要盡可能避免逃逸,這樣就不會被 GC 拖慢速度,從而提升效率。
從逃逸分析來看,指針雖然可以減少內(nèi)存的拷貝,但它同樣會引起逃逸,所以要根據(jù)實(shí)際情況選擇是否使用指針。
2.3 優(yōu)化技巧
幾個(gè)優(yōu)化的小技巧:
- 盡可能避免逃逸,因?yàn)闂?nèi)存效率更高,還不用 GC。比如小對象的傳參,array 要比 slice 效果好;
- 如果避免不了逃逸,還是在堆上分配了內(nèi)存,那么對于頻繁的內(nèi)存申請操作,要學(xué)會重用內(nèi)存,比如使用 sync.Pool;
- 選用合適的算法,達(dá)到高性能的目的,比如空間換時(shí)間。
性能優(yōu)化的時(shí)候,要結(jié)合基準(zhǔn)測試,來驗(yàn)證自己的優(yōu)化是否有提升。
除上述3點(diǎn)之外,還有一些小技巧,比如要盡可能避免使用鎖、并發(fā)加鎖的范圍要盡可能小、使用 StringBuilder 做 string 和 [ ] byte 之間的轉(zhuǎn)換、defer 嵌套不要太多等等。
Go 語言有一個(gè)自帶的性能剖析的工具 pprof,通過它可以查看 CPU 分析、內(nèi)存分析、阻塞分析、互斥鎖分析等。
三、協(xié)作開發(fā)
在 Go 語言中,包是同一目錄中,編譯在一起的源文件的集合。包里面含有函數(shù)、類型、變量和常量,不同包之間的調(diào)用,必須要首字母大寫才可以。
而模塊又是相關(guān)的包的集合,它里面包含了很多為了實(shí)現(xiàn)該模塊的包,并且還可以通過模塊的方式,把已經(jīng)完成的模塊提供給其他項(xiàng)目(模塊)使用,達(dá)到了代碼復(fù)用、研發(fā)效率提高的目的。
所以對于一個(gè)項(xiàng)目(模塊)來說,它具有模塊 ? 包 ? 函數(shù)類型這樣三層結(jié)構(gòu),同一個(gè)模塊中,可以通過包組織代碼,達(dá)到代碼復(fù)用的目的;在不同模塊中,就需要通過模塊的引入,達(dá)到這個(gè)目的。
1. Go 語言中的包
1.1 定義
在 Go 語言中,一個(gè)包是通過package 關(guān)鍵字定義的,最常見的就是main 包,它的定義如下所示:
package main
一個(gè)包就是一個(gè)獨(dú)立的空間,可以在這個(gè)包里定義函數(shù)、結(jié)構(gòu)體等。這時(shí)可以認(rèn)為這些函數(shù)、結(jié)構(gòu)體是屬于這個(gè)包的。
1.2 使用包
如果想使用一個(gè)包里的函數(shù)或者結(jié)構(gòu)體,就需要先導(dǎo)入這個(gè)包,才能使用,比如常用的 fmt包,代碼示例如下所示:
package main
import "fmt"
func main() {
fmt.Println("先導(dǎo)入fmt包,才能使用")
}
要導(dǎo)入一個(gè)包,需要使用 import 關(guān)鍵字;如果需要同時(shí)導(dǎo)入多個(gè)包,則可以使用小括號,示例代碼如下所示:
import (
"fmt"
"os"
)
1.3 作用域
在Java 語言中,通過 public、private 這些修飾符修飾一個(gè)類的作用域,但是在Go 語言中,并沒有這樣的作用域修飾符,它是通過首字母是否大寫來區(qū)分的,這同時(shí)也體現(xiàn)了 Go 語言的簡潔。
Go 語言的作用域可以總結(jié)以下兩點(diǎn):
- Go 語言中,所有的定義,比如函數(shù)、變量、結(jié)構(gòu)體等,如果首字母是大寫,那么就可以被其他包使用;
- 如果首字母是小寫的,就只能在同一個(gè)包內(nèi)使用。
1.4 自定義包
可以自定義自己的包,通過包的方式把相同業(yè)務(wù)、相同職責(zé)的代碼放在一起。比如有一個(gè) util 包,用于存放一些常用的工具函數(shù),項(xiàng)目結(jié)構(gòu)如下所示:
test
├── main.go
└── util
└── string.go
在 Go 語言中,一個(gè)包對應(yīng)一個(gè)文件夾。上例中的 string.go 文件就屬于 util 包,它的包定義如下所示:
package util
可以看到,Go 語言中的包是代碼的一種組織形式,通過包把相同業(yè)務(wù)或者相同職責(zé)的代碼放在一起。通過包對代碼進(jìn)行歸類,便于代碼維護(hù)以及被其他包調(diào)用,提高團(tuán)隊(duì)協(xié)作效率。
1.5 init 函數(shù)
除了 main 這個(gè)特殊的函數(shù)外,Go 語言還有一個(gè)特殊的函數(shù)——init,通過它可以實(shí)現(xiàn)包級別的一些初始化操作。
init 函數(shù)沒有返回值,也沒有參數(shù),它先于 main 函數(shù)執(zhí)行,代碼如下所示:
func init() {
fmt.Println("init in main.go ")
}
一個(gè)包中可以有多個(gè) init 函數(shù),但是它們的執(zhí)行順序并不確定,所以如果定義了多個(gè) init 函數(shù)的話,要確保它們是相互獨(dú)立的,一定不要有順序上的依賴。
init 函數(shù)作用主要就是在導(dǎo)入一個(gè)包時(shí),對這個(gè)包做一些必要的初始化操作,比如數(shù)據(jù)庫連接和一些數(shù)據(jù)的檢查,確??梢哉_地使用這個(gè)包。
2. Go 語言中的模塊
在 Go 語言中,一個(gè)模塊可以包含很多個(gè)包,所以模塊是相關(guān)的包的集合。
在 Go 語言中:
- 一個(gè)模塊通常是一個(gè)項(xiàng)目;
- 也可以是一個(gè)框架,比如常用的 Web 框架 gin。
2.1 go mod
Go 語言提供了 go mod 命令來方便創(chuàng)建一個(gè)模塊(項(xiàng)目),比如要創(chuàng)建一個(gè) test 模塊,可以通過如下命令實(shí)現(xiàn):
? go mod init test
go: creating new go.mod: module test
運(yùn)行這一命令后,會生成一個(gè) go.mod 文件,它里面的內(nèi)容如下所示:
module test
go 1.20
- 第一句是該項(xiàng)目的模塊名,也就是 test;
- 第二句表示要編譯該模塊至少需要Go 1.20 版本的 SDK。
模塊名最好是以自己的域名開頭,比如 company.org/test,這樣就可以很大程度上保證模塊名的唯一,不至于和其他模塊重名。
2.2 使用第三方模塊
在 Github 上有很多開源的 Go 語言項(xiàng)目,它們都是一個(gè)個(gè)獨(dú)立的模塊,可以直接使用,提高開發(fā)效率,比如 Web 框架 gin-gonic/gin。
在使用第三方模塊之前,需要先設(shè)置下 Go 代理,也就是 GOPROXY,這樣就可以獲取到第三方模塊。
可以使用 goproxy.io 這個(gè)代理,進(jìn)行如下代碼設(shè)置即可:
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct
在實(shí)際的項(xiàng)目開發(fā)中,除了第三方模塊外,還有自己開發(fā)的模塊,可以放在公司的 GitLab上,然后通過 Go 語言提供的GOPRIVATE 這個(gè)環(huán)境變量來設(shè)置私有模塊的代理和下載策略。當(dāng)使用go get命令下載或更新依賴模塊時(shí),Go會首先檢查模塊是否為私有模塊,如果是私有模塊,則根據(jù)GOPRIVATE的設(shè)置來確定下載策略。
具體來說,當(dāng)GOPRIVATE中有匹配的模式時(shí),Go會使用私有模塊代理進(jìn)行下載。如果GOPRIVATE中沒有任何模式匹配,則Go會使用公共代理或直接從代碼倉庫中下載。示例如下:
# 設(shè)置不走 proxy 的私有倉庫,多個(gè)用逗號相隔(可選)。
go env -w GOPRIVATE=*.corp.example.com
要使用一個(gè)具體的模塊,首先需要安裝它。以 Gin 這個(gè) Web 框架為例,通過如下命令即可安裝:
go get -u github.com/gin-gonic/gin
安裝成功后,像標(biāo)準(zhǔn)包一樣通過 import 命令導(dǎo)入即可,代碼如下所示:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
fmt.Println("先導(dǎo)入fmt包,才能使用")
r := gin.Default()
r.Run()
}
以上代碼現(xiàn)在還無法編譯通過,因?yàn)檫€沒有同步 Gin 這個(gè)模塊的依賴,也就是沒有把它添加到go.mod 文件中。通過如下命令可以添加缺失的模塊:文章來源:http://www.zghlxwxcb.cn/news/detail-639517.html
go mod tidy
該命令可以把缺失的模塊添加進(jìn)來,同時(shí)也可以移除不再需要的模塊。所以不用手動去修改 go.mod 文件,通過 Go 語言的工具鏈比如 go mod tidy 命令,就可以自動地維護(hù)、自動地添加或者修改 go.mod 的內(nèi)容。文章來源地址http://www.zghlxwxcb.cn/news/detail-639517.html
到了這里,關(guān)于【go語言學(xué)習(xí)筆記】04 Go 語言工程管理的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!