日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

​手把手教你如何进行 Golang 单元测试

發布時間:2024/2/28 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ​手把手教你如何进行 Golang 单元测试 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

作者:stevennzhou,騰訊 PCG 前端開發工程師

本篇是對單元測試的一個總結,通過完整的單元測試手把手教學,能夠讓剛接觸單元測試的開發者從整體上了解一個單元測試編寫的全過程。最終通過兩個問題,也能讓寫過單元測試的開發者收獲單測執行時的一些底層細節知識。

引入

隨著工程化開發在司內大力的推廣,單元測試越來越受到廣大開發者的重視。在學習的過程中,發現網上針對 Golang 單元測試大多從理論角度出發介紹,缺乏完整的實例說明,晦澀難懂的 API 讓初學接觸者難以下手。

本篇不準備大而全的談論單元測試、籠統的介紹 Golang 的單測工具,而將從 Golang 單測的使用場景出發,以最簡單且實際的例子講解如何進行單測,最終由淺入深探討 go 單元測試的兩個比較細節的問題。

在閱讀本文時,請務必對 Golang 的單元測試有最基本的了解。

一段需要單測的 Golang 代碼

package?unitimport?("encoding/json""errors""github.com/gomodule/redigo/redis""regexp" )type?PersonDetail?struct?{Username?string?`json:"username"`Email????string?`json:"email"` }//?檢查用戶名是否非法 func?checkUsername(username?string)?bool?{const?pattern?=?`^[a-z0-9_-]{3,16}$`reg?:=?regexp.MustCompile(pattern)return?reg.MatchString(username) }//?檢查用戶郵箱是否非法 func?checkEmail(email?string)?bool?{const?pattern?=?`^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$`reg?:=?regexp.MustCompile(pattern)return?reg.MatchString(email) }//?通過?redis?拉取對應用戶的資料信息 func?getPersonDetailRedis(username?string)?(*PersonDetail,?error)?{result?:=?&PersonDetail{}client,?err?:=?redis.Dial("tcp",?":6379")defer?client.Close()data,?err?:=?redis.Bytes(client.Do("GET",?username))if?err?!=?nil?{return?nil,?err}err?=?json.Unmarshal(data,?result)if?err?!=?nil?{return?nil,?err}return?result,?nil }//?拉取用戶資料信息并校驗 func?GetPersonDetail(username?string)?(*PersonDetail,?error)?{//?檢查用戶名是否有效if?ok?:=?checkUsername(username);?!ok?{return?nil,?errors.New("invalid?username")}//?從?redis?接口獲取信息detail,?err?:=?getPersonDetailRedis(username)if?err?!=?nil?{return?nil,?err}//?校驗if?ok?:=?checkEmail(detail.Email);?!ok?{return?nil,?errors.New("invalid?email")}return?detail,?nil }

這是一段典型的有 I/O 的功能代碼,主體功能是傳入用戶名,校驗合法性之后通過 redis 獲取信息,之后校驗獲取值內容的合法性后并返回。

后臺服務單測場景

對于一個傳統的后端服務,它主要有以下幾點的職責和功能:

  • 接收外部請求,controller 層分發請求、校驗請求參數

  • 請求有效分發后,在 service 層與 dao 層進行交互后做邏輯處理

  • dao 層負責數據操作,主要是數據庫或持久化存儲相關的操作

因此,從職責出發來看,在做后臺單測中,核心主要是驗證 service 層和 dao 層的相關邏輯,此外 controller 層的參數校驗也在單測之中。

細分來看,對于相關邏輯的單元測試,筆者傾向于把單測分為兩種:

  • 無第三方依賴,純邏輯代碼

  • 有第三方依賴,如文件、網絡 I/O、第三方依賴庫、數據庫操作相關的代碼

注:單元測試中只是針對單個函數的測試,關注其內部的邏輯,對于網絡/數據庫訪問等,需要通過相應的手段進行 mock。

Golang 單測工具選型

由于我們把單測簡單的分為了兩種:

  • 對于無第三方依賴的純邏輯代碼,我們只需要驗證相關邏輯即可,這里只需要使用 assert (斷言),通過控制輸入輸出比對結果即可。

  • 對于有第三方依賴的代碼,在驗證相關代碼邏輯之前,我們需要將相關的依賴 mock (模擬),之后才能通過斷言驗證邏輯。這里需要借助第三方工具庫來處理。

