首頁 > 軟體

Go1.18新特性工作區模糊測試及泛型的使用詳解

2022-07-19 18:01:53

前言

2022年3月15日,Google釋出了萬眾矚目的Golang 1.18,帶來了好幾個重大的新特性,包括:

  • 解決本地同時開發多個倉庫帶來的一些問題的工作區(Workspace)
  • 能夠自動探測程式碼分支,隨機生成輸入,並且檢查程式碼是否會panic的模糊測試(Fuzzing Test)
  • 眾多開發者盼星星盼月亮終於等到的泛型支援。

本文將簡單講述這三個特性的相關內容。

Go工作區模式(Go Workspace Mode)

現實的情況

多倉庫同時開發

在實際的開發工作中,我們經常會同時修改存在依賴關係的多個module,例如在某個service模組上實現需求的同時,也需要對專案組的某個common模組做出修改,整個的工作流就會變成下面這樣:

可以看到,每次修改Common庫,都需要將程式碼push到遠端,然後再修改本地service倉庫的依賴,再通過go mod tidy從遠端拉取Common程式碼,不可謂不麻煩。

有些同學可能會問了,這種情況,在service倉庫的go.mod中新增一條replace不就能夠解決嗎?

但是,如果在go.mod中使用replace,在維護上需要付出額外的心智成本,萬一將帶有replace的go.mod推到遠端程式碼庫了,其他同學不就一臉懵逼了?

多個新倉庫開始開發

假設此時我正在開發兩個新的模組,分別是:

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

並且MyService依賴於Common。

在開發過程中,出於各種原因,有可能不會立即將程式碼推播到遠端,那麼此時假設我需要本地編譯MyService,就會出現go build(或者go mod tidy)自動下載依賴失敗,因為此時Common庫根本就沒有釋出到程式碼庫中。

出於和上述“多倉庫同時開發”相同的理由,replace也不應該被新增到MyService的go.mod檔案中。

工作區模式是什麼

Go工作區模式最早出現於Go開發者Michael Matloob在2021年4月提出的一個名為“Multi-Module Workspaces in cmd/go”的提案。

這個提案中提出,新增一個go.work檔案,並且在這個檔案中指定一系列的本地路徑,這些本地路徑下的go module共同構成一個工作區(workspace),go命令可以操作這些路徑下的go module,在編譯時也會優先使用這些go module。

使用如下命令就可以初始化一個工作區,並且生成一個空的go.work檔案:

go work init .

新生成的go.work檔案內容如下:

go 1.18
directory ./.

go.work檔案中,directory指示了工作區的各個module目錄,在編譯程式碼時,會優先使用同一個workspace下的module。

在go.work中,也支援使用replace來指定使用原生程式碼庫,但在大多數情況下,更好的做法是將依賴的原生程式碼庫的路徑加入directory中。

推薦的使用方法

因為go.work描述的是原生的工作區,所以也是不能提交到遠端程式碼庫的,雖然可以在.gitignore中加入這個檔案,但是最推薦的做法還是在原生程式碼庫的上層目錄使用go.work。

例如上述的“多個新倉庫開始開發”的例子,假設我的兩個倉庫的本地路徑分別是:

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

那麼我就可以在“/Users/bytedance/dev/my_new_project”目錄下生成一個如下內容的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工作區。

Go模糊測試(Go Fuzzing Test)

為什麼Golang要支援模糊測試

從1.18起,模糊測試(Fuzzing Test)作為語言安全的一環,加入了Golang的testing標準庫。Golang加入模糊測試的原因非常明顯:安全是程式設計師在構建軟體的過程中必不可少且日益重要的考量因素。

Golang至今為止,已經在保障語言安全方面提供了很多的特性和工具,例如強制使用顯式型別轉換、禁止隱式型別轉換、對陣列與切片的越界存取檢查、通過go.sum對依賴包進行雜湊校驗等等。

