Go1.18新特性工作區(qū)模糊測試及泛型的使用詳解
前言
2022年3月15日,Google發(fā)布了萬眾矚目的Golang 1.18,帶來了好幾個重大的新特性,包括:
- 解決本地同時開發(fā)多個倉庫帶來的一些問題的工作區(qū)(Workspace)
- 能夠自動探測代碼分支,隨機生成輸入,并且檢查代碼是否會panic的模糊測試(Fuzzing Test)
- 眾多開發(fā)者盼星星盼月亮終于等到的泛型支持。
本文將簡單講述這三個特性的相關(guān)內(nèi)容。
Go工作區(qū)模式(Go Workspace Mode)
現(xiàn)實的情況
多倉庫同時開發(fā)
在實際的開發(fā)工作中,我們經(jīng)常會同時修改存在依賴關(guān)系的多個module,例如在某個service模塊上實現(xiàn)需求的同時,也需要對項目組的某個common模塊做出修改,整個的工作流就會變成下面這樣:
可以看到,每次修改Common庫,都需要將代碼push到遠端,然后再修改本地service倉庫的依賴,再通過go mod tidy從遠端拉取Common代碼,不可謂不麻煩。
有些同學可能會問了,這種情況,在service倉庫的go.mod中添加一條replace不就能夠解決嗎?
但是,如果在go.mod中使用replace,在維護上需要付出額外的心智成本,萬一將帶有replace的go.mod推到遠端代碼庫了,其他同學不就一臉懵逼了?
多個新倉庫開始開發(fā)
假設此時我正在開發(fā)兩個新的模塊,分別是:
code.byted.org/SomeNewProject/Common code.byted.org/SomeNewProject/MyService
并且MyService依賴于Common。
在開發(fā)過程中,出于各種原因,有可能不會立即將代碼推送到遠端,那么此時假設我需要本地編譯MyService,就會出現(xiàn)go build(或者go mod tidy)自動下載依賴失敗,因為此時Common庫根本就沒有發(fā)布到代碼庫中。
出于和上述“多倉庫同時開發(fā)”相同的理由,replace也不應該被添加到MyService的go.mod文件中。
工作區(qū)模式是什么
Go工作區(qū)模式最早出現(xiàn)于Go開發(fā)者Michael Matloob在2021年4月提出的一個名為“Multi-Module Workspaces in cmd/go”的提案。
這個提案中提出,新增一個go.work文件,并且在這個文件中指定一系列的本地路徑,這些本地路徑下的go module共同構(gòu)成一個工作區(qū)(workspace),go命令可以操作這些路徑下的go module,在編譯時也會優(yōu)先使用這些go module。
使用如下命令就可以初始化一個工作區(qū),并且生成一個空的go.work文件:
go work init .
新生成的go.work文件內(nèi)容如下:
go 1.18 directory ./.
go.work文件中,directory指示了工作區(qū)的各個module目錄,在編譯代碼時,會優(yōu)先使用同一個workspace下的module。
在go.work中,也支持使用replace來指定使用本地代碼庫,但在大多數(shù)情況下,更好的做法是將依賴的本地代碼庫的路徑加入directory中。
推薦的使用方法
因為go.work描述的是本地的工作區(qū),所以也是不能提交到遠端代碼庫的,雖然可以在.gitignore中加入這個文件,但是最推薦的做法還是在本地代碼庫的上層目錄使用go.work。
例如上述的“多個新倉庫開始開發(fā)”的例子,假設我的兩個倉庫的本地路徑分別是:
/Users/bytedance/dev/my_new_project/common /Users/bytedance/dev/my_new_project/my_service
那么我就可以在“/Users/bytedance/dev/my_new_project”目錄下生成一個如下內(nèi)容的go.work:
/Users/bytedance/dev/my_new_project/go.work: go 1.18 directory ( ./common ./my_service )
在上層目錄放置go.work,也可以將多個目錄組織成一個workspace,并且由于上層目錄本身不受git管理,所以也不用去管gitignore之類的問題,是比較省心的方式。
使用時的注意點
目前(go 1.18)僅go build會對go.work做出判斷,而go mod tidy并不care Go工作區(qū)。
Go模糊測試(Go Fuzzing Test)
為什么Golang要支持模糊測試
從1.18起,模糊測試(Fuzzing Test)作為語言安全的一環(huán),加入了Golang的testing標準庫。Golang加入模糊測試的原因非常明顯:安全是程序員在構(gòu)建軟件的過程中必不可少且日益重要的考量因素。
Golang至今為止,已經(jīng)在保障語言安全方面提供了很多的特性和工具,例如強制使用顯式類型轉(zhuǎn)換、禁止隱式類型轉(zhuǎn)換、對數(shù)組與切片的越界訪問檢查、通過go.sum對依賴包進行哈希校驗等等。
在進入云原生時代之后,Golang成為了云原生基礎設施與服務的頭部語言之一。這些系統(tǒng)對安全性的要求自然不言而喻。尤其是針對用戶的輸入,不被用戶的輸入弄出處理異常、崩潰、被操控是對這些系統(tǒng)的基本要求之一。
這就要求我們的系統(tǒng)在處理任何用戶輸入的時候都能保持穩(wěn)定,但是傳統(tǒng)的質(zhì)量保障手段,例如Code Review、靜態(tài)分析、人工測試、Unit Test等等,在面對日益復雜的系統(tǒng)時,自然就無法窮盡所有可能的輸入組合,尤其是一些非常不明顯的corner case。
而模糊測試就是業(yè)界在解決這方面問題的優(yōu)秀實踐之一,Golang選擇支持它也就不難理解了。
模糊測試是什么
模糊測試是一種通過數(shù)據(jù)構(gòu)造引擎,輔以開發(fā)者可以提供的一些初始數(shù)據(jù),自動構(gòu)造出一些隨機數(shù)據(jù),作為對程序的輸入來進行測試的一種方式。模糊測試可以幫助開發(fā)人員發(fā)現(xiàn)難以發(fā)現(xiàn)的穩(wěn)定性、邏輯性甚至是安全性方面的錯誤,特別是當被測系統(tǒng)變得更加復雜時。
模糊測試在具體的實現(xiàn)上,通??梢圆灰蕾囉陂_發(fā)測試人員定義好的數(shù)據(jù)集,取而代之的則是一組通過數(shù)據(jù)構(gòu)造引擎自行構(gòu)造的一系列隨機數(shù)據(jù)。模糊測試會將這些數(shù)據(jù)作為輸入提供給待測程序,并且監(jiān)測程序是否出現(xiàn)panic、斷言失敗、無限循環(huán),或者其他什么異常情況。這些通過數(shù)據(jù)構(gòu)造引擎生成的數(shù)據(jù)被稱為語料(corpus) 。另外模糊測試其實也是一種持續(xù)測試的手段,因為如果不限制執(zhí)行的次數(shù)或者執(zhí)行的最大時間,它就會一直不停的執(zhí)行下去。
Golang的模糊測試由于被實現(xiàn)在了編譯器工具鏈中,所以采用了一種名為“覆蓋率引導的fuzzing”的入?yún)⑸杉夹g(shù),大致運行過程如下:
Golang的模糊測試如何使用
Golang的模糊測試在使用時,可以簡單地直接使用,也可以自己提供一些初始的語料。
最簡單的實踐例子
模糊測試的函數(shù)也是放在xxx_test.go里的,編寫一個最簡單的模糊測試例子(明顯的除0錯誤):
package main import "testing" import "fmt" func FuzzDiv(f *testing.F) { f.Fuzz(func(t *testing.T, a, b int) { fmt.Println(a/b) }) }
可以看到類似于單元測試,模糊測試的函數(shù)名都是FuzzXxx格式,且接受一個testing.F指針對象。
然后在函數(shù)中使用f.Fuzz對指定的函數(shù)進行模糊測試,被測試的函數(shù)的第一個參數(shù)必須是“*testing.T”類型,后面可以跟任意多個基本類型的參數(shù)。
編寫完成之后,使用這樣的命令來啟動模糊測試:
go test -fuzz .
模糊測試默認會一直進行下去,只要被測試的函數(shù)不panic不出錯。可以通過“-fuzztime”選項來限制模糊測試的時間:
go test -fuzztime 10s -fuzz .
使用模糊測試對上述代碼進行測試時,會碰到產(chǎn)生panic的情況,此時模糊測試會輸出如下信息:
warning: starting with empty corpus
fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)
fuzz: elapsed: 0s, execs: 1 (65/sec), new interesting: 0 (total: 0)
--- FAIL: FuzzDiv (0.02s)
--- FAIL: FuzzDiv (0.00s)
testing.go:1349: panic: runtime error: integer divide by zero
goroutine 11 [running]:
runtime/debug.Stack()
/Users/bytedance/.mytools/go/src/runtime/debug/stack.go:24 +0x90
testing.tRunner.func1()
/Users/bytedance/.mytools/go/src/testing/testing.go:1349 +0x1f2
panic({0x1196b80, 0x12e3140})
/Users/bytedance/.mytools/go/src/runtime/panic.go:838 +0x207
mydev/fuzz.FuzzDiv.func1(0x0?, 0x0?, 0x0?)
/Users/bytedance/Documents/dev_test/fuzz/main_test.go:8 +0x8c
reflect.Value.call({0x11932a0?, 0x11cbf68?, 0x13?}, {0x11be123, 0x4}, {0xc000010420, 0x3, 0x4?})
/Users/bytedance/.mytools/go/src/reflect/value.go:556 +0x845
reflect.Value.Call({0x11932a0?, 0x11cbf68?, 0x514?}, {0xc000010420, 0x3, 0x4})
/Users/bytedance/.mytools/go/src/reflect/value.go:339 +0xbf
testing.(*F).Fuzz.func1.1(0x0?)
/Users/bytedance/.mytools/go/src/testing/fuzz.go:337 +0x231
testing.tRunner(0xc000003a00, 0xc00007e3f0)
/Users/bytedance/.mytools/go/src/testing/testing.go:1439 +0x102
created by testing.(*F).Fuzz.func1
/Users/bytedance/.mytools/go/src/testing/fuzz.go:324 +0x5b8
Failing input written to testdata/fuzz/FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
To re-run:
go test -run=FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
FAIL
exit status 1
FAIL mydev/fuzz 0.059s
其中的:
Failing input written to testdata/fuzz/FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
這一行表示模糊測試將出現(xiàn)panic的測試入?yún)⒈4娴搅诉@個文件里面,此時嘗試輸出這個文件的內(nèi)容:
go test fuzz v1 int(-60) int(0)
就可以看到引發(fā)panic的入?yún)?,此時我們就可以根據(jù)入?yún)z查我們的代碼是哪里有問題。當然,這個簡單的例子就是故意寫了個除0錯誤。
提供自定義語料
Golang的模糊測試還允許開發(fā)者自行提供初始語料,初始語料可以通過“f.Add”方法提供,也可以將語料以上面的“Failing input”相同的格式,寫入“testdata/fuzz/FuzzXXX/自定義語料文件名”中。
使用時的注意點
目前Golang的模糊測試僅支持被測試的函數(shù)使用這些類型的參數(shù):
[]byte, string, bool, byte, rune, float32, float64,
int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
Go的泛型
Golang在1.18中終于加入了對泛型的支持,有了泛型之后,我們可以這樣寫一些公共庫的代碼:
舊代碼(反射):
func IsContainCommon(val interface{}, array interface{}) bool { switch reflect.TypeOf(array).Kind() { case reflect.Slice: lst := reflect.ValueOf(array) for index := 0; index < lst.Len(); index++ { if reflect.DeepEqual(val, lst.Index(index).Interface()) { return true } } } return false }
新代碼(泛型):
func IsContainCommon[T any](val T, array []T) bool { for _, item := range array { if reflect.DeepEqual(val, item) { return true } } return false }
泛型在Golang中增加了三個新的重要特性:
- 在定義函數(shù)和類型時,支持使用類型參數(shù)(Type parameters)
- 將接口(interface)重新定義為“類型的集合”
- 泛型支持類型推導
下面逐個對這些內(nèi)容進行簡單說明。
類型參數(shù)(Type Parameters)
現(xiàn)在在定義函數(shù)和類型時,支持使用“類型參數(shù)”,類型參數(shù)的列表和函數(shù)參數(shù)列表很相似,只不過它使用的是方括號:
func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y }
上述的代碼中,給Min函數(shù)定義了一個參數(shù)類型T,這很類似于C++中的“template”,只不過在Golang中,可以為這種參數(shù)類型指定它需要滿足的“約束”。在這個例子中,使用的“約束”是“constraints.Ordered”。
然后就可以按照如下方式,使用這個函數(shù)了:
x := Min[int](1, 2) y := Min[float64](1.1, 2.2)
為泛型函數(shù)指定類型參數(shù)的過程叫做“實例化(Instantiation)”,也可以將實例化后的函數(shù)保存成為函數(shù)對象,并且進一步使用:
f := Min[int64] // 這一步保存了一個實例化的函數(shù)對象 n := f(123, 456)
同樣的,自定義的類型也支持泛型:
type TreeNode[T interface{}] struct { left, right *TreeNode[T] value T } func (t *TreeNode[T]) Find(x T) { ... } var myBinaryTree TreeNode[int]
如上述代碼,struct類型在使用泛型時,支持自己的成員變量和自己持有同樣的泛型類型。
類型集合(Type Sets)
下面稍微深入的講一下上述例子提到的“約束”。上文的例子中的“int”“float64”“int64”在實例化時,實際上是被作為“參數(shù)”傳遞給了“類型參數(shù)列表”,即上文例子中的“[T constraints.Ordered]”。
就像傳遞普通參數(shù)需要校驗參數(shù)的類型一樣,傳遞類型參數(shù)時也需要對被傳遞的類型參數(shù)進行校驗,檢查被傳遞的類型是否滿足要求。
例如上文例子中,使用“int”“float64”“int64”這幾個類型對Min函數(shù)進行實例化時,編譯器都會檢查這些參數(shù)是否滿足“constraints.Ordered”這個約束。而這個約束描述了所有可以使用“<”進行比較的類型的集合,這個約束本身也是一個interface。
在Go的泛型中,類型約束必須是一種interface,而“傳統(tǒng)”的Golang中對interface的定義是“一個接口定義了一組方法集合”,任何實現(xiàn)了這組方法集合的類型都實現(xiàn)了這個interface:
不過這里就出現(xiàn)了一個問題:“<”的比較顯然不是一個方法(Go當中不存在C++的運算符重載),而描述了這個約束的constraints.Ordered自身的確也是一個interface。
所以從1.18開始,Golang將Interface重新定義為“一組類型的集合”,按照以前對interface的看法,也可以將一個interface看成是“所有實現(xiàn)了這個interface的方法集合的類型所構(gòu)成的集合”:
其實兩種看法殊途同歸,但是后者顯然可以更靈活,直接將一組具體類型指定成一個interface,即使這些類型沒有任何的方法。
例如在1.18中,可以這樣定義一個interface:
type MyInterface interface { int|bool|string }
這樣的定義表示int/bool/string都可以被當作MyInterface進行使用。
那么回到constraints.Ordered,它的定義實際上是:
type Ordered interface { Integer|Float|~string } type Float interface { ~float32|~float64 } type Integer interface { Signed|Unsigned } type Signed interface { ~int|~int8|~int16|~int32|~int64 } type Unsigned interface { ~uint|~uint8|~uint16|~uint32|~uint64 }
其中前置的“~”符號表示“任何底層類型是后面所跟著的類型的類型”,例如:
type MyString string
這樣定義的MyString是可以滿足“~string”的類型約束的。
類型推導(Type Inference)
最后,所有支持泛型的語言都會有的類型推導自然也不會缺席。類型推導功能可以允許使用者在調(diào)用泛型函數(shù)時,無需指定所有的類型參數(shù)。例如下面這個函數(shù):
// 將F類型的slice變換為T類型的slice // 關(guān)鍵字 any 等同于 interface{} func Map[F, T any](src []F, f func(F) T) []T { ret := make([]T, 0, len(src)) for _, item := range src { ret = append(ret, f(item)) } return ret }
在使用時可以這樣:
var myConv := func(i int)string {return fmt.Sprint(i)} var src []int var dest []string dest = Map[int, string](src, myConv) // 明確指定F和T的類型 dest = Map[int](src, myConv) // 僅指定F的類型,T的類型交由編譯器推導 dest = Map(src, myConv) // 完全不指定類型,F(xiàn)和T都交由編譯器推導
泛型函數(shù)在使用時,可以不指定具體的類型參數(shù),也可以僅指定類型參數(shù)列表左邊的部分類型。當自動的類型推導失敗時,編譯器會報錯。
Golang泛型中的類型推導主要分為兩大部分:
- 函數(shù)參數(shù)類型推導:通過函數(shù)的入?yún)ⅲ瑢︻愋蛥?shù)對應的具體類型進行推導。
- 約束類型推導:通過已知具體類型的類型參數(shù),來推斷出未知類型參數(shù)的具體類型。
而這兩種類型推導,都依賴一種名為“類型統(tǒng)一化(Type Unification)”的技術(shù)。
類型統(tǒng)一化(Type Unification)
類型統(tǒng)一化是對兩個類型進行比較,這兩個類型有可能本身是一個類型參數(shù),也有可能包含一個類型參數(shù)。
比較的過程是對這兩個類型的“結(jié)構(gòu)”進行對比,并且要求被比較的兩個類型滿足下列條件:
- 剔除類型參數(shù)后,兩個類型的“結(jié)構(gòu)”必須能夠匹配
- 剔除類型參數(shù)后,結(jié)構(gòu)中剩余的具體類型必須相同
- 如果兩者均不含類型參數(shù),那么兩者的類型必須完全相同,或者底層數(shù)據(jù)類型完全相同
這里說的“結(jié)構(gòu)”,指的是類型定義中的slice、map、function等等,以及它們之間的任意嵌套。
滿足這幾個條件時,類型統(tǒng)一性對比才算做成功,編譯器才能進一步對類型參數(shù)進行推測,例如:
如果我們此時有“T1”、“T2”兩個類型參數(shù),那么“[]map[int]bool”可以匹配如下類型:
[]map[int]bool // 它本身 T1 // T1被推斷為 []map[int]bool []T1 // T1被推斷為 map[int]bool []map[T1]T2 // T1被推斷為 int, T2被推斷為 bool
作為反例,“[]map[int]bool”顯然無法匹配這些類型:
int struct{} []struct{} []map[T1]string // etc...
函數(shù)參數(shù)類型推導(Function Argument Type Inference)
函數(shù)參數(shù)類型推導,顧名思義是在泛型函數(shù)被調(diào)用時,如果沒有被完全指定所有的類型參數(shù),那么編譯器就會根據(jù)函數(shù)實際入?yún)⒌念愋停瑢︻愋蛥?shù)所對應的具體類型進行推導,例如本文最開始的Min函數(shù):
func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y } ans := Min(1, 2) // 此時類型參數(shù)T被推導為int
和其他支持泛型的語言一樣,Golang的函數(shù)參數(shù)類型推導只支持“能夠從入?yún)⑼茖У念愋蛥?shù)”,如果類型參數(shù)用于標記返回類型,那么在使用時必須明確指定類型參數(shù):
func MyFunc[T1, T2, T3 any](x T1) T2 { // ... var x T3 // ... } ans := MyFunc[int, bool, string](123) // 需要手動指定
類似這樣的函數(shù),部分的類型參數(shù)僅出現(xiàn)在返回值當中(或者僅出現(xiàn)在函數(shù)體中,不作為入?yún)⒒虺鰠⒊霈F(xiàn)),就無法使用函數(shù)參數(shù)類型推導,而必須明確手動指定類型。
推導算法與示例
還是拿Min函數(shù)作為例子,講解一下函數(shù)參數(shù)類型推導的過程:
func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y }
先來看看第一種情況:
Min(1, 2)
此時兩個入?yún)⒕鶠闊o類型字面值常量,所以第一輪的類型統(tǒng)一化被跳過,且入?yún)⒌木唧w類型沒有被確定,此時編譯器嘗試使用兩個參數(shù)的默認類型int,由于兩個入?yún)⒃诤瘮?shù)定義處的類型都是“T”,且兩者都使用默認類型int,所以此時T被成功推斷為int。
然后來看第二種情況:
Min(1, int64(2))
此時第二個參數(shù)有一個明確的類型int64,所以在第一輪的類型統(tǒng)一化中,T被推斷為int64,且在嘗試為第一輪漏掉的第一個參數(shù)“1”確定類型時,由于“1”是一個合法的int64類型值,所以T被成功推斷為int64。
再來看第三種情況:
Min(1.5, int64(2))
此時第二個參數(shù)有一個明確的類型int64,所以在第一輪的類型統(tǒng)一化中,T被推斷為int64,且在嘗試為第一輪漏掉的第一個參數(shù)“1.5”確定類型時,由于“1.5”不是一個合法的int64類型值,類型推導失敗,此時編譯器報錯。
最后看第四種情況:
Min(1, 2.5)
和第一種情況類似,第一輪的類型統(tǒng)一化被跳過,且兩個入?yún)⒌木唧w類型沒有被確定,此時編譯器開始嘗試使用默認類型。兩個參數(shù)的默認類型分別是int和float64,由于在類型推導中,同一個類型參數(shù)T只能被確定為一種類型,所以此時類型推導也會失敗。
約束類型推導(Constraints Type Inference)
約束類型推導是Golang泛型的另一個強大武器,它可以允許編譯器通過一個類型參數(shù)來推導另一個類型參數(shù)的具體類型,也可以通過使用類型參數(shù)來保存調(diào)用者的類型信息。
約束類型推導可以允許使用其他類型參數(shù)來為某個類型參數(shù)指定約束,這類約束被稱為“結(jié)構(gòu)化約束”,這種約束定義了類型參數(shù)必須滿足的數(shù)據(jù)結(jié)構(gòu),例如:
// 將一個整數(shù)slice中的每個元素都x2后返回 func DoubleSlice[S ~[]E, E constraints.Integer](slice S) S { ret := make(S, 0, len(slice)) for _, item := range slice { ret = append(ret, item + item) } return ret }
在這個函數(shù)的定義中,“[]E”就是一個簡寫的對S的結(jié)構(gòu)化約束,其完整寫法應是“interface{[]E}”,即以類型集合的方式來定義的interface,且其中只包含一種定義“~[]E”,意為“底層數(shù)據(jù)類型是[]E的所有類型”。
注意,一個合法的結(jié)構(gòu)化約束所對應的類型集合,應該滿足下列任意一個條件:
- 類型集合中只包含一種類型
- 類型集合中所有類型的底層數(shù)據(jù)類型均完全相同
在這個例子中,S使用的結(jié)構(gòu)化約束中,所有滿足約束的類型的底層數(shù)據(jù)類型均為[]E,所以是一個合法的結(jié)構(gòu)化約束。
當存在無法通過函數(shù)參數(shù)類型推導確定具體類型的類型參數(shù),且類型參數(shù)列表中包含結(jié)構(gòu)化約束時,編譯器會嘗試進行約束類型推導。
推導算法與示例
簡單的例子
結(jié)合我們剛才的例子“DoubleSlice”函數(shù),講一下約束類型推導的具體過程:
type MySlice []int ans := DoubleSlice(MySlice{1, 2, 3})
在這個調(diào)用中,首先執(zhí)行的是普通的函數(shù)參數(shù)類型推導,這一步會得到一個這樣的推導結(jié)果:
S => MySlice
此時編譯器發(fā)現(xiàn),還有一個類型參數(shù)E沒有被推導,且當前存在一個使用結(jié)構(gòu)化約束的類型參數(shù)S,此時開始約束類型推導。
首先需要尋找已經(jīng)完成類型推導的類型參數(shù),在這個例子里是S,它的類型已經(jīng)被推導出是MySlice。
然后會將S的實際類型“MySlice”,與S的結(jié)構(gòu)化約束“~[]E”進行類型統(tǒng)一化,由于MySlice的底層類型是[]int,所以結(jié)構(gòu)化匹配之后,得到了這樣的匹配結(jié)果:
E => int
此時所有的類型參數(shù)都已經(jīng)被推斷,且符合各自的約束,類型推導結(jié)束。
一個更復雜的例子
假設有這樣一個函數(shù):
func SomeComplicatedMethod[S ~[]M, M ~map[K]V, K comparable, V any](s S) { // comparable 是一個內(nèi)置的約束,表示所有可以使用 == != 運算符的類型 }
然后我們這樣去調(diào)用它:
SomeComplicatedMethod([]map[string]int{})
編譯時產(chǎn)生的類型推導過程如下,首先是函數(shù)參數(shù)類型推導的結(jié)果:
S => []map[string]int
然后對S使用約束類型推導,對比 []map[string]int 和 ~[]M,得到:
M => map[string]int
再繼續(xù)對M使用約束類型推導,對比 map[string]int 和 ~map[K]V,得到:
K => string V => int
至此類型推導成功完成。
使用約束類型推導保存類型信息
約束類型推導的另一個作用就是,它能夠保存調(diào)用者的原始參數(shù)的類型信息。
還是以這一節(jié)的“DoubleSlice”函數(shù)做例子,假設我們現(xiàn)在實現(xiàn)一個更加“簡單”的版本:
func DoubleSliceSimple[E constraints.Integer](slice []E) []E { ret := make([]E, 0, len(slice)) for _, item := range slice { ret = append(ret, item + item) } return ret }
這個版本只有一個類型參數(shù)E。此時我們按照之前的方式去調(diào)用它:
type MySlice []int ans := DoubleSliceSimple(MySlice{1, 2, 3}) // ans 的類型是 []int !!!
此時的類型推導僅僅是最基礎的函數(shù)參數(shù)類型推導,編譯器會對MySlice和[]E直接做結(jié)構(gòu)化比較,得出E的實際類型是int的結(jié)論。
此時DoubleSliceSimple這個函數(shù)返回的類型是[]E,也就是[]int,而不是調(diào)用者傳入的MySlice。而之前的DoubleSlice函數(shù),通過定義了一個使用結(jié)構(gòu)化約束的類型參數(shù)S,并且直接用S去匹配入?yún)⒌念愋停曳祷刂殿愋鸵彩荢,就可以保留調(diào)用者的原始參數(shù)類型。
泛型的使用局限
目前Golang泛型依然還有不少的局限,幾個主要的局限點包括:
- 成員函數(shù)無法使用泛型
- 不能使用沒在約束定義中指定的方法,即使類型集合里所有的類型都實現(xiàn)了該方法
- 不能使用成員變量,即使類型集合里所有的類型都擁有該成員
下面分別舉例:
成員函數(shù)無法使用泛型
type MyStruct[T any] struct { // ... } func (s *MyStruct[T]) Method[T2 any](param T2) { // 錯誤:成員函數(shù)無法使用泛型 // ... }
在這個例子中,MyStruct[T]的成員函數(shù)Method定義了一個只屬于自己的函數(shù)參數(shù)T2,然而這樣的操作目前是不被編譯器支持的(今后也很可能不會支持)。
無法使用約束定義之外的方法
type MyType1 struct { // ... } func (t MyType1) Method() {} type MyType2 struct { // ... } func (t MyType2) Method() {} type MyConstraint interface { MyType1 | MyType2 } func MyFunc[T MyConstraint](t T) { t.Method() // 錯誤: MyConstraint 不包含 .Method() 方法 }
這個例子中,MyConstraint集合中的兩個成員MyType1和MyType2盡管都實現(xiàn)了.Method()函數(shù),但是也無法直接在泛型函數(shù)中調(diào)用。
如果需要調(diào)用,則應該將MyConstraint改寫為如下形式:
type MyConstraint interface { MyType1 | MyType2 Method() }
無法使用成員變量
type MyType1 struct { Name string } type MyType2 struct { Name string } type MyConstraint interface { MyType1 | MyType2 } func MyFunc[T MyConstraint](t T) { fmt.Println(t.Name) // 錯誤: MyConstraint 不包含 .Name 成員 }
在這個例子當中,雖然MyType1和MyType2都包含了一個Name成員,且類型都是string,也依然無法以任何方式在泛型函數(shù)當中直接使用。
因為類型約束本身是一個interface,而interface的定義中只能包含類型集合,以及成員函數(shù)列表。
總結(jié)
Golang 1.18帶來了上述三個非常重要的新特性,其中:
- 工作區(qū)模式可以讓本地開發(fā)的工作流更加順暢。
- 模糊測試可以發(fā)現(xiàn)一些邊邊角角的情況,提升代碼的魯棒性。
- 泛型可以讓一些公共庫的代碼更加優(yōu)雅,避免像以前一樣,為了“通用性”不得不采用反射的方式,不僅寫起來難寫,讀起來難受,還增加了運行期的開銷,因為反射是運行時的動態(tài)信息,而泛型是編譯期的靜態(tài)信息。
本文也是簡單講了這幾方面的內(nèi)容,希望能讓大家對Golang中的這些新玩意兒有一個基本的了解。
參考文獻
《Get familiar with workspaces》
《Tutorial: Getting started with fuzzing》
以上就是Go 1.18新特性工作區(qū) 模糊測試 泛型的使用詳解的詳細內(nèi)容,更多關(guān)于Go 1.18 工作區(qū)模糊測試泛型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言defer與return執(zhí)行的先后順序詳解
這篇文章主要為大家介紹了Go語言defer與return執(zhí)行的先后順序詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12go-zero數(shù)據(jù)的流處理利器fx使用詳解
這篇文章主要為大家介紹了go-zero數(shù)據(jù)的流處理利器fx使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05Golang常見錯誤之值拷貝和for循環(huán)中的單一變量詳解
這篇文章主要給大家介紹了關(guān)于Golang常見錯誤之值拷貝和for循環(huán)中單一變量的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2017-11-11