自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go 1.18 的那些事——工作區(qū)、模糊測(cè)試、泛型

原創(chuàng) 精選
開(kāi)發(fā)
2022 年 3 月 15 日,Google 發(fā)布了萬(wàn)眾矚目的 Golang 1.18,帶來(lái)了好幾個(gè)重大的新特性。本文將簡(jiǎn)單講述這幾個(gè)新特性的相關(guān)內(nèi)容。

作者 | 張聞闐

前言

2022 年 3 月 15 日,Google 發(fā)布了萬(wàn)眾矚目的 Golang 1.18,帶來(lái)了好幾個(gè)重大的新特性,包括:

  • 解決本地同時(shí)開(kāi)發(fā)多個(gè)倉(cāng)庫(kù)帶來(lái)的一些問(wèn)題的工作區(qū)(Workspace)
  • 能夠自動(dòng)探測(cè)代碼分支,隨機(jī)生成輸入,并且檢查代碼是否會(huì) panic 的模糊測(cè)試(Fuzzing Test)
  • 眾多開(kāi)發(fā)者盼星星盼月亮終于等到的泛型支持。

本文將簡(jiǎn)單講述這三個(gè)特性的相關(guān)內(nèi)容。

Go 工作區(qū)模式(Go Workspace Mode)

現(xiàn)實(shí)的情況

多倉(cāng)庫(kù)同時(shí)開(kāi)發(fā)

在實(shí)際的開(kāi)發(fā)工作中,我們經(jīng)常會(huì)同時(shí)修改存在依賴(lài)關(guān)系的多個(gè) module,例如在某個(gè) service 模塊上實(shí)現(xiàn)需求的同時(shí),也需要對(duì)項(xiàng)目組的某個(gè) common 模塊做出修改,整個(gè)的工作流就會(huì)變成下面這樣:

圖片

可以看到,每次修改 Common 庫(kù),都需要將代碼 push 到遠(yuǎn)端,然后再修改本地 service 倉(cāng)庫(kù)的依賴(lài),再通過(guò) go mod tidy 從遠(yuǎn)端拉取 Common 代碼,不可謂不麻煩。

有些同學(xué)可能會(huì)問(wèn)了,這種情況,在 service 倉(cāng)庫(kù)的 go.mod 中添加一條 replace 不就能夠解決嗎?

但是,如果在 go.mod 中使用 replace,在維護(hù)上需要付出額外的心智成本,萬(wàn)一將帶有 replace 的 go.mod 推到遠(yuǎn)端代碼庫(kù)了,其他同學(xué)不就一臉懵逼了?

多個(gè)新倉(cāng)庫(kù)開(kāi)始開(kāi)發(fā)

假設(shè)此時(shí)我正在開(kāi)發(fā)兩個(gè)新的模塊,分別是:

code.byted.org/SomeNewProject/Common
code.byted.org/SomeNewProject/MyService

并且 MyService 依賴(lài)于 Common。

在開(kāi)發(fā)過(guò)程中,出于各種原因,有可能不會(huì)立即將代碼推送到遠(yuǎn)端,那么此時(shí)假設(shè)我需要本地編譯 MyService,就會(huì)出現(xiàn) go build(或者 go mod tidy)自動(dòng)下載依賴(lài)失敗,因?yàn)榇藭r(shí) Common 庫(kù)根本就沒(méi)有發(fā)布到代碼庫(kù)中。

出于和上述“多倉(cāng)庫(kù)同時(shí)開(kāi)發(fā)”相同的理由,replace 也不應(yīng)該被添加到 MyService 的 go.mod 文件中。

工作區(qū)模式是什么

Go 工作區(qū)模式最早出現(xiàn)于 Go 開(kāi)發(fā)者 Michael Matloob 在 2021 年 4 月提出的一個(gè)名為“Multi-Module Workspaces in cmd/go”的提案。

這個(gè)提案中提出,新增一個(gè) go.work 文件,并且在這個(gè)文件中指定一系列的本地路徑,這些本地路徑下的 go module 共同構(gòu)成一個(gè)工作區(qū)(workspace),go 命令可以操作這些路徑下的 go module,在編譯時(shí)也會(huì)優(yōu)先使用這些 go module。

使用如下命令就可以初始化一個(gè)工作區(qū),并且生成一個(gè)空的 go.work 文件:

go work init .

新生成的 go.work 文件內(nèi)容如下:

go 1.18
directory ./.

go.work 文件中,directory 指示了工作區(qū)的各個(gè) module 目錄,在編譯代碼時(shí),會(huì)優(yōu)先使用同一個(gè) workspace 下的 module。

在 go.work 中,也支持使用 replace 來(lái)指定使用本地代碼庫(kù),但在大多數(shù)情況下,更好的做法是將依賴(lài)的本地代碼庫(kù)的路徑加入 directory 中。