在進入雲原生時代之後,Golang成為了雲原生基礎設施與服務的頭部語言之一。這些系統對安全性的要求自然不言而喻。尤其是針對使用者的輸入,不被使用者的輸入弄出處理異常、崩潰、被操控是對這些系統的基本要求之一。

這就要求我們的系統在處理任何使用者輸入的時候都能保持穩定,但是傳統的質量保障手段,例如Code Review、靜態分析、人工測試、Unit Test等等,在面對日益複雜的系統時,自然就無法窮盡所有可能的輸入組合,尤其是一些非常不明顯的corner case。

而模糊測試就是業界在解決這方面問題的優秀實踐之一,Golang選擇支援它也就不難理解了。

模糊測試是什麼

模糊測試是一種通過資料構造引擎,輔以開發者可以提供的一些初始資料,自動構造出一些亂資料,作為對程式的輸入來進行測試的一種方式。模糊測試可以幫助開發人員發現難以發現的穩定性、邏輯性甚至是安全性方面的錯誤,特別是當被測系統變得更加複雜時。

模糊測試在具體的實現上,通常可以不依賴於開發測試人員定義好的資料集,取而代之的則是一組通過資料構造引擎自行構造的一系列亂資料。模糊測試會將這些資料作為輸入提供給待測程式,並且監測程式是否出現panic、斷言失敗、無限迴圈,或者其他什麼異常情況。這些通過資料構造引擎生成的資料被稱為語料(corpus) 。另外模糊測試其實也是一種持續測試的手段,因為如果不限制執行的次數或者執行的最大時間,它就會一直不停的執行下去。

Golang的模糊測試由於被實現在了編譯器工具鏈中,所以採用了一種名為“覆蓋率引導的fuzzing”的入參生成技術,大致執行過程如下:

Golang的模糊測試如何使用

Golang的模糊測試在使用時,可以簡單地直接使用,也可以自己提供一些初始的語料。

最簡單的實踐例子

模糊測試的函數也是放在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)
        })
}

可以看到類似於單元測試,模糊測試的函數名都是FuzzXxx格式,且接受一個testing.F指標物件。

然後在函數中使用f.Fuzz對指定的函數進行模糊測試,被測試的函數的第一個引數必須是“*testing.T”型別,後面可以跟任意多個基本型別的引數。

編寫完成之後,使用這樣的命令來啟動模糊測試:

go test -fuzz .

模糊測試預設會一直進行下去,只要被測試的函數不panic不出錯。可以通過“-fuzztime”選項來限制模糊測試的時間:

go test -fuzztime 10s -fuzz .

使用模糊測試對上述程式碼進行測試時,會碰到產生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

這一行表示模糊測試將出現panic的測試入參儲存到了這個檔案裡面,此時嘗試輸出這個檔案的內容:

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

就可以看到引發panic的入參,此時我們就可以根據入參檢查我們的程式碼是哪裡有問題。當然,這個簡單的例子就是故意寫了個除0錯誤。

提供自定義語料

Golang的模糊測試還允許開發者自行提供初始語料,初始語料可以通過“f.Add”方法提供,也可以將語料以上面的“Failing input”相同的格式,寫入“testdata/fuzz/FuzzXXX/自定義語料檔名”中。

使用時的注意點

目前Golang的模糊測試僅支援被測試的函數使用這些型別的引數:

[]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中增加了三個新的重要特性:

  • 在定義函數和型別時,支援使用型別引數(Type parameters)
  • 將介面(interface)重新定義為“型別的集合”
  • 泛型支援型別推導

下面逐個對這些內容進行簡單說明。

型別引數(Type Parameters)

現在在定義函數和型別時,支援使用“型別引數”,型別引數的列表和函數參數列很相似,只不過它使用的是方括號:

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