因此,對于 assert (斷言)工具,可以選擇 testify 或 convery,筆者這里選擇了 testify。對于 mock (模擬)工具,筆者這里選擇了 gomock 和 gomonkey。關于 mock 工具同時使用 gomock 和 gomonkey,這里跟 Golang 的語言特性有關,下面會詳細的說明。

完善測試用例

這里我們開始對示例代碼中的函數做單元測試。

生成單測模板代碼

首先在 Goland 中打開項目,加載對應文件后右鍵找到 Generate 項,點擊后選擇 Tests for package,之后生成以 _test.go 結尾的單測文件。(如果想針對某一特定函數做單測,請選擇對應的函數后右鍵選定 Generate 項執行 Tests for selection。)

這里展示通過 IDE 生成的 TestGetPersonDetail 測試函數:

package?unitimport?("reflect""testing" )func?TestGetPersonDetail(t?*testing.T)?{type?args?struct?{username?string}tests?:=?[]struct?{name????stringargs????argswant????*PersonDetailwantErr?bool}{//?TODO:?Add?test?cases.}for?_,?tt?:=?range?tests?{t.Run(tt.name,?func(t?*testing.T)?{got,?err?:=?GetPersonDetail(tt.args.username)if?(err?!=?nil)?!=?tt.wantErr?{t.Errorf("GetPersonDetail()?error?=?%v,?wantErr?%v",?err,?tt.wantErr)return}if?!reflect.DeepEqual(got,?tt.want)?{t.Errorf("GetPersonDetail()?got?=?%v,?want?%v",?got,?tt.want)}})} }

由 Goland 生成的單測模板代碼使用的是官方的 testing 框架,為了更方便的斷言,我們把 testing 改造成 testify 的斷言方式。

這里其實只需要引入 testify 后修改 test 函數最后的斷言代碼即可,這里我們以 TestGetPersonDetail 為例子,其他函數不贅述。

package?unit import?("github.com/stretchr/testify/assert"?//?這里引入了?testify"reflect""testing" )func?TestGetPersonDetail(t?*testing.T)?{type?args?struct?{username?string}tests?:=?[]struct?{name????stringargs????argswant????*PersonDetailwantErr?bool}{//?TODO:?Add?test?cases.}for?_,?tt?:=?range?tests?{got,?err?:=?GetPersonDetail(tt.args.username)//?改寫這里斷言的方式即可assert.Equal(t,?tt.want,?got)assert.Equal(t,?tt.wantErr,?err?!=?nil)} }

分析代碼生成測試用例

對 checkUsername 、 checkEmail 純邏輯函數編寫測試用例,這里以 checkEmail 為例。

func?Test_checkEmail(t?*testing.T)?{type?args?struct?{email?string}tests?:=?[]struct?{name?stringargs?argswant?bool}{{name:?"email?valid",args:?args{email:?"1234567@qq.com",},want:?true,},{name:?"email?invalid",args:?args{email:?"test.com",},want:?false,},}for?_,?tt?:=?range?tests?{got?:=?checkEmail(tt.args.email)assert.Equal(t,?tt.want,?got)} }

使用 gomonkey 打樁

對于 GetPersonDetail 函數而言,該函數調用了 getPersonDetailRedis 函數獲取具體的 PersonDetail 信息。為此,我們需要為它打一個“樁”。

所謂的“樁”,也叫做“樁代碼”,是指用來代替關聯代碼或者未實現代碼的代碼。

對于函數、成員方法或者是變量的打樁,我們通常使用 gomonkey 來進行打樁。具體 API 請參考:https://pkg.go.dev/github.com/agiledragon/gomonkey

//?拉取用戶資料信息并校驗 func?GetPersonDetail(username?string)?(*PersonDetail,?error)?{//?檢查用戶名是否有效if?ok?:=?checkUsername(username);?!ok?{return?nil,?errors.New("invalid?username")}//?從?redis?接口獲取信息detail,?err?:=?getPersonDetailRedis(username)if?err?!=?nil?{return?nil,?err}//?校驗if?ok?:=?checkEmail(detail.Email);?!ok?{return?nil,?errors.New("invalid?email")}return?detail,?nil }

從 GetPersonDetail 函數可見,為了能夠完全覆蓋該函數,我們需要控制 getPersonDetailRedis 函數不同的輸出來保證后續代碼都能夠被覆蓋運行到。因此,這里需要使用 gomonkey 來給 getPersonDetailRedis 函數打一個“樁序列”。

所謂的函數“樁序列”指的是提前指定好調用函數的返回值序列,當該函數多次調用時候,能夠按照原先指定的返回值序列依次返回。

func?TestGetPersonDetail(t?*testing.T)?{type?args?struct?{username?string}tests?:=?[]struct?{name????stringargs????argswant????*PersonDetailwantErr?bool}{{name:?"invalid?username",?args:?args{username:?"steven?xxx"},?want:?nil,?wantErr:?true},{name:?"invalid?email",?args:?args{username:?"invalid_email"},?want:?nil,?wantErr:?true},{name:?"throw?err",?args:?args{username:?"throw_err"},?want:?nil,?wantErr:?true},{name:?"valid?return",?args:?args{username:?"steven"},?want:?&PersonDetail{Username:?"steven",?Email:?"12345678@qq.com"},?wantErr:?false},}//?為函數打樁序列//?使用?gomonkey?打函數樁序列//?第一個用例不會調用?getPersonDetailRedis,所以只需要?3?個值outputs?:=?[]gomonkey.OutputCell{{Values:?gomonkey.Params{&PersonDetail{Username:?"invalid_email",?Email:?"test.com"},?nil},},{Values:?gomonkey.Params{nil,?errors.New("request?err")},},{Values:?gomonkey.Params{&PersonDetail{Username:?"steven",?Email:?"12345678@qq.com"},?nil},},}patches?:=?gomonkey.ApplyFuncSeq(getPersonDetailRedis,?outputs)//?執行完畢后釋放樁序列defer?patches.Reset()for?_,?tt?:=?range?tests?{got,?err?:=?GetPersonDetail(tt.args.username)assert.Equal(t,?tt.want,?got)assert.Equal(t,?tt.wantErr,?err?!=?nil)} }

當使用樁序列時,要分析好單元測試用例和序列值的對應關系,保證最終被測試的代碼塊都能被完整覆蓋。

使用 gomock 打樁

最后剩下 getPersonDetailRedis 函數,我們先來看一下這個函數的邏輯。

//?通過?redis?拉取對應用戶的資料信息 func?getPersonDetailRedis(username?string)?(*PersonDetail,?error)?{result?:=?&PersonDetail{}client,?err?:=?redis.Dial("tcp",?":6379")defer?client.Close()data,?err?:=?redis.Bytes(client.Do("GET",?username))if?err?!=?nil?{return?nil,?err}err?=?json.Unmarshal(data,?result)if?err?!=?nil?{return?nil,?err}return?result,?nil }

getPersonDetailRedis 函數的核心在于生成了 client 調用了它的 Do 方法,通過分析得知 client 實際上是一個符合 Conn 接口的結構體。如果我們使用 gomonkey 來進行打樁,需要先聲明一個結構體并實現 Client 接口擁有的方法,之后才能使用 gomonkey 給函數打樁。

//?redis?包中關于?Conn?的定義 //?Conn?represents?a?connection?to?a?Redis?server. type?Conn?interface?{//?Close?closes?the?connection.Close()?error//?Err?returns?a?non-nil?value?when?the?connection?is?not?usable.Err()?error//?Do?sends?a?command?to?the?server?and?returns?the?received?reply.Do(commandName?string,?args?...interface{})?(reply?interface{},?err?error)//?Send?writes?the?command?to?the?client's?output?buffer.Send(commandName?string,?args?...interface{})?error//?Flush?flushes?the?output?buffer?to?the?Redis?server.Flush()?error//?Receive?receives?a?single?reply?from?the?Redis?serverReceive()?(reply?interface{},?err?error) }//?實現接口 type?Client?struct?{} func?(c?*Client)?Close()?error?{return?nil } func?(c?*Client)?Err()?error?{return?nil } func?(c?*Client)?Do(commandName?string,?args?...interface{})?(interface{},?error)?{return?nil,?nil } func?(c?*Client)?Send(commandName?string,?args?...interface{})?error?{return?nil } func?(c?*Client)?Flush()?error?{return?nil } func?(c?*Client)?Receive()?(interface{},?error)?{return?nil,?nil }//?實現接口 type?Client?struct?{} func?(c?*Client)?Close()?error?{return?nil } func?(c?*Client)?Err()?error?{return?nil } func?(c?*Client)?Do(commandName?string,?args?...interface{})?(interface{},?error)?{return?nil,?nil } func?(c?*Client)?Send(commandName?string,?args?...interface{})?error?{return?nil } func?(c?*Client)?Flush()?error?{return?nil } func?(c?*Client)?Receive()?(interface{},?error)?{return?nil,?nil } //?進行測試 func?test()?{c?:=?&Client{}gomonkey.ApplyFunc(redis.Dial,?func(_?string,?_?string,?_?...redis.DialOption)?(redis.Conn,?error)?{return?c,?nil})gomonkey.ApplyMethod(reflect.TypeOf(c),?"Do",?func(commandName?string,?args?...interface{})?(interface{},?error)?{var?result?interface{}return?result,?nil}) }

可見,如果接口實現的方法更多,那么打樁需要手寫的代碼會更多。因此這里需要一種能自動根據原接口的定義生成接口的 mock 代碼以及更方便的接口 mock 方式。于是這里我們使用 gomock 來解決這個問題。

本地安裝 gomock
#?打開終端后依次執行 go?get?-u?github.com/golang/mock/gomock go?install?github.com/golang/mock/mockgen #?備注說明,很重要!!! #?安裝完成之后,執行?mockgen?看命令是否生效?#?如果顯示命令無效,請找到本機的?GOPATH?安裝目錄下的?bin?文件夾是否有?mockgen?二進制文件 #?GOPATH?可以執行?go?env?命令找到 #?如果命令無效但是?GOPATH?路徑下的?bin?文件夾中存在?mockgen,請將?GOPATH?下?bin?文件夾的絕對路徑添加到全局?PATH?中
生成 gomock 樁代碼

安裝完畢后,找到要進行打樁的接口,這里是 github.com/gomodule/redigo/redis 包里面的 Conn 接口。

在當前代碼目錄下執行以下指令,這里我們只對某個特定的接口生成 mock 代碼。

mockgen?-destination=mock_redis.go?-package=unit?github.com/gomodule/redigo/redis?Conn #?更多指令參考:https://github.com/golang/mock#flags

生成的代碼參考 mock_redis.go

完善 gomock 相關邏輯
func?Test_getPersonDetailRedis(t?*testing.T)?{tests?:=?[]struct?{name????stringwant????*PersonDetailwantErr?bool}{{name:?"redis.Do?err",?want:?nil,?wantErr:?true},{name:?"json.Unmarshal?err",?want:?nil,?wantErr:?true},{name:?"success",?want:?&PersonDetail{Username:?"steven",Email:????"1234567@qq.com",},?wantErr:?false},}ctrl?:=?gomock.NewController(t)defer?ctrl.Finish()//?1.?生成符合?redis.Conn?接口的?mockConnmockConn?:=?NewMockConn(ctrl)//?2.?給接口打樁序列gomock.InOrder(mockConn.EXPECT().Do("GET",?gomock.Any()).Return("",?errors.New("redis.Do?err")),mockConn.EXPECT().Close().Return(nil),mockConn.EXPECT().Do("GET",?gomock.Any()).Return("123",?nil),mockConn.EXPECT().Close().Return(nil),mockConn.EXPECT().Do("GET",?gomock.Any()).Return([]byte(`{"username":?"steven",?"email":?"1234567@qq.com"}`),?nil),mockConn.EXPECT().Close().Return(nil),)//?3.?給?redis.Dail?函數打樁outputs?:=?[]gomonkey.OutputCell{{Values:?gomonkey.Params{mockConn,?nil},Times:??3,?//?3?個用例},}patches?:=?gomonkey.ApplyFuncSeq(redis.Dial,?outputs)//?執行完畢之后釋放樁序列defer?patches.Reset()//?4.?斷言for?_,?tt?:=?range?tests?{actual,?err?:=?getPersonDetailRedis(tt.name)//?注意,equal?函數能夠對結構體進行?deap?diffassert.Equal(t,?tt.want,?actual)assert.Equal(t,?tt.wantErr,?err?!=?nil)} }

從上面可以看到,給 getPersonDetailRedis 函數做單元測試主要做了四件事情:

  • 生成符合 redis.Conn 接口的 mockConn

  • 給接口打樁序列

  • 給函數 redis.Dial 打樁

  • 斷言

這里面同時使用了 gomock、gomonkey 和 testify 三個包作為壓測工具,日常使用中,由于復雜的調用邏輯帶來繁雜的單測,也無外乎使用這三個包協同完成。

查看單測報告

單元測試編寫完畢之后,我們可以調用相關的指令來查看覆蓋范圍,幫助我們查看單元測試是否已經完全覆蓋邏輯代碼,以便我們及時調整單測邏輯和用例。本文中完整的單測代碼參考:get_person_detail_test.go

使用 go test 指令

默認情況下,我們在當前代碼目錄下執行 go test 指令,會自動的執行當前目錄下面帶 _test.go 后綴的文件進行測試。如若想展示具體的測試函數以及覆蓋率,可以添加 -v 和 -cover 參數,如下所示:

????go_unit_test?[master]???????go?test?-v?-cover ===?RUN???TestGetPersonDetail ---?PASS:?TestGetPersonDetail?(0.00s) ===?RUN???Test_checkEmail ---?PASS:?Test_checkEmail?(0.00s) ===?RUN???Test_checkUsername ---?PASS:?Test_checkUsername?(0.00s) ===?RUN???Test_getPersonDetailRedis ---?PASS:?Test_getPersonDetailRedis?(0.00s) PASS coverage:?60.8%?of?statements ok??????unit????0.131s

如果想指定測試某一個函數,可以在指令后面添加 -run ${test文件內函數名} 來指定執行。

????go_unit_test?[master]???????go?test?-cover?-v??-run?Test_getPersonDetailRedis ===?RUN???Test_getPersonDetailRedis ---?PASS:?Test_getPersonDetailRedis?(0.00s) PASS coverage:?41.9%?of?statements ok??????unit????0.369s

在執行 go test 命令時,需要加上 -gcflags=all=-l 防止編譯器內聯優化導致單測出現問題,這跟打樁代碼存在密切的關系,后面我們會詳細的介紹這一點。

因此,一個完整的單測指令可以是 go test -v -cover -gcflags=all=-l -coverprofile=coverage.out

生成覆蓋報告

最后,我們可以執行 go tool cover -html=coverage.out ,查看代碼的覆蓋情況,使用前請先安裝好 go tool 工具。


可以看到待測的代碼覆蓋率達到 100% 了,完整的代碼倉庫可以參考:https://github.com/xunan007/go_unit_test

關于 go test 更多的使用方法,可以參考:

https://golang.org/pkg/cmd/go/internal/test/

思考

上面我們已經詳細的介紹了如何對 go 代碼進行單元測試。下面探討兩個問題,幫助我們深入理解 go 單元測試的過程。

Q1:樁代碼在單測中是如何執行的

在上面的案例中,針對 interface 我們通過 gomock 來幫我們自動生成符合接口的類后,只需要通過 gomock 約定的 API 就能夠對 interface 中的函數按期望和需要來模擬,這個很好理解。

對于函數以及方法的 mock,由于本身代碼邏輯已經聲明好(go 是靜態強類型語言),我們很難通過編碼的方式將其 mock 掉,這對我們做單元測試提供了很大的挑戰。實際上 gomonkey 提供了讓我們在運行時替換原函數/方法的能力。雖然說我們在語言層面很難去替換運行中的函數體,但是本身代碼最終都會轉換成機器可以理解的匯編指令,我們可以通過創建指令來改寫函數。

在 gomonkey 打樁的過程中,其核心函數其實是 ApplyCore。

func?(this?*Patches)?ApplyCore(target,?double?reflect.Value)?*Patches?{this.check(target,?double)if?_,?ok?:=?this.originals[target];?ok?{panic("patch?has?been?existed")}this.valueHolders[double]?=?doubleoriginal?:=?replace(*(*uintptr)(getPointer(target)),?uintptr(getPointer(double)))this.originals[target]?=?originalreturn?this }

不管是對函數打樁還是對方法打樁,實際上最后都會調用這個 ApplyCore 函數。

在第 8 行的位置,獲取到傳入的原始函數和替換函數做了一個 replace 的操作,這里就是替換的邏輯所在了。

func?replace(target,?double?uintptr)?[]byte?{code?:=?buildJmpDirective(double)bytes?:=?entryAddress(target,?len(code))original?:=?make([]byte,?len(bytes))copy(original,?bytes)modifyBinary(target,?code)return?original }//?關鍵函數:構建跳轉指令 func?buildJmpDirective(double?uintptr)?[]byte?{d0?:=?byte(double)d1?:=?byte(double?>>?8)d2?:=?byte(double?>>?16)d3?:=?byte(double?>>?24)d4?:=?byte(double?>>?32)d5?:=?byte(double?>>?40)d6?:=?byte(double?>>?48)d7?:=?byte(double?>>?56)return?[]byte{0x48,?0xBA,?d0,?d1,?d2,?d3,?d4,?d5,?d6,?d7,?//?MOV?rdx,?double0xFF,?0x22,?????//?JMP?[rdx]} }//?關鍵函數:重寫目標函數 func?modifyBinary(target?uintptr,?bytes?[]byte)?{function?:=?entryAddress(target,?len(bytes))page?:=?entryAddress(pageStart(target),?syscall.Getpagesize())err?:=?syscall.Mprotect(page,?syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)if?err?!=?nil?{panic(err)}copy(function,?bytes)err?=?syscall.Mprotect(page,?syscall.PROT_READ|syscall.PROT_EXEC)if?err?!=?nil?{panic(err)} }

從上面的代碼可以看出,buildJmpDirective 構建了一個函數跳轉的指令,把目標函數指針移動到寄存器 rdx 中,然后跳轉到寄存器 rdx 中函數指針指向的地址。之后通過 modifyBinary 函數,先通過 entryAddress 方法獲取到原函數所在的內存地址,之后通過 syscall.Mprotect 方法打開內存保護,將函數跳轉指令以 bytes 數組的形式調用 copy 方法寫入到原函數所在內存之中,最終達到替換的目的。此外,這里 replace 方法還保留了原函數的副本,方便后續函數 mock 的恢復。

為什么 buildJmpDirective 要構建這樣的跳轉指令呢?這里只說結論,具體的推導過程可以參考:https://bou.ke/blog/monkey-patching-in-go

package?main func?a()?int?{?return?1?} func?main()?{f?:=?af() }

上面這段代碼,a 是一個指向函數實體的指針,f 是指向函數 a 指針的指針。把上面函數的調用反匯編,能夠看到操作寄存器的具體細節。( 如果對匯編不是很了解,可以先閱讀 http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html )

第一行,lea 為 load effective address,這里是將 f 變量這個值直接賦給 rdx 寄存器, f 變量的值是指向 a 函數的地址。

第二行,mov 表示移動,這里是取到內存地址為 rdx 的數據賦值給 rbx,此時內存地址 rbx 指向的剛好就是 a 函數。

最后,調用 rbx 里面的內容,其實也就是執行函數體。

因此,我們想改寫函數,只要想辦法把需要跳轉的函數的地址加載到 rdx 寄存器中,之后使用指令跳轉執行。

MOV?rdx,?double JMP?[rdx]

最終,把匯編指令翻譯成 go 能夠識別的版本。

這其實也是匯編里面很常見的熱補丁,多用于進程中函數的替換。

Q2:執行 -gcflags=all=-l 具體有什么作用

-gcflags 用于在 go 編譯構建時進行參數的傳遞,all 表示覆蓋所有在 GOPATH 中的包,-l 表示禁止編譯的內聯優化。該指令可以防止編譯時代碼內聯優化使得 mock 失敗,最終導致執行單元測試不通過。下面我們具體來探討一下“內聯”以及給單元測試帶來的影響。

通俗來講,內聯指的是把簡短的函數在調用它的地方展開。由于函數調用有固定的開銷(棧和搶占檢查),在編譯過程中,編譯器可以針對代碼進行內聯,減少函數調用開銷。內聯優化是高性能編程的一種重要手段。

在 go 中,編譯器不會對所有簡單函數進行內聯優化。go 在決策是否要對函數進行內聯時有一個標準:函數體內包含:閉包調用,select ,for ,defer,go 關鍵字的的函數不會進行內聯。并且除了這些,還有其它的限制。當解析 AST 時,Go 申請了 80 個節點作為內聯的預算。每個節點都會消耗一個預算。當一個函數的開銷超過了這個預算,就無法內聯。( 參考自:https://juejin.cn/post/6924888439577903117 )

下面我們通過一段簡短的代碼來理解 go 編譯過程的內聯優化過程。我們從 gomonkey 關于內聯的 issue 摘取了一段代碼:

package?main import?"fmt" func?G2()?string?{??return?"G2"?} func?G()?string?{??return?G2()?} func?main()?{g?:=?G()fmt.Println(g) }

上面這段代碼很簡單,main 函數中調用了 G 函數拿到返回值賦值變量給 g 后打印結果。其中 G 函數調用了 G2 函數,G2 函數返回了字符串 "G2"。

然而,經過編譯器內聯優化后的代碼,G 函數實際被展開了,最終 main 函數被內聯優化成:

func?main()?{//?展開?g?:=?G()//?=>?g?:=?"G2"//?展開?fmt.Println(g)//?=>?相關 }

可見,G 函數和 G2 函數原本執行時候帶來函數棧申請回收,優化過后將不再有。

這里我們執行 go run -gcflags="-m -m" main.go 來查看編譯在進行以上代碼的內聯優化。

????test??go?run?-gcflags="-m?-m"?main.go #?command-line-arguments ./main.go:5:6:?can?inline?G2?as:?func()?string?{?return?"G2"?}?./main.go:9:6:?can?inline?G?as:?func()?string?{?return?G2()?}?./main.go:10:11:?inlining?call?to?G2?func()?string?{?return?"G2"?}?./main.go:13:6:?cannot?inline?main:?function?too?complex:?cost?87?exceeds?budget?80 ./main.go:14:8:?inlining?call?to?G?func()?string?{?return?G2()?}?./main.go:14:8:?inlining?call?to?G2?func()?string?{?return?"G2"?}?./main.go:15:13:?inlining?call?to?fmt.Println?func(...interface?{})?(int,?error)?{?var?fmt..autotmp_3?int;?fmt..autotmp_3?=?<N>;?var?fmt..autotmp_4?error;?fmt..autotmp_4?=?<N>;?fmt..autotmp_3,?fmt..autotmp_4?=?fmt.Fprintln(io.Writer(os.Stdout),?fmt.a...);?return?fmt..autotmp_3,?fmt..autotmp_4?} ./main.go:15:13:?g?escapes?to?heap?./main.go:15:13:?main?[]interface?{}?literal?does?not?escape ./main.go:15:13:?io.Writer(os.Stdout)?escapes?to?heap?<autogenerated>:1:?(*File).close?.this?does?not?escape?G2

從打印出的內容可以看,G2\G\fmt.Println 都被內聯了。

上面提到了 gomokey 打樁的邏輯,它是在函數調用的時候通過機器指令將函數的指向替換了。由于函數編譯后被內聯,實際上不存在函數的調用,導致單測執行不通過,這也是內聯導致 gomonkey 打樁無效的問題所在。

參考

內聯函數和編譯器對 Go 代碼的優化

monkey patching in go

阮一峰--匯編入門

最近好文:

GPU虛擬化,算力隔離,和qGPU

一文入門 Kafka

騰訊代碼安全指南開源,涉及 C/C++、Go 等六門編程語言

“碼上有趣” 視頻挑戰活動來了!

上傳視頻贏HHKB鍵盤和羅技鼠標!

了解活動可加微信:teg_helper(備注碼上有趣)

最新視頻

總結

以上是生活随笔為你收集整理的​手把手教你如何进行 Golang 单元测试的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。