推薦的使用方法

因?yàn)?go.work 描述的是本地的工作區(qū),所以也是不能提交到遠(yuǎn)端代碼庫(kù)的,雖然可以在.gitignore 中加入這個(gè)文件,但是最推薦的做法還是在本地代碼庫(kù)的上層目錄使用 go.work。

例如上述的“多個(gè)新倉(cāng)庫(kù)開(kāi)始開(kāi)發(fā)”的例子,假設(shè)我的兩個(gè)倉(cāng)庫(kù)的本地路徑分別是:

/Users/bytedance/dev/my_new_project/common
/Users/bytedance/dev/my_new_project/my_service

那么我就可以在“/Users/bytedance/dev/my_new_project”目錄下生成一個(gè)如下內(nèi)容的 go.work:

/Users/bytedance/dev/my_new_project/go.work:

go 1.18

directory (
./common
./my_service
)

在上層目錄放置 go.work,也可以將多個(gè)目錄組織成一個(gè) workspace,并且由于上層目錄本身不受 git 管理,所以也不用去管 gitignore 之類(lèi)的問(wèn)題,是比較省心的方式。

使用時(shí)的注意點(diǎn)

目前(go 1.18)僅 go build 會(huì)對(duì) go.work 做出判斷,而 go mod tidy 并不 care Go 工作區(qū)。

Go 模糊測(cè)試(Go Fuzzing Test)

為什么 Golang 要支持模糊測(cè)試

從 1.18 起,模糊測(cè)試(Fuzzing Test)作為語(yǔ)言安全的一環(huán),加入了 Golang 的 testing 標(biāo)準(zhǔn)庫(kù)。Golang 加入模糊測(cè)試的原因非常明顯:安全是程序員在構(gòu)建軟件的過(guò)程中必不可少且日益重要的考量因素。

Golang 至今為止,已經(jīng)在保障語(yǔ)言安全方面提供了很多的特性和工具,例如強(qiáng)制使用顯式類(lèi)型轉(zhuǎn)換、禁止隱式類(lèi)型轉(zhuǎn)換、對(duì)數(shù)組與切片的越界訪(fǎng)問(wèn)檢查、通過(guò) go.sum 對(duì)依賴(lài)包進(jìn)行哈希校驗(yàn)等等。

在進(jìn)入云原生時(shí)代之后,Golang 成為了云原生基礎(chǔ)設(shè)施與服務(wù)的頭部語(yǔ)言之一。這些系統(tǒng)對(duì)安全性的要求自然不言而喻。尤其是針對(duì)用戶(hù)的輸入,不被用戶(hù)的輸入弄出處理異常、崩潰、被操控是對(duì)這些系統(tǒng)的基本要求之一。

這就要求我們的系統(tǒng)在處理任何用戶(hù)輸入的時(shí)候都能保持穩(wěn)定,但是傳統(tǒng)的質(zhì)量保障手段,例如 Code Review、靜態(tài)分析、人工測(cè)試、Unit Test 等等,在面對(duì)日益復(fù)雜的系統(tǒng)時(shí),自然就無(wú)法窮盡所有可能的輸入組合,尤其是一些非常不明顯的 corner case。

而模糊測(cè)試就是業(yè)界在解決這方面問(wèn)題的優(yōu)秀實(shí)踐之一,Golang 選擇支持它也就不難理解了。

模糊測(cè)試是什么

模糊測(cè)試是一種通過(guò)數(shù)據(jù)構(gòu)造引擎,輔以開(kāi)發(fā)者可以提供的一些初始數(shù)據(jù),自動(dòng)構(gòu)造出一些隨機(jī)數(shù)據(jù),作為對(duì)程序的輸入來(lái)進(jìn)行測(cè)試的一種方式。模糊測(cè)試可以幫助開(kāi)發(fā)人員發(fā)現(xiàn)難以發(fā)現(xiàn)的穩(wěn)定性、邏輯性甚至是安全性方面的錯(cuò)誤,特別是當(dāng)被測(cè)系統(tǒng)變得更加復(fù)雜時(shí)。

模糊測(cè)試在具體的實(shí)現(xiàn)上,通常可以不依賴(lài)于開(kāi)發(fā)測(cè)試人員定義好的數(shù)據(jù)集,取而代之的則是一組通過(guò)數(shù)據(jù)構(gòu)造引擎自行構(gòu)造的一系列隨機(jī)數(shù)據(jù)。模糊測(cè)試會(huì)將這些數(shù)據(jù)作為輸入提供給待測(cè)程序,并且監(jiān)測(cè)程序是否出現(xiàn) panic、斷言失敗、無(wú)限循環(huán),或者其他什么異常情況。這些通過(guò)數(shù)據(jù)構(gòu)造引擎生成的數(shù)據(jù)被稱(chēng)為語(yǔ)料(corpus)。另外模糊測(cè)試其實(shí)也是一種持續(xù)測(cè)試的手段,因?yàn)槿绻幌拗茍?zhí)行的次數(shù)或者執(zhí)行的最大時(shí)間,它就會(huì)一直不停的執(zhí)行下去。