上述的程式碼中,給Min函數定義了一個引數型別T,這很類似於C++中的“template”,只不過在Golang中,可以為這種引數型別指定它需要滿足的“約束”。在這個例子中,使用的“約束”是“constraints.Ordered”。

然後就可以按照如下方式,使用這個函數了:

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

為泛型函數指定型別引數的過程叫做“範例化(Instantiation)”,也可以將範例化後的函數儲存成為函數物件,並且進一步使用:

f := Min[int64] // 這一步儲存了一個範例化的函數物件
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”在範例化時,實際上是被作為“引數”傳遞給了“型別參數列”,即上文例子中的“[T constraints.Ordered]”。

就像傳遞普通引數需要校驗引數的型別一樣,傳遞型別引數時也需要對被傳遞的型別引數進行校驗,檢查被傳遞的型別是否滿足要求。

例如上文例子中,使用“int”“float64”“int64”這幾個型別對Min函數進行範例化時,編譯器都會檢查這些引數是否滿足“constraints.Ordered”這個約束。而這個約束描述了所有可以使用“<”進行比較的型別的集合,這個約束本身也是一個interface。

在Go的泛型中,型別約束必須是一種interface,而“傳統”的Golang中對interface的定義是“一個介面定義了一組方法集合”,任何實現了這組方法集合的型別都實現了這個interface:

不過這裡就出現了一個問題:“<”的比較顯然不是一個方法(Go當中不存在C++的運運算元過載),而描述了這個約束的constraints.Ordered自身的確也是一個interface。

所以從1.18開始,Golang將Interface重新定義為“一組型別的集合”,按照以前對interface的看法,也可以將一個interface看成是“所有實現了這個interface的方法集合的型別所構成的集合”:

其實兩種看法殊途同歸,但是後者顯然可以更靈活,直接將一組具體型別指定成一個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)

最後,所有支援泛型的語言都會有的型別推導自然也不會缺席。型別推導功能可以允許使用者在呼叫泛型函數時,無需指定所有的型別引數。例如下面這個函數:

// 將F型別的slice變換為T型別的slice
// 關鍵字 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和T都交由編譯器推導

泛型函數在使用時,可以不指定具體的型別引數,也可以僅指定型別參數列左邊的部分型別。當自動的型別推導失敗時,編譯器會報錯。

Golang泛型中的型別推導主要分為兩大部分:

  • 函數引數型別推導:通過函數的入參,對型別引數對應的具體型別進行推導。
  • 約束型別推導:通過已知具體型別的型別引數,來推斷出未知型別引數的具體型別。

而這兩種型別推導,都依賴一種名為“型別統一化(Type Unification)”的技術。

型別統一化(Type Unification)

型別統一化是對兩個型別進行比較,這兩個型別有可能本身是一個型別引數,也有可能包含一個型別引數。

比較的過程是對這兩個型別的“結構”進行對比,並且要求被比較的兩個型別滿足下列條件:

  • 剔除型別引數後,兩個型別的“結構”必須能夠匹配
  • 剔除型別引數後,結構中剩餘的具體型別必須相同
  • 如果兩者均不含型別引數,那麼兩者的型別必須完全相同,或者底層資料型別完全相同

這裡說的“結構”,指的是型別定義中的slice、map、function等等,以及它們之間的任意巢狀。

滿足這幾個條件時,型別統一性對比才算做成功,編譯器才能進一步對型別引數進行推測,例如:

如果我們此時有“T1”、“T2”兩個型別引數,那麼“[]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...

函數引數型別推導(Function Argument Type Inference)

函數引數型別推導,顧名思義是在泛型函數被呼叫時,如果沒有被完全指定所有的型別引數,那麼編譯器就會根據函數實際入參的型別,對型別引數所對應的具體型別進行推導,例如本文最開始的Min函數:

func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}
ans := Min(1, 2) // 此時型別引數T被推導為int

