從一個實際例子開始
有這樣一個需求:
實現(xiàn)兩個輸入求和的能力,輸入可能是 int32、int64、float32、float64
在 Go 1.18 之前,你可能會這樣寫:
func AddInt32(a, b int32) int32 {
return a + b
}
func AddInt64(a, b int64) int64 {
return a + b
}
func AddFloat32(a, b float32) float32 {
return a + b
}
func Addfloat64(a, b float64) float64 {
return a + b
}
或者借助 reflect:
func AddByReflect(a, b interface{}) (interface{}, error) {
aValue := reflect.ValueOf(a)
bValue := reflect.ValueOf(b)
if aValue.Type() != bValue.Type() {
return nil, errors.New("invalid error")
}
switch aValue.Kind() {
case reflect.Int32, reflect.Int64:
return aValue.Int() + bValue.Int(), nil
case reflect.Float32, reflect.Float64:
return aValue.Float() + bValue.Float(), nil
default:
return nil, errors.New("invalid error")
}
}
從上面的例子可以看出,為了達到同樣的功能適配不同的參數(shù)類型的目的,我們或者會重復(fù)的制造類似的函數(shù),或者基于 interface + reflect 在運行時識別具體類型在做處理:
- 前者導(dǎo)致代碼實現(xiàn)臃腫,且不優(yōu)雅,大量的重復(fù)實現(xiàn)還會影響編譯速度
- 而后者引入的運行時開銷,則對性能有一定的沖擊。
而泛型編程則是為了解決這一問題,比較成熟的語言,如 C++(template) 、Java (generic)早已給開發(fā)者提供了相應(yīng)的能力,golang 社區(qū)也一直在致力于解決這個問題,終于在 golang 1.18 支持:
Go 1.18 includes an implementation of generic features as described by the Type Parameters Proposal.
注釋: Type Parameters Proposal 有泛型的完整闡釋,詳細的描述的golang 泛型設(shè)計過程中的一些思考與抉擇,非常推薦閱讀
上述例子在 Golang 1.18,則可以這么寫:
func AddByGeneric[T int32| int64 | float32 | float64](a , b T) T{
return a + b
}
泛型函數(shù)
上面的例子中,我們使用的方式是泛型函數(shù)(generic function),AddByGeneric 即是一個泛型函數(shù),我們先來看一些新的概念。

- T 是 type parameter,實質(zhì)上是個占位符
- int32| int64 | float32 | float64 是 type constraint,約束了 T 的類型范圍,使用時,傳入的具體類型被稱為 type argument
- 泛型函數(shù)要實例化(instantiations)后才可以使用
-
int32 | int64是一種新的語法結(jié)構(gòu),叫做 union element
【TIP】type constraint 為什么選擇使用 []?
1. () 函數(shù)入?yún)⒑头祷氐榷际菆A括號,容易搞混
2. <> 容易和 <, > 容易搞混,實現(xiàn)時還要考慮兼容,成本也較高
3. 《》非 ASCII 碼不考慮
泛型類型
除了泛型函數(shù)外,go 的泛型還支持泛型類型(generic type),再來看一個例子:
// Vector is a name for a slice of any element type.
type Vector[T any] []T
上面的例子中,Vector 即一個泛型類型,同泛型函數(shù)一樣,基于 type parameter,使用時需要傳入具體的 type argument 實例化,泛型類型也可以擁有方法(method):
type Vector[T any] []T
func (v *Vector[T]) Push(x T) {
*v = append(*v, x)
}
func (v *Vector[T]) PushList(x []T) {
*v = append(*v, x...)
}
func useVector(){
var v Vector[int64]
fmt.Println("before push:", v)
v.Push(1)
v.PushList([]int64{2,3,4})
fmt.Println("after push:", v)
}
但是,非泛型類型的的方法中不能使用 type parameter,eg:

Constraint
再看一個例子,以下使用方式在 golang 是不合法的:

在上面的代碼中,T 有可能沒有 String 方法,所以會存在問題,這是所有實現(xiàn)泛型的語言都要面對的一個問題,C++ 中「可以這么寫的」,但是會在編譯時報錯,而且為了找到這個錯誤的根因要打印非常長的調(diào)用棧,也不怎么優(yōu)雅。
Golang 沒有采用類似的機制,原因是:
- One reason is the style of the language.
- Another reason is that Go is designed to support programming at scale.
這里提現(xiàn)了 golang 在設(shè)計泛型時的原則:
This is an important rule that we believe should apply to any attempt to define generic programming in Go: generic code can only use operations that its type arguments are known to implement.
Any
上面的例子中出現(xiàn)了一個新的關(guān)鍵字 any,其實際上是空接口的別名,也就是說在在 go1.18 以后,所有使用空接口的地方都可以使用 any 替換(后面會更詳細的展開 interface 的討論)。
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}
Interface: Method Set -> Type Set
回顧上面的例子(使用 any 的例子,而 any 本質(zhì)是個空 interface ),我們會使用 interface 做作為 constraint,而 1.18 前 interface 本質(zhì)是a set of methods,即一組方法的集合,這也就限定了我們使用任意 type 只能用來實現(xiàn)方法調(diào)用,但是方法調(diào)用并不能滿足我們?nèi)康淖兂蓤鼍埃覀冞€會使用 operator,來看一個使用 operator 的例子:

為了解決以上問題,golang 引入了新概念 type set,即一組類型的集合,而 interface 的定義也悅?cè)灰恍拢?/p>
An interface type defines a type set (一個接口類型定義了一個類型集)
PS:其實從之前的定義來看(method set),也可以理解成 type set,即實現(xiàn)了這 method 的類型的集合
相應(yīng)的,我們可以這樣定義一個這樣的 interface,在泛型編程時用作 constraint:
// SignedInteger is a constraint that matches any signed integer type.
type SignedInteger interface {
int | int8 | int16 | int32 | int64
}
func Smallest[T SignedInteger](s []T) T {
r := s[0] // panic if slice is empty
for _, v := range s[1:] {
if v < r {
r = v
}
}
return r
}
新符號 ~
假設(shè)我們定義了一個類型 type MyInt64 Int64,在上述Smallest中是行不通的,因為 constraint 中只有 int64 而沒有 MyInt64:

所以,go 1.18 提供了一個新的符號~來描述所有底層都是這一基礎(chǔ)類型的所有類型,新的SignedInteger 定義如下,這時 Smallest 方法就是接受 MyInt64 型的 argument了。
type SignedInteger interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
使用上有一些需要注意的地方:
- ~ 只支持基本類型(most predeclared types)
- 不支持 type parameter 或者是 interface type
comparable 和 ordered
Golang 在 1.18 還新增加了一個內(nèi)置關(guān)鍵字comparable,用來解決使用 「==」 和「!=」 這兩種 operator 的場景
// 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只能被用作 type parameter 的 constraint,不能用來聲明變量,這里是不是和 interface 之前的用法有些矛盾呢?確實,按1.18 以前的邏輯,這里是沖突的,所以在 1.18 后,為了兼容泛型的實現(xiàn),golang 在 interface 上還有很多變化,不僅僅是 type set,下個段落我們詳細展開 interface 聊聊。
注意,comparable 是不包含 「<」 、「+」這些 operator,對于這類operator,golang 也提供了一個額外的庫(見后文)支持:
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type Integer interface {
Signed | Unsigned
}
type Float interface {
~float32 | ~float64
}
type Ordered interface {
Integer | Float | ~string
}
回過頭來再看 interface
上文我們提到 interface 變成了 type set,此外還有比較多的概念,首先是 interface 有不同的類型定義:
- Basic interfaces:這個比較好理解,即1.8 版本之前的接口,只包含 method
- Embedded interfaces:一個 interface T 中嵌入了另一個 interface E,即 interface 中包含其他 interface
type Reader interface {
Read(p []byte) (n int, err error)
Close() error
}
type Writer interface {
Write(p []byte) (n int, err error)
Close() error
}
// ReadWriter's methods are Read, Write, and Close.
type ReadWriter interface {
Reader // includes methods of Reader in ReadWriter's method set
Writer // includes methods of Writer in ReadWriter's method set
}
// 注意:嵌入接口時,同名 method 需要有相同的函數(shù)簽名,否則不合法
type ReadCloser interface {
Reader // includes methods of Reader in ReadCloser's method set
Close() // illegal: signatures of Reader.Close and Close are different
}
- General interface:既包含任意的類型 T、或者 ~T、T1|T2|T3|...,同時包含 method,要注意都是,這種接口是不能用來定義變量的,只能在泛型場景使用。
interface 的 Implementing 語義也發(fā)生了變化,當(dāng)滿足以下條件時,我們可以說 類型 T 實現(xiàn)了接口 I ( type T implements interface I):
- T 不是接口時:類型 T 是接口 I 代表的類型集中的一個成員 (T is an element of the type set of I)
- T 是接口時: T 接口代表的類型集是 I 代表的類型集的子集(Type set of T is a subset of the type set of I)
官方提供的一些泛型庫
golang.org/x/exp/constraints
Constraints that are useful for generic code, such as constraints.Ordered.golang.org/x/exp/slices
A collection of generic functions that operate on slices of any element type.golang.org/x/exp/maps
A collection of generic functions that operate on maps of any key or element type.
泛型的實現(xiàn)原理
根據(jù)Russ Cox的觀察,實現(xiàn)泛型至少要面對下面三條困境之一,那還是在2009年:
- Leave them out(slow programmers):比如C語言,增加了程序員的負擔(dān),需要曲折的實現(xiàn),但是不對增加語言的復(fù)雜性
- Compile-time specialization or macro expansion(slow compilers): 比如C++編程語言,增加了編譯器的負擔(dān),可能會產(chǎn)生很多冗余的代碼,重復(fù)的代碼還需要編譯器斟酌刪除,編譯的文件可能非常大(Rust的泛型也屬于這一類)。
- Box everything implicitly(slow execution times):比如Java,將一些裝箱成Object,進行類型擦除。雖然代碼沒啥冗余了,空間節(jié)省了,但是需要裝箱拆箱操作,代碼效率低。java 主要借助 “Type Erasure” 實現(xiàn)
在 type parameter 的提案中有提到,golang不會是 slow programmers,所以會在slow compilers 和 slow execution times 中做選擇
In other words, this design permits people to stop choosing slow programmers, and permits the implementation to decide between slow compilers (compile each set of type arguments separately) or slow execution times (use method calls for each operation on a value of a type argument).
找到一篇大佬的分析資料,golang 在1.18 中實際使用的是一種 GC Shape Stenciling 的方案,更多分析參考:https://colobu.com/2021/08/30/how-is-go-generic-implemented/
總結(jié)
- 泛型函數(shù)&泛型類型,以及 constraint
- 新符號 ~ 和 |
- 新的關(guān)鍵字 any、comparable
- 煥然一些的 Interface(type set)
- 官方庫
- 實現(xiàn)原理
參考
- 官方提案:https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
- 官方 interface 的說明:https://go.dev/ref/spec#Interface_types
- 一篇比較全面的介紹:https://segmentfault.com/a/1190000041634906
https://colobu.com/2021/08/30/how-is-go-generic-implemented/ - 分析golang 泛型實現(xiàn)原理都blog:https://colobu.com/2021/08/30/how-is-go-generic-implemented/
- 一篇 go 語言設(shè)計哲學(xué)的思考:https://golang3.eddycjy.com/posts/generics-history/