Golang 的模糊測(cè)試由于被實(shí)現(xiàn)在了編譯器工具鏈中,所以采用了一種名為“覆蓋率引導(dǎo)的 fuzzing”的入?yún)⑸杉夹g(shù),大致運(yùn)行過(guò)程如下:

圖片

Golang 的模糊測(cè)試如何使用

Golang 的模糊測(cè)試在使用時(shí),可以簡(jiǎn)單地直接使用,也可以自己提供一些初始的語(yǔ)料。

最簡(jiǎn)單的實(shí)踐例子

模糊測(cè)試的函數(shù)也是放在 xxx_test.go 里的,編寫(xiě)一個(gè)最簡(jiǎn)單的模糊測(cè)試?yán)樱黠@的除 0 錯(cuò)誤):

package main

import "testing"
import "fmt"

func FuzzDiv(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b int) {
fmt.Println(a/b)
})
}

可以看到類(lèi)似于單元測(cè)試,模糊測(cè)試的函數(shù)名都是 FuzzXxx 格式,且接受一個(gè) testing.F 指針對(duì)象。

然后在函數(shù)中使用 f.Fuzz 對(duì)指定的函數(shù)進(jìn)行模糊測(cè)試,被測(cè)試的函數(shù)的第一個(gè)參數(shù)必須是“*testing.T”類(lèi)型,后面可以跟任意多個(gè)基本類(lèi)型的參數(shù)。

編寫(xiě)完成之后,使用這樣的命令來(lái)啟動(dòng)模糊測(cè)試:

go test -fuzz .

模糊測(cè)試默認(rèn)會(huì)一直進(jìn)行下去,只要被測(cè)試的函數(shù)不 panic 不出錯(cuò)。可以通過(guò)“-fuzztime”選項(xiàng)來(lái)限制模糊測(cè)試的時(shí)間:

go test -fuzztime 10s -fuzz .

使用模糊測(cè)試對(duì)上述代碼進(jìn)行測(cè)試時(shí),會(huì)碰到產(chǎn)生 panic 的情況,此時(shí)模糊測(cè)試會(huì)輸出如下信息:

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

這一行表示模糊測(cè)試將出現(xiàn) panic 的測(cè)試入?yún)⒈4娴搅诉@個(gè)文件里面,此時(shí)嘗試輸出這個(gè)文件的內(nèi)容:

go test fuzz v1
int(-60)
int(0)

就可以看到引發(fā) panic 的入?yún)?,此時(shí)我們就可以根據(jù)入?yún)z查我們的代碼是哪里有問(wèn)題。當(dāng)然,這個(gè)簡(jiǎn)單的例子就是故意寫(xiě)了個(gè)除 0 錯(cuò)誤。

提供自定義語(yǔ)料

Golang 的模糊測(cè)試還允許開(kāi)發(fā)者自行提供初始語(yǔ)料,初始語(yǔ)料可以通過(guò)“f.Add”方法提供,也可以將語(yǔ)料以上面的“Failing input”相同的格式,寫(xiě)入“testdata/fuzz/FuzzXXX/自定義語(yǔ)料文件名”中。

使用時(shí)的注意點(diǎn)

目前 Golang 的模糊測(cè)試僅支持被測(cè)試的函數(shù)使用這些類(lèi)型的參數(shù):

[]byte, string, bool, byte, rune, float32, float64,
int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64

根據(jù)標(biāo)準(zhǔn)庫(kù)的文檔描述,更多的類(lèi)型支持會(huì)在以后加入。

Go 的泛型

Golang 在 1.18 中終于加入了對(duì)泛型的支持,有了泛型之后,我們可以這樣寫(xiě)一些公共庫(kù)的代碼:

舊代碼(反射):

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 中增加了三個(gè)新的重要特性:

  1. 在定義函數(shù)和類(lèi)型時(shí),支持使用類(lèi)型參數(shù)(Type parameters)
  2. 將接口(interface)重新定義為“類(lèi)型的集合”
  3. 泛型支持類(lèi)型推導(dǎo)

下面逐個(gè)對(duì)這些內(nèi)容進(jìn)行簡(jiǎn)單說(shuō)明。

類(lèi)型參數(shù)(Type Parameters)

現(xiàn)在在定義函數(shù)和類(lèi)型時(shí),支持使用“類(lèi)型參數(shù)”,類(lèi)型參數(shù)的列表和函數(shù)參數(shù)列表很相似,只不過(guò)它使用的是方括號(hào):

func Min[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}