和其他支援泛型的語言一樣,Golang的函數引數型別推導只支援“能夠從入參推導的型別引數”,如果型別引數用於標記返回型別,那麼在使用時必須明確指定型別引數:

func MyFunc[T1, T2, T3 any](x T1) T2 {
    // ...
    var x T3
    // ...
}
ans := MyFunc[int, bool, string](123) // 需要手動指定

類似這樣的函數,部分的型別引數僅出現在返回值當中(或者僅出現在函數體中,不作為入參或出參出現),就無法使用函數引數型別推導,而必須明確手動指定型別。

推導演演算法與範例

還是拿Min函數作為例子,講解一下函數引數型別推導的過程:

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

先來看看第一種情況:

Min(1, 2)

此時兩個入參均為無型別字面值常數,所以第一輪的型別統一化被跳過,且入參的具體型別沒有被確定,此時編譯器嘗試使用兩個引數的預設型別int,由於兩個入參在函數定義處的型別都是“T”,且兩者都使用預設型別int,所以此時T被成功推斷為int。

然後來看第二種情況:

Min(1, int64(2))

此時第二個引數有一個明確的型別int64,所以在第一輪的型別統一化中,T被推斷為int64,且在嘗試為第一輪漏掉的第一個引數“1”確定型別時,由於“1”是一個合法的int64型別值,所以T被成功推斷為int64。

再來看第三種情況:

Min(1.5, int64(2))

此時第二個引數有一個明確的型別int64,所以在第一輪的型別統一化中,T被推斷為int64,且在嘗試為第一輪漏掉的第一個引數“1.5”確定型別時,由於“1.5”不是一個合法的int64型別值,型別推導失敗,此時編譯器報錯。

最後看第四種情況:

Min(1, 2.5)

和第一種情況類似,第一輪的型別統一化被跳過,且兩個入參的具體型別沒有被確定,此時編譯器開始嘗試使用預設型別。兩個引數的預設型別分別是int和float64,由於在型別推導中,同一個型別引數T只能被確定為一種型別,所以此時型別推導也會失敗。

約束型別推導(Constraints Type Inference)

約束型別推導是Golang泛型的另一個強大武器,它可以允許編譯器通過一個型別引數來推導另一個型別引數的具體型別,也可以通過使用型別引數來儲存呼叫者的型別資訊。

約束型別推導可以允許使用其他型別引數來為某個型別引數指定約束,這類約束被稱為“結構化約束”,這種約束定義了型別引數必須滿足的資料結構,例如:

// 將一個整數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
}

在這個函數的定義中,“[]E”就是一個簡寫的對S的結構化約束,其完整寫法應是“interface{[]E}”,即以型別集合的方式來定義的interface,且其中只包含一種定義“~[]E”,意為“底層資料型別是[]E的所有型別”。

注意,一個合法的結構化約束所對應的型別集合,應該滿足下列任意一個條件:

  • 型別集合中只包含一種型別
  • 型別集合中所有型別的底層資料型別均完全相同

在這個例子中,S使用的結構化約束中,所有滿足約束的型別的底層資料型別均為[]E,所以是一個合法的結構化約束。

當存在無法通過函數引數型別推導確定具體型別的型別引數,且型別參數列中包含結構化約束時,編譯器會嘗試進行約束型別推導。

推導演演算法與範例

簡單的例子

結合我們剛才的例子“DoubleSlice”函數,講一下約束型別推導的具體過程:

type MySlice []int
ans := DoubleSlice(MySlice{1, 2, 3})

在這個呼叫中,首先執行的是普通的函數引數型別推導,這一步會得到一個這樣的推導結果:

S => MySlice

此時編譯器發現,還有一個型別引數E沒有被推導,且當前存在一個使用結構化約束的型別引數S,此時開始約束型別推導。

首先需要尋找已經完成型別推導的型別引數,在這個例子裡是S,它的型別已經被推匯出是MySlice。

