Go 泛型之泛型約束 目錄Go 泛型之泛型約束一、引入二、最寬鬆的約束:any三、支持比較操作的內置約束:comparable四、自定義約束五、類型集合(type set)六、簡化版的約束形式七、約束的類型推斷八、小結 一、引入 雖然泛型是開發人員表達“通用代碼”的一種重要方式,但這並不意味著所有泛 ...
Go 泛型之泛型約束
目錄一、引入
雖然泛型是開發人員表達“通用代碼”的一種重要方式,但這並不意味著所有泛型代碼對所有類型都適用。更多的時候,我們需要對泛型函數的類型參數以及泛型函數中的實現代碼設置限制。泛型函數調用者只能傳遞滿足限制條件的類型實參,泛型函數內部也只能以類型參數允許的方式使用這些類型實參值。在 Go 泛型語法中,我們使用類型參數約束(type parameter constraint
)(以下簡稱約束)來表達這種限制條件。
約束之於類型參數就好比函數參數列表中的類型之於參數:
函數普通參數在函數實現代碼中可以表現出來的性質與可以參與的運算由參數類型限制,而泛型函數的類型參數就由約束(constraint
)來限制。
2018 年 8 月由伊恩·泰勒和羅伯特·格瑞史莫主寫的 Go 泛型第一版設計方案中,Go 引入了 contract
關鍵字來定義泛型類型參數的約束。但經過約兩年的 Go 社區公示和討論,在 2020 年 6 月末發佈的泛型新設計方案中,Go 團隊又放棄了新引入的 contract
關鍵字,轉而採用已有的 interface
類型來替代 contract
定義約束。這一改變得到了 Go 社區的大力支持。使用 interface
類型作為約束的定義方法能夠最大程度地復用已有語法,並抑制語言引入泛型後的複雜度。
但原有的 interface
語法尚不能滿足定義約束的要求。所以,在 Go 泛型版本中,interface
語法也得到了一些擴展,也正是這些擴展給那些剛剛入門 Go 泛型的 Go 開發者帶來了一絲困惑,這也是約束被認為是 Go 泛型的一個難點的原因。
下麵我們來看一下 Go 類型參數的約束, Go 原生內置的約束、如何定義自己的約束、新引入的類型集合概念等。我們先來看一下 Go 語言的內置約束,從 Go 泛型中最寬鬆的約束:any
開始。
二、最寬鬆的約束:any
無論是泛型函數還是泛型類型,其所有類型參數聲明中都必須顯式包含約束,即便你允許類型形參接受所有類型作為類型實參傳入也是一樣。那麼我們如何表達“所有類型”這種約束呢?我們可以使用空介面類型(interface{}
)來作為類型參數的約束:
func Print[T interface{}](sl []T) {
// ... ...
}
func doSomething[T1 interface{}, T2 interface{}, T3 interface{}](t1 T1, t2 T2, t3 T3) {
// ... ...
}
不過使用 interface{}
作為約束至少有以下幾點“不足”:
- 如果存在多個這類約束時,泛型函數聲明部分會顯得很冗長,比如上面示例中的
doSomething
的聲明部分; interface{}
包含{}
這樣的符號,會讓本已經很複雜的類型參數聲明部分顯得更加複雜;- 和
comparable
、Sortable
、ordered
這樣的約束命名相比,interface{}
作為約束的表意不那麼直接。
為此,Go 團隊在 Go 1.18 泛型落地的同時又引入了一個預定義標識符:any
。any
本質上是 interface{}
的一個類型別名:
// $GOROOT/src/builtin/buildin.go
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}
這樣,我們在泛型類型參數聲明中就可以使用 any
替代 interface{}
,而上述 interface{}
作為類型參數約束的幾點“不足”也隨之被消除掉了。
any
約束的類型參數意味著可以接受所有類型作為類型實參。在函數體內,使用 any
約束的形參 T
可以用來做如下操作:
- 聲明變數
- 同類型賦值
- 將變數傳給其他函數或從函數返回
- 取變數地址
- 轉換或賦值給
interface{}
類型變數 - 用在類型斷言或 type switch 中
- 作為複合類型中的元素類型
- 傳遞給預定義的函數,比如
new
下麵是 any
約束的類型參數執行這些操作的一個示例:
// any.go
func doSomething[T1, T2 any](t1 T1, t2 T2) T1 {
var a T1 // 聲明變數
var b T2
a, b = t1, t2 // 同類型賦值
_ = b
f := func(t T1) {
}
f(a) // 傳給其他函數
p := &a // 取變數地址
_ = p
var i interface{} = a // 轉換或賦值給interface{}類型變數
_ = i
c := new(T1) // 傳遞給預定義函數
_ = c
f(a) // 將變數傳給其他函數
sl := make([]T1, 0, 10) // 作為複合類型中的元素類型
_ = sl
j, ok := i.(T1) // 用在類型斷言中
_ = ok
_ = j
switch i.(type) { // 作為type switch中的case類型
case T1:
case T2:
}
return a // 從函數返回
}
但如果對 any 約束的類型參數進行了非上述允許的操作,比如相等性或不等性比較,那麼 Go 編譯器就會報錯:
// any.go
func doSomething[T1, T2 any](t1 T1, t2 T2) T1 {
var a T1
if a == t1 { // 編譯器報錯:invalid operation: a == t1 (incomparable types in type set)
}
if a != t1 { // 編譯器報錯:invalid operation: a != t1 (incomparable types in type set)
}
... ...
}
所以說,如果我們想在泛型函數體內部對類型參數聲明的變數實施相等性(==
)或不等性比較(!=
)操作,我們就需要更換約束,這就引出了 Go 內置的另外一個預定義約束:comparable
。
三、支持比較操作的內置約束:comparable
Go 泛型提供了預定義的約束:comparable
,其定義如下:
// $GOROOT/src/builtin/buildin.go
// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, arrays of comparable types,
// structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface{ comparable }
不過從上述這行源碼我們仍然無法直觀看到 comparable
的實現細節,Go 編譯器會在編譯期間判斷某個類型是否實現了 comparable
介面。
根據其註釋說明,所有可比較的類型都實現了 comparable
這個介面,包括:布爾類型、數值類型、字元串類型、指針類型、channel
類型、元素類型實現了 comparable
的數組和成員類型均實現了 comparable
介面的結構體類型。下麵的例子可以讓我們直觀地看到這一點:
// comparable.go
type foo struct {
a int
s string
}
type bar struct {
a int
sl []string
}
func doSomething[T comparable](t T) T {
var a T
if a == t {
}
if a != t {
}
return a
}
func main() {
doSomething(true)
doSomething(3)
doSomething(3.14)
doSomething(3 + 4i)
doSomething("hello")
var p *int
doSomething(p)
doSomething(make(chan int))
doSomething([3]int{1, 2, 3})
doSomething(foo{})
doSomething(bar{}) // bar does not implement comparable
}
我們看到,最後一行 bar
結構體類型因為內含不支持比較的切片類型,被 Go 編譯器認為未實現 comparable
介面,但除此之外的其他類型作為類型實參都滿足 comparable
約束的要求。
此外還要註意,comparable
雖然也是一個 interface
,但它不能像普通 interface 類型那樣來用,比如下麵代碼會導致編譯器報錯:
var i comparable = 5 // 編譯器錯誤:cannot use type comparable outside a type constraint: interface is (or embeds) comparable
從編譯器的錯誤提示,我們看到:comparable
只能用作修飾類型參數的約束。
四、自定義約束
我們知道,Go 泛型最終決定使用 interface
語法來定義約束。這樣一來,凡是介面類型均可作為類型參數的約束。下麵是一個使用普通介面類型作為類型參數約束的示例:
// stringify.go
func Stringify[T fmt.Stringer](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
type MyString string
func (s MyString) String() string {
return string(s)
}
func main() {
sl := Stringify([]MyString{"I", "love", "golang"})
fmt.Println(sl) // 輸出:[I love golang]
}
這個例子中,我們使用的是 fmt.Stringer
介面作為約束。一方面,這要求類型參數 T
的實參必須實現 fmt.Stringer
介面的所有方法;另一方面,泛型函數 Stringify
的實現代碼中,聲明的 T
類型實例(比如 v
)也僅被允許調用 fmt.Stringer
的 String
方法。
這類基於行為(方法集合)定義的約束對於習慣了 Go 介面類型的開發者來說,是相對好理解的。定義和使用起來,與下麵這樣的以介面類型作為形參的普通 Go 函數相比,區別似乎不大:
func Stringify(s []fmt.Stringer) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
但現在我想擴展一下上面 stringify.go
這個示例,將 Stringify
的語義改為只處理非零值的元素:
// stringify_without_zero.go
func StringifyWithoutZero[T fmt.Stringer](s []T) (ret []string) {
var zero T
for _, v := range s {
if v == zero { // 編譯器報錯:invalid operation: v == zero (incomparable types in type set)
continue
}
ret = append(ret, v.String())
}
return ret
}
我們看到,針對 v
的相等性判斷導致了編譯器報錯,我們需要為類型參數賦予更多的能力,比如支持相等性和不等性比較。這讓我們想起了我們剛剛學過的 Go 內置約束 comparable
,實現 comparable
的類型,便可以支持相等性和不等性判斷操作了。
我們知道,comparable
雖然不能像普通介面類型那樣聲明變數,但它卻可以作為類型嵌入到其他介面類型中,下麵我們就擴展一下上面示例:
// stringify_new_without_zero.go
type Stringer interface {
comparable
String() string
}
func StringifyWithoutZero[T Stringer](s []T) (ret []string) {
var zero T
for _, v := range s {
if v == zero {
continue
}
ret = append(ret, v.String())
}
return ret
}
type MyString string
func (s MyString) String() string {
return string(s)
}
func main() {
sl := StringifyWithoutZero([]MyString{"I", "", "love", "", "golang"}) // 輸出:[I love golang]
fmt.Println(sl)
}
在這個示例里,我們自定義了一個 Stringer
介面類型作為約束。在該類型中,我們不僅定義了 String
方法,還嵌入了 comparable
,這樣在泛型函數中,我們用 Stringer
約束的類型參數就具備了進行相等性和不等性比較的能力了!
但我們的示例演進還沒有完,現在相等性和不等性比較已經不能滿足我們需求了,我們還要為之加上對排序行為的支持,並基於排序能力實現下麵的 StringifyLessThan
泛型函數:
func StringifyLessThan[T Stringer](s []T, max T) (ret []string) {
var zero T
for _, v := range s {
if v == zero || v >= max {
continue
}
ret = append(ret, v.String())
}
return ret
}
但現在當我們編譯上面 StringifyLessThan
函數時,我們會得到編譯器的報錯信息 invalid operation: v >= max (type parameter T is not comparable with >=)
。Go 編譯器認為 Stringer
約束的類型參數 T
不具備排序比較能力。
如果連排序比較性都無法支持,這將大大限制我們泛型函數的表達能力。但是 Go 又不支持運算符重載(operator overloading
),不允許我們定義出下麵這樣的介面類型作為類型參數的約束:
type Stringer[T any] interface {
String() string
comparable
>(t T) bool
>=(t T) bool
<(t T) bool
<=(t T) bool
}
那我們又該如何做呢?別擔心,Go 核心團隊顯然也想到了這一點,於是對 Go 介面類型聲明語法做了擴展,支持在介面類型中放入類型元素(type element
)信息,比如下麵的 ordered
介面類型:
type ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 | ~string
}
在這個介面類型的聲明中,我們沒有看到任何方法,取而代之的是一組由豎線 “|” 分隔的、帶著小尾巴 “~” 的類型列表。這個列表表示的是,以它們為底層類型(underlying type)的類型都滿足 ordered
約束,都可以作為以 ordered
為約束的類型參數的類型實參,傳入泛型函數。
我們將其組合到我們聲明的 Stringer
介面中,然後應用一下我們的 StringifyLessThan
函數:
type Stringer interface {
ordered
comparable
String() string
}
func main() {
sl := StringifyLessThan([]MyString{"I", "", "love", "", "golang"}, MyString("cpp")) // 輸出:[I]
fmt.Println(sl)
}
這回編譯器沒有報錯,並且程式輸出了預期的結果。
好了,看了那麼多例子,是時候正式對 Go 介面類型語法的擴展做一個說明瞭。下麵是擴展後的介面類型定義的組成示意圖:
我們看到,新的介面類型依然可以嵌入其他介面類型,滿足組合的設計哲學;除了嵌入的其他介面類型外,其餘的組成元素被稱為介面元素(interface element)。
介面元素也有兩類,一類就是常規的方法元素(method element),每個方法元素對應一個方法原型;另一類則是此次擴展新增的類型元素(type element),即在介面類型中,我們可以放入一些類型信息,就像前面的 ordered
介面那樣。
類型元素可以是單個類型,也可以是一組由豎線 “|” 連接的類型,豎線 “|” 的含義是“並”,這樣的一組類型被稱為 union element。無論是單個類型,還是 union element
中由 “|” 分隔的類型,如果類型中不帶有 “~” 符號的類型就代表其自身;而帶有 “~” 符號的類型則代表以該類型為底層類型(underlying type)的所有類型,這類帶有 “~” 的類型也被稱為 approximation element,如下麵示例:
type Ia interface {
int | string // 僅代表int和string
}
type Ib interface {
~int | ~string // 代表以int和string為底層類型的所有類型
}
下圖是類型元素的分解說明,供你參考:
不過要註意的是:union element
中不能包含帶有方法元素的介面類型,也不能包含預定義的約束類型,如 comparable
。
擴展後,Go 將介面類型分成了兩類,一類是基本介面類型(basic interface type
),即其自身和其嵌入的介面類型都只包含方法元素,而不包含類型元素。基本介面類型不僅可以當做常規介面類型來用,即聲明介面類型變數、介面類型變數賦值等,還可以作為泛型類型參數的約束。
除此之外的非空介面類型都屬於非基本介面類型,即直接或間接(通過嵌入其他介面類型)包含了類型元素的介面類型。這類介面類型僅可以用作泛型類型參數的約束,或被嵌入到其他僅作為約束的介面類型中,下麵的代碼就很直觀地展示了這兩種介面類型的特征:
type BasicInterface interface { // 基本介面類型
M1()
}
type NonBasicInterface interface { // 非基本介面類型
BasicInterface
~int | ~string // 包含類型元素
}
type MyString string
func (MyString) M1() {
}
func foo[T NonBasicInterface](a T) { // 非基本介面類型作為約束
}
func bar[T BasicInterface](a T) { // 基本介面類型作為約束
}
func main() {
var s = MyString("hello")
var bi BasicInterface = s // 基本介面類型支持常規用法
var nbi NonBasicInterface = s // 非基本介面不支持常規用法,導致編譯器錯誤:cannot use type NonBasicInterface outside a type constraint: interface contains type constraints
bi.M1()
nbi.M1()
foo(s)
bar(s)
}
看到這裡,你可能會覺得有問題了:基本介面類型,由於其僅包含方法元素,我們依舊可以基於之前講過的方法集合,來確定一個類型是否實現了介面,以及是否可以作為類型實參傳遞給約束下的類型形參。但對於只能作為約束的非基本介面類型,既有方法元素,也有類型元素,我們如何判斷一個類型是否滿足約束,並作為類型實參傳給類型形參呢?
這時候我們就需要 Go 泛型落地時引入的新概念:類型集合(type set),類型集合將作為後續判斷類型是否滿足約束的基本手段。
五、類型集合(type set)
類型集合(type set
)的概念是 Go 核心團隊在 2021 年 4 月更新 Go 泛型設計方案時引入的。在那一次方案變更中,原方案中用於介面類型中定義類型元素的 type
關鍵字被去除了,泛型相關語法得到了進一步的簡化。
一旦確定了一個介面類型的類型集合,類型集合中的元素就可以滿足以該介面類型作為的類型約束,也就是可以將該集合中的元素作為類型實參傳遞給該介面類型約束的類型參數。
那麼類型集合究竟是怎麼定義的呢?下麵我們來看一下。
結合 Go 泛型設計方案以及Go 語法規範,我們可以這麼來理解類型集合:
- 每個類型都有一個類型集合;
- 非介面類型的類型的類型集合中僅包含其自身,比如非介面類型
T
,它的類型集合為{T}
,即集合中僅有一個元素且這唯一的元素就是它自身。
但我們最終要搞懂的是用於定義約束的介面類型的類型集合,所以以上這兩點都是在為下麵介面類型的類型集合定義做鋪墊,定義如下:
- 空介面類型(
any
或interface{}
)的類型集合是一個無限集合,該集合中的元素為所有非介面類型。這個與我們之前的認知也是一致的,所有非介面類型都實現了空介面類型; - 非空介面類型的類型集合則是其定義中介面元素的類型集合的交集(如下圖)。
由此可見,要想確定一個介面類型的類型集合,我們需要知道其中每個介面元素的類型集合。
上面我們說過,介面元素可以是其他嵌入介面類型,可以是常規方法元素,也可以是類型元素。當介面元素為其他嵌入介面類型時,該介面元素的類型集合就為該嵌入介面類型的類型集合;而當介面元素為常規方法元素時,介面元素的類型集合就為該方法的類型集合。
到這裡你可能會很疑惑:一個方法也有自己的類型集合?
是的。Go 規定一個方法的類型集合為所有實現了該方法的非介面類型的集合,這顯然也是一個無限集合,如下圖所示:
通過方法元素的類型集合,我們也可以合理解釋僅包含多個方法的常規介面類型的類型集合,那就是這些方法元素的類型集合的交集,即所有實現了這三個方法的類型所組成的集合。
最後我們再來看看類型元素。類型元素的類型集合相對來說是最好理解的,每個類型元素的類型集合就是其表示的所有類型組成的集合。如果是 ~T 形式,則集合中不僅包含 T 本身,還包含所有以 T 為底層類型的類型。如果使用 Union element
,則類型集合是所有豎線 “|” 連接的類型的類型集合的並集。
接下來,我們來做個稍複雜些的實例分析,我們來分析一下下麵介面類型I
的類型集合:
type Intf1 interface {
~int | string
F1()
F2()
}
type Intf2 interface {
~int | ~float64
}
type I interface {
Intf1
M1()
M2()
int | ~string | Intf2
}
我們看到,介面類型 I
由四個介面元素組成,分別是 Intf1
、M1
、M2
和 Union element “int | ~string | Intf2”
,我們只要分別求出這四個元素的類型集合,再取一個交集即可。
Intf1
的類型集合
Intf1
是介面類型 I
的一個嵌入介面,它自身也是由三個介面元素組成,它的類型集合為這三個介面元素的交集,即 {以 int 為底層類型的所有類型、string、實現了 F1 和 F2 方法的所有類型}
。
- M1 和 M2 的類型集合
就像前面所說的,方法的類型集合是由所有實現該方法的類型組成的,因此 M1
的方法集合為 {實現了 M1 的所有類型}
,M2
的方法集合為 {實現了 M2 的所有類型}
。
int | ~string | Intf2
的類型集合
這是一個類型元素,它的類型集合為 int
、~string
和 Intf2
類型集合的並集。int
類型集合就是 {int}
,~string
的類型集合為 {以 string 為底層類型的所有類型}
,而 Intf2
的類型集合為 {以 int 為底層類型的所有類型,以 float64 為底層類型的所有類型}
。
為了更好地說明最終類型集合是如何取得的,我們在下麵再列一下各個介面元素的類型集合:
Intf1
的類型集合:{以int
為底層類型的所有類型、string
、實現了F1
和F2
方法的所有類型};M1
的類型集合:{實現了M1
的所有類型};M2
的類型集合:{實現了M2
的所有類型};int | ~string | Intf2
的類型集合:{以int
為底層類型的所有類型,以float64
為底層類型的所有類型,以string
為底層類型的所有類型}
。
接下來我們取一下上面集合的交集,也就是 {以 int
為底層類型的且實現了 F1
、F2
、M1
、M2
這個四個方法的所有類型}。
現在我們用代碼來驗證一下:
// typeset.go
func doSomething[T I](t T) {
}
type MyInt int
func (MyInt) F1() {
}
func (MyInt) F2() {
}
func (MyInt) M1() {
}
func (MyInt) M2() {
}
func main() {
var a int = 11
//doSomething(a) //int does not implement I (missing F1 method)
var b = MyInt(a)
doSomething(b) // ok
}
如上代碼,我們定義了一個以 int
為底層類型的自定義類型 MyInt
並實現了四個方法,這樣 MyInt
就滿足了泛型函數 doSomething
中約束 I
的要求,可以作為類型實參傳遞。
六、簡化版的約束形式
在前面的介紹和示例中,泛型參數的約束都是一個完整的介面類型,要麼是獨立定義在泛型函數外面(比如下麵代碼中的 I
介面),要麼以介面字面值的形式,直接放在類型參數列表中對類型參數進行約束,比如下麵示例中 doSomething2
類型參數列表中的介面類型字面值:
type I interface { // 獨立於泛型函數外面定義
~int | ~string
}
func doSomething1[T I](t T)
func doSomething2[T interface{~int | ~string}](t T) // 以介面類型字面值作為約束
但在約束對應的介面類型中僅有一個介面元素,且該元素為類型元素時,Go 提供了簡化版的約束形式,我們不必將約束獨立定義為一個介面類型,比如上面的 doSomething2
可以簡寫為下麵簡化形式:
func doSomething2[T ~int | ~string](t T) // 簡化版的約束形式
你看,這個簡化版的約束形式就是去掉了 interface
關鍵字和外圍的大括弧,如果用一個一般形式來表述,那就是:
func doSomething[T interface {T1 | T2 | ... | Tn}](t T)
等價於下麵簡化版的約束形式:
func doSomething[T T1 | T2 | ... | Tn](t T)
這種簡化形式也可以理解為一種類型約束的語法糖。不過有一種情況要註意,那就是定義僅包含一個類型參數的泛型類型時,如果約束中僅有一個 *int
型類型元素,我們使用上述簡化版形式就會有問題,比如:
type MyStruct [T * int]struct{} // 編譯錯誤:undefined: T
// 編譯錯誤:int (type) is not an expression
當遇到這種情況時,Go 編譯器會將該語句理解為一個類型聲明:MyStruct
為新類型的名字,而其底層類型為 [T *int]struct{}
,即一個元素為空結構體類型的數組。
那麼怎麼解決這個問題呢?目前有兩種方案,一種是用完整形式的約束:
type MyStruct[T interface{*int}] struct{}
另外一種則是在簡化版約束的 *int
類型後面加上一個逗號:
type MyStruct[T *int,] struct{}
七、約束的類型推斷
在大多數情況下,我們都可以使用類型推斷避免在調用泛型函數時顯式傳入類型實參,Go 泛型可以根據泛型函數的實參推斷出類型實參。但當我們遇到下麵示例中的泛型函數時,光依靠函數類型實參的推斷是無法完全推斷出所有類型實參的:
func DoubleDefined[S ~[]E, E constraints.Integer](s S) S {
因為像 DoubleDefined
這樣的泛型函數,其類型參數 E
在其常規參數列表中並未被用來聲明輸入參數,函數類型實參推斷僅能根據傳入的 S
的類型,推斷出類型參數 S
的類型實參,E
是無法推斷出來的。所以為了進一步避免開發者顯式傳入類型實參,Go 泛型支持了約束類型推斷(constraint type inference),即基於一個已知的類型實參(已經由函數類型實參推斷判斷出來了),來推斷其他類型參數的類型。
我們還以上面 DoubleDefined
這個泛型函數為例,當通過實參推斷得到類型 S
後,Go 會嘗試啟動約束類型推斷來推斷類型參數 E
的類型。但你可能也看出來了,約束類型推斷可成功應用的前提是 S
是由 E
所表示的。
八、小結
本文我們先從 Go 泛型內置的約束 any
和 comparable
入手,充分瞭解了約束對於泛型函數的類型參數以及泛型函數中的實現代碼的限制與影響。然後,我們瞭解瞭如何自定義約束,知道了因為 Go 不支持操作符重載,單純依賴基於行為的介面類型(僅包含方法元素)作約束是無法滿足泛型函數的要求的。這樣我們進一步學習了 Go 介面類型的擴展語法:支持類型元素。
既有方法元素,也有類型元素,對於作為約束的非基本介面類型,我們就不能像以前那樣僅憑是否實現方法集合來判斷是否實現了該介面,新的判定手段為類型集合。並且,類型集合不是一個運行時概念,我們目前還無法通過運行時反射直觀看到一個介面類型的類型集合是什麼!
Go 內置了像 any
、comparable
的約束,後續隨著 Go 核心團隊在 Go 泛型使用上的經驗的逐漸豐富,Go 標準庫中會增加更多可直接使用的約束。原計劃在 Go 1.18 版本加入 Go 標準庫的一些泛型約束的定義暫放在了 Go 實驗倉庫中,你可以自行參考。