上述的代碼中,給 Min 函數(shù)定義了一個(gè)參數(shù)類(lèi)型 T,這很類(lèi)似于 C++中的“template < typename T > ”,只不過(guò)在 Golang 中,可以為這種參數(shù)類(lèi)型指定它需要滿(mǎn)足的“約束”。在這個(gè)例子中,使用的“約束”是“constraints.Ordered”。

然后就可以按照如下方式,使用這個(gè)函數(shù)了:

x := Min[int](1, 2)
y := Min[float64](1.1, 2.2)

為泛型函數(shù)指定類(lèi)型參數(shù)的過(guò)程叫做“實(shí)例化(Instantiation)”,也可以將實(shí)例化后的函數(shù)保存成為函數(shù)對(duì)象,并且進(jìn)一步使用:

f := Min[int64] // 這一步保存了一個(gè)實(shí)例化的函數(shù)對(duì)象
n := f(123, 456)

同樣的,自定義的類(lèi)型也支持泛型:

type TreeNode[T interface{}] struct {
left, right *TreeNode[T]
value T
}

func (t *TreeNode[T]) Find(x T) { ... }

var myBinaryTree TreeNode[int]

如上述代碼,struct 類(lèi)型在使用泛型時(shí),支持自己的成員變量和自己持有同樣的泛型類(lèi)型。

類(lèi)型集合(Type Sets)

下面稍微深入的講一下上述例子提到的“約束”。上文的例子中的“int”“float64”“int64”在實(shí)例化時(shí),實(shí)際上是被作為“參數(shù)”傳遞給了“類(lèi)型參數(shù)列表”,即上文例子中的“[T constraints.Ordered]”。

就像傳遞普通參數(shù)需要校驗(yàn)參數(shù)的類(lèi)型一樣,傳遞類(lèi)型參數(shù)時(shí)也需要對(duì)被傳遞的類(lèi)型參數(shù)進(jìn)行校驗(yàn),檢查被傳遞的類(lèi)型是否滿(mǎn)足要求。

例如上文例子中,使用“int”“float64”“int64”這幾個(gè)類(lèi)型對(duì) Min 函數(shù)進(jìn)行實(shí)例化時(shí),編譯器都會(huì)檢查這些參數(shù)是否滿(mǎn)足“constraints.Ordered”這個(gè)約束。而這個(gè)約束描述了所有可以使用“<”進(jìn)行比較的類(lèi)型的集合,這個(gè)約束本身也是一個(gè) interface。

在 Go 的泛型中,類(lèi)型約束必須是一種 interface,而“傳統(tǒng)”的 Golang 中對(duì) interface 的定義是“一個(gè)接口定義了一組方法集合”,任何實(shí)現(xiàn)了這組方法集合的類(lèi)型都實(shí)現(xiàn)了這個(gè) interface:

圖片

不過(guò)這里就出現(xiàn)了一個(gè)問(wèn)題:“<”的比較顯然不是一個(gè)方法(Go 當(dāng)中不存在 C++的運(yùn)算符重載),而描述了這個(gè)約束的 constraints.Ordered 自身的確也是一個(gè) interface。

所以從 1.18 開(kāi)始,Golang 將 Interface 重新定義為“一組類(lèi)型的集合”,按照以前對(duì) interface 的看法,也可以將一個(gè) interface 看成是“所有實(shí)現(xiàn)了這個(gè) interface 的方法集合的類(lèi)型所構(gòu)成的集合”:

圖片

其實(shí)兩種看法殊途同歸,但是后者顯然可以更靈活,直接將一組具體類(lèi)型指定成一個(gè) interface,即使這些類(lèi)型沒(méi)有任何的方法。

例如在 1.18 中,可以這樣定義一個(gè) interface:

type MyInterface interface {
int|bool|string
}

這樣的定義表示 int/bool/string 都可以被當(dāng)作 MyInterface 進(jìn)行使用。

那么回到 constraints.Ordered,它的定義實(shí)際上是:

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
}

其中前置的“~”符號(hào)表示“任何底層類(lèi)型是后面所跟著的類(lèi)型的類(lèi)型”,例如:

type MyString string

這樣定義的 MyString 是可以滿(mǎn)足“~string”的類(lèi)型約束的。

類(lèi)型推導(dǎo)(Type Inference)

最后,所有支持泛型的語(yǔ)言都會(huì)有的類(lèi)型推導(dǎo)自然也不會(huì)缺席。類(lèi)型推導(dǎo)功能可以允許使用者在調(diào)用泛型函數(shù)時(shí),無(wú)需指定所有的類(lèi)型參數(shù)。例如下面這個(gè)函數(shù):

// 將F類(lèi)型的slice變換為T(mén)類(lèi)型的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
}

在使用時(shí)可以這樣:

var myConv := func(i int)string {return fmt.Sprint(i)}
var src []int
var dest []string
dest = Map[int, string](src, myConv) // 明確指定F和T的類(lèi)型
dest = Map[int](src, myConv) // 僅指定F的類(lèi)型,T的類(lèi)型交由編譯器推導(dǎo)
dest = Map(src, myConv) // 完全不指定類(lèi)型,F(xiàn)和T都交由編譯器推導(dǎo)

泛型函數(shù)在使用時(shí),可以不指定具體的類(lèi)型參數(shù),也可以?xún)H指定類(lèi)型參數(shù)列表左邊的部分類(lèi)型。當(dāng)自動(dòng)的類(lèi)型推導(dǎo)失敗時(shí),編譯器會(huì)報(bào)錯(cuò)。

Golang 泛型中的類(lèi)型推導(dǎo)主要分為兩大部分:

  1. 函數(shù)參數(shù)類(lèi)型推導(dǎo):通過(guò)函數(shù)的入?yún)?,?duì)類(lèi)型參數(shù)對(duì)應(yīng)的具體類(lèi)型進(jìn)行推導(dǎo)。
  2. 約束類(lèi)型推導(dǎo):通過(guò)已知具體類(lèi)型的類(lèi)型參數(shù),來(lái)推斷出未知類(lèi)型參數(shù)的具體類(lèi)型。

而這兩種類(lèi)型推導(dǎo),都依賴(lài)一種名為“類(lèi)型統(tǒng)一化(Type Unification)”的技術(shù)。

類(lèi)型統(tǒng)一化(Type Unification)

類(lèi)型統(tǒng)一化是對(duì)兩個(gè)類(lèi)型進(jìn)行比較,這兩個(gè)類(lèi)型有可能本身是一個(gè)類(lèi)型參數(shù),也有可能包含一個(gè)類(lèi)型參數(shù)。

比較的過(guò)程是對(duì)這兩個(gè)類(lèi)型的“結(jié)構(gòu)”進(jìn)行對(duì)比,并且要求被比較的兩個(gè)類(lèi)型滿(mǎn)足下列條件:

  1. 剔除類(lèi)型參數(shù)后,兩個(gè)類(lèi)型的“結(jié)構(gòu)”必須能夠匹配
  2. 剔除類(lèi)型參數(shù)后,結(jié)構(gòu)中剩余的具體類(lèi)型必須相同
  3. 如果兩者均不含類(lèi)型參數(shù),那么兩者的類(lèi)型必須完全相同,或者底層數(shù)據(jù)類(lèi)型完全相同

這里說(shuō)的“結(jié)構(gòu)”,指的是類(lèi)型定義中的 slice、map、function 等等,以及它們之間的任意嵌套。

滿(mǎn)足這幾個(gè)條件時(shí),類(lèi)型統(tǒng)一性對(duì)比才算做成功,編譯器才能進(jìn)一步對(duì)類(lèi)型參數(shù)進(jìn)行推測(cè),例如:

如果我們此時(shí)有“T1”、“T2”兩個(gè)類(lèi)型參數(shù),那么“[]map[int]bool”可以匹配如下類(lèi)型:

[]map[int]bool // 它本身
T1 // T1被推斷為 []map[int]bool
[]T1 // T1被推斷為 map[int]bool
[]map[T1]T2 // T1被推斷為 int, T2被推斷為 bool

圖片

作為反例,“[]map[int]bool”顯然無(wú)法匹配這些類(lèi)型:

int
struct{}
[]struct{}
[]map[T1]string
// etc...

函數(shù)參數(shù)類(lèi)型推導(dǎo)(Function Argument Type Inference)

函數(shù)參數(shù)類(lèi)型推導(dǎo),顧名思義是在泛型函數(shù)被調(diào)用時(shí),如果沒(méi)有被完全指定所有的類(lèi)型參數(shù),那么編譯器就會(huì)根據(jù)函數(shù)實(shí)際入?yún)⒌念?lèi)型,對(duì)類(lèi)型參數(shù)所對(duì)應(yīng)的具體類(lèi)型進(jìn)行推導(dǎo),例如本文最開(kāi)始的 Min 函數(shù):

func Min[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}

ans := Min(1, 2) // 此時(shí)類(lèi)型參數(shù)T被推導(dǎo)為int

和其他支持泛型的語(yǔ)言一樣,Golang 的函數(shù)參數(shù)類(lèi)型推導(dǎo)只支持“能夠從入?yún)⑼茖?dǎo)的類(lèi)型參數(shù)”,如果類(lèi)型參數(shù)用于標(biāo)記返回類(lèi)型,那么在使用時(shí)必須明確指定類(lèi)型參數(shù):

func MyFunc[T1, T2, T3 any](x T1) T2 {
// ...
var x T3
// ...
}

ans := MyFunc[int, bool, string](123) // 需要手動(dòng)指定