然後會將S的實際型別“MySlice”,與S的結構化約束“~[]E”進行型別統一化,由於MySlice的底層型別是[]int,所以結構化匹配之後,得到了這樣的匹配結果:

E => int

此時所有的型別引數都已經被推斷,且符合各自的約束,型別推導結束。

一個更復雜的例子

假設有這樣一個函數:

func SomeComplicatedMethod[S ~[]M, M ~map[K]V, K comparable, V any](s S) {
    // comparable 是一個內建的約束,表示所有可以使用 == != 運運算元的型別
}

然後我們這樣去呼叫它:

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

編譯時產生的型別推導過程如下,首先是函數引數型別推導的結果:

S => []map[string]int

然後對S使用約束型別推導,對比 []map[string]int 和 ~[]M,得到:

M => map[string]int

再繼續對M使用約束型別推導,對比 map[string]int 和 ~map[K]V,得到:

K => string
V => int

至此型別推導成功完成。

使用約束型別推導儲存型別資訊

約束型別推導的另一個作用就是,它能夠儲存呼叫者的原始引數的型別資訊。

還是以這一節的“DoubleSlice”函數做例子,假設我們現在實現一個更加“簡單”的版本:

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
}

這個版本只有一個型別引數E。此時我們按照之前的方式去呼叫它:

type MySlice []int
ans := DoubleSliceSimple(MySlice{1, 2, 3}) // ans 的型別是 []int !!!

此時的型別推導僅僅是最基礎的函數引數型別推導,編譯器會對MySlice和[]E直接做結構化比較,得出E的實際型別是int的結論。

此時DoubleSliceSimple這個函數返回的型別是[]E,也就是[]int,而不是呼叫者傳入的MySlice。而之前的DoubleSlice函數,通過定義了一個使用結構化約束的型別引數S,並且直接用S去匹配入參的型別,且返回值型別也是S,就可以保留呼叫者的原始引數型別。

泛型的使用侷限

目前Golang泛型依然還有不少的侷限,幾個主要的侷限點包括:

  • 成員函數無法使用泛型
  • 不能使用沒在約束定義中指定的方法,即使型別集合裡所有的型別都實現了該方法
  • 不能使用成員變數,即使型別集合裡所有的型別都擁有該成員

下面分別舉例:

成員函數無法使用泛型

type MyStruct[T any] struct {
    // ...
}
func (s *MyStruct[T]) Method[T2 any](param T2) { // 錯誤:成員函數無法使用泛型
    // ...
}

在這個例子中,MyStruct[T]的成員函數Method定義了一個只屬於自己的函數引數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儘管都實現了.Method()函數,但是也無法直接在泛型函數中呼叫。

如果需要呼叫,則應該將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,也依然無法以任何方式在泛型函數當中直接使用。

因為型別約束本身是一個interface,而interface的定義中只能包含型別集合,以及成員函數列表。

總結

Golang 1.18帶來了上述三個非常重要的新特性,其中:

  • 工作區模式可以讓本地開發的工作流更加順暢。
  • 模糊測試可以發現一些邊邊角角的情況,提升程式碼的魯棒性。
  • 泛型可以讓一些公共庫的程式碼更加優雅,避免像以前一樣,為了“通用性”不得不採用反射的方式,不僅寫起來難寫,讀起來難受,還增加了執行期的開銷,因為反射是執行時的動態資訊,而泛型是編譯期的靜態資訊。

本文也是簡單講了這幾方面的內容,希望能讓大家對Golang中的這些新玩意兒有一個基本的瞭解。

參考文獻

《Go 1.18 is released!》

《An Introduction To Generics》

《Get familiar with workspaces》

《Tutorial: Getting started with fuzzing》

《Go 1.18新特性前瞻:原生支援Fuzzing測試》

以上就是Go 1.18新特性工作區 模糊測試 泛型的使用詳解的詳細內容,更多關於Go 1.18 工作區模糊測試泛型的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com