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 實驗倉庫中,你可以自行參考。