類(lèi)似這樣的函數(shù),部分的類(lèi)型參數(shù)僅出現(xiàn)在返回值當(dāng)中(或者僅出現(xiàn)在函數(shù)體中,不作為入?yún)⒒虺鰠⒊霈F(xiàn)),就無(wú)法使用函數(shù)參數(shù)類(lèi)型推導(dǎo),而必須明確手動(dòng)指定類(lèi)型。

推導(dǎo)算法與示例

圖片

還是拿 Min 函數(shù)作為例子,講解一下函數(shù)參數(shù)類(lèi)型推導(dǎo)的過(guò)程:

func Min[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}

先來(lái)看看第一種情況:

Min(1, 2)

此時(shí)兩個(gè)入?yún)⒕鶠闊o(wú)類(lèi)型字面值常量,所以第一輪的類(lèi)型統(tǒng)一化被跳過(guò),且入?yún)⒌木唧w類(lèi)型沒(méi)有被確定,此時(shí)編譯器嘗試使用兩個(gè)參數(shù)的默認(rèn)類(lèi)型 int,由于兩個(gè)入?yún)⒃诤瘮?shù)定義處的類(lèi)型都是“T”,且兩者都使用默認(rèn)類(lèi)型 int,所以此時(shí) T 被成功推斷為 int。

然后來(lái)看第二種情況:

Min(1, int64(2))

此時(shí)第二個(gè)參數(shù)有一個(gè)明確的類(lèi)型 int64,所以在第一輪的類(lèi)型統(tǒng)一化中,T 被推斷為 int64,且在嘗試為第一輪漏掉的第一個(gè)參數(shù)“1”確定類(lèi)型時(shí),由于“1”是一個(gè)合法的 int64 類(lèi)型值,所以 T 被成功推斷為 int64。

再來(lái)看第三種情況:

Min(1.5, int64(2))

此時(shí)第二個(gè)參數(shù)有一個(gè)明確的類(lèi)型 int64,所以在第一輪的類(lèi)型統(tǒng)一化中,T 被推斷為 int64,且在嘗試為第一輪漏掉的第一個(gè)參數(shù)“1.5”確定類(lèi)型時(shí),由于“1.5”不是一個(gè)合法的 int64 類(lèi)型值,類(lèi)型推導(dǎo)失敗,此時(shí)編譯器報(bào)錯(cuò)。

最后看第四種情況:

Min(1, 2.5)

和第一種情況類(lèi)似,第一輪的類(lèi)型統(tǒng)一化被跳過(guò),且兩個(gè)入?yún)⒌木唧w類(lèi)型沒(méi)有被確定,此時(shí)編譯器開(kāi)始嘗試使用默認(rèn)類(lèi)型。兩個(gè)參數(shù)的默認(rèn)類(lèi)型分別是 int 和 float64,由于在類(lèi)型推導(dǎo)中,同一個(gè)類(lèi)型參數(shù) T 只能被確定為一種類(lèi)型,所以此時(shí)類(lèi)型推導(dǎo)也會(huì)失敗。

約束類(lèi)型推導(dǎo)(Constraints Type Inference)

約束類(lèi)型推導(dǎo)是 Golang 泛型的另一個(gè)強(qiáng)大武器,它可以允許編譯器通過(guò)一個(gè)類(lèi)型參數(shù)來(lái)推導(dǎo)另一個(gè)類(lèi)型參數(shù)的具體類(lèi)型,也可以通過(guò)使用類(lèi)型參數(shù)來(lái)保存調(diào)用者的類(lèi)型信息。

約束類(lèi)型推導(dǎo)可以允許使用其他類(lèi)型參數(shù)來(lái)為某個(gè)類(lèi)型參數(shù)指定約束,這類(lèi)約束被稱(chēng)為“結(jié)構(gòu)化約束”,這種約束定義了類(lèi)型參數(shù)必須滿(mǎn)足的數(shù)據(jù)結(jié)構(gòu),例如:

// 將一個(gè)整數(shù)slice中的每個(gè)元素都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
}

在這個(gè)函數(shù)的定義中,“~[]E”就是一個(gè)簡(jiǎn)寫(xiě)的對(duì) S 的結(jié)構(gòu)化約束,其完整寫(xiě)法應(yīng)是“interface{~[]E}”,即以類(lèi)型集合的方式來(lái)定義的 interface,且其中只包含一種定義“~[]E”,意為“底層數(shù)據(jù)類(lèi)型是[]E 的所有類(lèi)型”。

注意,一個(gè)合法的結(jié)構(gòu)化約束所對(duì)應(yīng)的類(lèi)型集合,應(yīng)該滿(mǎn)足下列任意一個(gè)條件:

  1. 類(lèi)型集合中只包含一種類(lèi)型
  2. 類(lèi)型集合中所有類(lèi)型的底層數(shù)據(jù)類(lèi)型均完全相同

