手把手教你如何進行 Golang 單元測試
引入
隨着工程化開發在司內大力的推廣,單元測試越來越受到廣大開發者的重視。在學習的過程中,發現網上針對 Golang 單元測試大多從理論角度出發介紹,缺乏完整的實例說明,晦澀難懂的 API 讓初學接觸者難以下手。
本篇不準備大而全的談論單元測試、籠統的介紹 Golang 的單測工具,而將從 Golang 單測的使用場景出發,以最簡單且實際的例子講解如何進行單測,最終由淺入深探討 go 單元測試的兩個比較細節的問題。
在閱讀本文時,請務必對 Golang 的單元測試有最基本的瞭解。
一段需要單測的 Golang 代碼
package unit
import (
"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 unit
import (
"reflect"
"testing"
)
func TestGetPersonDetail(t *testing.T) {
type args struct {
username string
}
tests := []struct {
name string
args args
want *PersonDetail
wantErr 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 string
args args
want *PersonDetail
wantErr 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 string
args args
want 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 string
args args
want *PersonDetail
wantErr 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 server
Receive() (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
生成的代碼參考
https://github.com/xunan007/go_unit_test/blob/master/mock_redis.go
完善 gomock 相關邏輯
func Test_getPersonDetailRedis(t *testing.T) {
tests := []struct {
name string
want *PersonDetail
wantErr 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 接口的 mockConn
mockConn := 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 diff
assert.Equal(t, tt.want, actual)
assert.Equal(t, tt.wantErr, err != nil)
}
}
從上面可以看到,給 getPersonDetailRedis
函數做單元測試主要做了四件事情:
-
生成符合
redis.Conn
接口的mockConn
-
給接口打樁序列
-
給函數
redis.Dial
打樁 -
斷言
這裏面同時使用了 gomock、gomonkey 和 testify 三個包作爲壓測工具,日常使用中,由於複雜的調用邏輯帶來繁雜的單測,也無外乎使用這三個包協同完成。
查看單測報告
單元測試編寫完畢之後,我們可以調用相關的指令來查看覆蓋範圍,幫助我們查看單元測試是否已經完全覆蓋邏輯代碼,以便我們及時調整單測邏輯和用例。本文中完整的單測代碼參考:
https://github.com/xunan007/go_unit_test/blob/master/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] = double
original := replace(*(*uintptr)(getPointer(target)), uintptr(getPointer(double)))
this.originals[target] = original
return 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, double
0xFF, 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 := a
f()
}
上面這段代碼, 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 代碼的優化:
https://juejin.cn/post/6924888439577903117
monkey patching in go:
https://bou.ke/blog/monkey-patching-in-go/
阮一峯 -- 彙編入門:
http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Plvrr71MTo-RhSfSAPoLFQ