在這個(gè)例子中,S 使用的結(jié)構(gòu)化約束中,所有滿(mǎn)足約束的類(lèi)型的底層數(shù)據(jù)類(lèi)型均為[]E,所以是一個(gè)合法的結(jié)構(gòu)化約束。

當(dāng)存在無(wú)法通過(guò)函數(shù)參數(shù)類(lèi)型推導(dǎo)確定具體類(lèi)型的類(lèi)型參數(shù),且類(lèi)型參數(shù)列表中包含結(jié)構(gòu)化約束時(shí),編譯器會(huì)嘗試進(jìn)行約束類(lèi)型推導(dǎo)。

推導(dǎo)算法與示例

圖片

簡(jiǎn)單的例子

結(jié)合我們剛才的例子“DoubleSlice”函數(shù),講一下約束類(lèi)型推導(dǎo)的具體過(guò)程:

type MySlice []int

ans := DoubleSlice(MySlice{1, 2, 3})

在這個(gè)調(diào)用中,首先執(zhí)行的是普通的函數(shù)參數(shù)類(lèi)型推導(dǎo),這一步會(huì)得到一個(gè)這樣的推導(dǎo)結(jié)果:

S => MySlice

此時(shí)編譯器發(fā)現(xiàn),還有一個(gè)類(lèi)型參數(shù) E 沒(méi)有被推導(dǎo),且當(dāng)前存在一個(gè)使用結(jié)構(gòu)化約束的類(lèi)型參數(shù) S,此時(shí)開(kāi)始約束類(lèi)型推導(dǎo)。

首先需要尋找已經(jīng)完成類(lèi)型推導(dǎo)的類(lèi)型參數(shù),在這個(gè)例子里是 S,它的類(lèi)型已經(jīng)被推導(dǎo)出是 MySlice。

然后會(huì)將 S 的實(shí)際類(lèi)型“MySlice”,與 S 的結(jié)構(gòu)化約束“~[]E”進(jìn)行類(lèi)型統(tǒng)一化,由于 MySlice 的底層類(lèi)型是[]int,所以結(jié)構(gòu)化匹配之后,得到了這樣的匹配結(jié)果:

E => int

此時(shí)所有的類(lèi)型參數(shù)都已經(jīng)被推斷,且符合各自的約束,類(lèi)型推導(dǎo)結(jié)束。

一個(gè)更復(fù)雜的例子

假設(shè)有這樣一個(gè)函數(shù):

func SomeComplicatedMethod[S ~[]M, M ~map[K]V, K comparable, V any](s S) {
// comparable 是一個(gè)內(nèi)置的約束,表示所有可以使用 == != 運(yùn)算符的類(lèi)型
}

然后我們這樣去調(diào)用它:

SomeComplicatedMethod([]map[string]int{})

編譯時(shí)產(chǎn)生的類(lèi)型推導(dǎo)過(guò)程如下,首先是函數(shù)參數(shù)類(lèi)型推導(dǎo)的結(jié)果:

S => []map[string]int

然后對(duì) S 使用約束類(lèi)型推導(dǎo),對(duì)比 []map[string]int 和 ~[]M,得到:

M => map[string]int

再繼續(xù)對(duì) M 使用約束類(lèi)型推導(dǎo),對(duì)比 map[string]int 和 ~map[K]V,得到:

K => string
V => int

至此類(lèi)型推導(dǎo)成功完成。

使用約束類(lèi)型推導(dǎo)保存類(lèi)型信息

約束類(lèi)型推導(dǎo)的另一個(gè)作用就是,它能夠保存調(diào)用者的原始參數(shù)的類(lèi)型信息。

還是以這一節(jié)的“DoubleSlice”函數(shù)做例子,假設(shè)我們現(xiàn)在實(shí)現(xiàn)一個(gè)更加“簡(jiǎ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
}

這個(gè)版本只有一個(gè)類(lèi)型參數(shù) E。此時(shí)我們按照之前的方式去調(diào)用它:

type MySlice []int

ans := DoubleSliceSimple(MySlice{1, 2, 3}) // ans 的類(lèi)型是 []int !!!

此時(shí)的類(lèi)型推導(dǎo)僅僅是最基礎(chǔ)的函數(shù)參數(shù)類(lèi)型推導(dǎo),編譯器會(huì)對(duì) MySlice 和[]E 直接做結(jié)構(gòu)化比較,得出 E 的實(shí)際類(lèi)型是 int 的結(jié)論。

此時(shí) DoubleSliceSimple 這個(gè)函數(shù)返回的類(lèi)型是[]E,也就是[]int,而不是調(diào)用者傳入的 MySlice。而之前的 DoubleSlice 函數(shù),通過(guò)定義了一個(gè)使用結(jié)構(gòu)化約束的類(lèi)型參數(shù) S,并且直接用 S 去匹配入?yún)⒌念?lèi)型,且返回值類(lèi)型也是 S,就可以保留調(diào)用者的原始參數(shù)類(lèi)型。

泛型的使用局限

目前 Golang 泛型依然還有不少的局限,幾個(gè)主要的局限點(diǎn)包括:

  1. 成員函數(shù)無(wú)法使用泛型
  2. 不能使用沒(méi)在約束定義中指定的方法,即使類(lèi)型集合里所有的類(lèi)型都實(shí)現(xiàn)了該方法
  3. 不能使用成員變量,即使類(lèi)型集合里所有的類(lèi)型都擁有該成員

下面分別舉例:

成員函數(shù)無(wú)法使用泛型

type MyStruct[T any] struct {
// ...
}

func (s *MyStruct[T]) Method[T2 any](param T2) { // 錯(cuò)誤:成員函數(shù)無(wú)法使用泛型
// ...
}

在這個(gè)例子中,MyStruct[T]的成員函數(shù) Method 定義了一個(gè)只屬于自己的函數(shù)參數(shù) T2,然而這樣的操作目前是不被編譯器支持的(今后也很可能不會(huì)支持)。

無(wú)法使用約束定義之外的方法

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() // 錯(cuò)誤:MyConstraint 不包含 .Method() 方法
}

這個(gè)例子中,MyConstraint 集合中的兩個(gè)成員 MyType1 和 MyType2 盡管都實(shí)現(xiàn)了.Method()函數(shù),但是也無(wú)法直接在泛型函數(shù)中調(diào)用。

如果需要調(diào)用,則應(yīng)該將 MyConstraint 改寫(xiě)為如下形式:

type MyConstraint interface {
MyType1 | MyType2
Method()
}

無(wú)法使用成員變量

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) // 錯(cuò)誤:MyConstraint 不包含 .Name 成員
}

在這個(gè)例子當(dāng)中,雖然 MyType1 和 MyType2 都包含了一個(gè) Name 成員,且類(lèi)型都是 string,也依然無(wú)法以任何方式在泛型函數(shù)當(dāng)中直接使用。

因?yàn)轭?lèi)型約束本身是一個(gè) interface,而 interface 的定義中只能包含類(lèi)型集合,以及成員函數(shù)列表。

總結(jié)

Golang 1.18 帶來(lái)了上述三個(gè)非常重要的新特性,其中:

  1. 工作區(qū)模式可以讓本地開(kāi)發(fā)的工作流更加順暢。
  2. 模糊測(cè)試可以發(fā)現(xiàn)一些邊邊角角的情況,提升代碼的魯棒性。
  3. 泛型可以讓一些公共庫(kù)的代碼更加優(yōu)雅,避免像以前一樣,為了“通用性”不得不采用反射的方式,不僅寫(xiě)起來(lái)難寫(xiě),讀起來(lái)難受,還增加了運(yùn)行期的開(kāi)銷(xiāo),因?yàn)榉瓷涫沁\(yùn)行時(shí)的動(dòng)態(tài)信息,而泛型是編譯期的靜態(tài)信息。

本文也是簡(jiǎn)單講了這幾方面的內(nèi)容,希望能讓大家對(duì) Golang 中的這些新玩意兒有一個(gè)基本的了解。

參考文獻(xiàn)

  1. https://go.dev/blog/go1.18
  2. https://go.dev/blog/intro-generics
  3. https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
  4. https://go.dev/blog/get-familiar-with-workspaces
  5. https://go.dev/doc/tutorial/fuzz
  6. https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18/?
責(zé)任編輯:未麗燕 來(lái)源: 字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2021-12-15 10:23:56

Go 1.18 Bet語(yǔ)言泛型

2021-11-01 12:41:39

Go

2021-12-28 07:20:44

泛型Go場(chǎng)景

2021-10-29 10:55:07

Go 泛型語(yǔ)言

2022-01-19 08:51:00

Module工作區(qū)Go

2021-10-18 10:53:26

Go 代碼技術(shù)

2021-07-29 09:20:18

Java泛型String

2021-12-15 12:59:56

Go泛型版Beta1

2021-09-29 18:17:30

Go泛型語(yǔ)言

2022-12-14 23:05:29

Go模糊測(cè)試

2022-11-27 23:37:34

Go模式Workspaces

2022-01-10 11:33:17

Go測(cè)試軟件

2011-05-19 16:47:50

軟件測(cè)試

2022-03-29 11:48:40

Go泛型測(cè)試

2021-12-30 19:34:15

Java泛型JDK

2023-11-29 08:19:45

Go泛型缺陷

2024-10-28 00:40:49

Go語(yǔ)法版本

2023-08-09 08:53:50

GoWASI語(yǔ)義

2023-09-26 01:21:34

2022-04-15 09:55:59

Go 泛型Go 程序函數(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)