前言
Go 1.18 版本之後正式引入泛型,它被稱作型別引數(type parameters),本文初步介紹 Go 中泛型的使用。長期以來 go 都沒有泛型的概念,只有介面 interface 偶爾類似的充當泛型的作用,然而介面終究無法滿足一些基本的泛型需求,比如這篇文章裡,我們會嘗試用 Go 的泛型循序漸進地實現一些常見的函式式特性,從而探索 Go 泛型的優勢和不足。
Go 1.18
在 Go1.18 可以通過如下命令安裝體驗:
go install golang.org/dl/go1.18@latest
go1.18 download
例1: 泛型版本的求和函式
import (
"golang.org/x/exp/constraints"
)
func Sum[T constraints.Integer](values ...T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}
constraints 原本是放在標準庫的包,但是近期被移除了,改到了 x/exp 中,參見 #50792
這個版本實現了對任意多個同型別的整數求和。Sum 後面的中括號 [] 內就是定義型別引數的地方,其中 T 為型別引數名,constraints.Integer 是對該型別引數的約束,即 T 應該滿足的條件,在這裡我們要求 T 是一個整數。剩下的程式碼就和普通沒有泛型的程式碼一致了,只不過後面 T 可以當作一個型別來使用。
泛型語法
-
函式名後可以附帶一個方括號,包含了該函式涉及的型別引數(Type Paramters)的列表:
func F[T any](p T) { ... }
-
這些型別引數可以在函式引數和函式體中(作為型別)被使用
-
自定義型別也可以有型別引數列表:
type M[T any] []T
-
每個型別引數對應一個型別約束,上述的 any 就是預定義的匹配任意型別的約束
-
型別約束在語法上以 interface 的形式存在,在 interface 中嵌入型別 T 可以表示這個型別必須是 T:
type Integer1 interface {
int
}
- 嵌入單個型別意義不大,我們可以用 | 來描述型別的 union:
type Integer2 interface {
int | int8 | int16 | int32 | int64
}
- ~T 語法可以表示該型別的「基礎型別」是 T,比如說我們的自定義型別 type MyInt int 不滿足上述的 Integer1 約束,但滿足以下的約束:
type Integer3 interface {
~int
}
高階函式例項
filter 操作是高階函式的經典應用,它接受一個函式 f(func (T) bool)
和一個線性表 l([] T)
,對 l 中的每個元素應用函式 f
,如結果為 true
,則將該元素加入新的線性表裡,否則丟棄該元素,最後返回新的線性表。
func Filter[T any](f func(T) bool, src []T) []T {
var dst []T
for _, v := range src {
if f(v) {
dst = append(dst, v)
}
}
return dst
}
func main() {
src := []int{-2, -1, -0, 1, 2}
dst := Filter(func(v int) bool { return v >= 0 }, src)
fmt.Println(dst)
}
// Output:
// [0 1 2]
讓人開心的改變 : )
實現一個三元操作
眾所周知Go語言不支援三元運算子操作,現在有了泛型,讓我們來模擬一個:
// IFF if yes return a else b
func IFF[T any](yes bool, a, b T) T {
if yes {
return a
}
return b
}
// IFN if yes return func, a() else b().
func IFN[T any](yes bool, a, b func() T) T {
if yes {
return a()
}
return b()
}
func main() {
a := -1
assert.Equal(t, utils.IFF(a > 0, a, 0), 0)
assert.Equal(t, utils.IFN(a > 0, func() int { return a }, func() int { return 0 }), 0)
}
令人沮喪 ?
泛型型別系統的不足
眾多函式式特性的實現依賴於一個強大型別系統,Go 的型別系統顯然不足以勝任, 在 Go 語言中引入泛型之後,型別系統有哪些水土不服的地方。
編譯期型別判斷
當我們在寫一段泛型程式碼裡的時候,有時候會需要根據 T 實際上的型別決定接下來的流程,可 Go 的完全沒有提供在編譯期操作型別的能力。執行期的 workaround 當然有,怎麼做呢:將 T 轉化為 interface{}
,然後做一次 type assertion, 比如我想實現一個通用的字串型別到數字型別的轉換函式:
import "strconv"
type Number interface {
int | int32 | int64 | uint32 | uint64 | float64
}
func Str2Number[N Number](strNumber string) (N, error) {
var num N
switch (interface{})(num).(type) {
case int:
cn, err := strconv.Atoi(strNumber)
return N(cn), err
case int32:
cn, err := strconv.ParseInt(strNumber, 10, 32)
return N(cn), err
case int64:
cn, err := strconv.ParseInt(strNumber, 10, 64)
return N(cn), err
case uint32:
cn, err := strconv.ParseUint(strNumber, 10, 32)
return N(cn), err
case uint64:
cn, err := strconv.ParseUint(strNumber, 10, 64)
return N(cn), err
case float64:
cn, err := strconv.ParseFloat(strNumber, 64)
return N(cn), err
}
return 0, nil
}
無法辨認「基礎型別」
在型別約束中可以用 ~T 的語法約束所有 基礎型別為 T 的型別,這是 Go 在語法層面上首次暴露出「基礎型別」的概念,在之前我們只能通過 reflect.(Value).Kind 獲取。而在 type assertion 和 type switch 裡並沒有對應的語法處理「基礎型別」:
type Int interface {
~int | ~uint
}
func IsSigned[T Int](n T) {
switch (interface{})(n).(type) {
case int:
fmt.Println("signed")
default:
fmt.Println("unsigned")
}
}
func main() {
type MyInt int
IsSigned(1)
IsSigned(MyInt(1))
}
// Output:
// signed
// unsigned
乍一看很合理,MyInt 確實不是 int。那我們要如何在函式不瞭解 MyInt 的情況下把它當 int 處理呢, 比較抱歉的是目前在1.18中沒辦法對這個進行處理。
型別約束不可用於 type assertion
一個直觀的想法是單獨定義一個 Signed 約束,然後判斷 T 是否滿足 Signed:
type Signed interface {
~int
}
func IsSigned[T Int](n T) {
if _, ok := (interface{})(n).(Signed); ok {
fmt.Println("signed")
} else {
fmt.Println("unsigned")
}
}
但很可惜,型別約束不能用於 type assertion/switch,編譯器報錯如下:
interface contains type constraints
儘管讓型別約束用於 type assertion 可能會引入額外的問題,但犧牲這個支援讓 Go 的型別表達能力大大地打了折扣。
總結
-
確實可以實現部分函式式特效能以更通用的方式。
-
靈活度比程式碼生成更高 ,用法更自然,但細節上的小問題很多。
-
1.18 的泛型在引入 type paramters 語法之外並沒有其他大刀闊斧的改變,導致泛型和這個語言的其他部分顯得有些格格不入,也使得泛型的能力受限。 至少在 1.18 裡,我們要忍受泛型中存在的種種不一致。
-
受制於 Go 型別系統的表達能力,我們無法表示複雜的型別約束,自然也無法實現完備的函式式特性。
推廣
推廣下個人專案,目前也正在使用Go 1.18的特性也踩了很多坑:
YoyoGo is a simple, light and fast , dependency injection based micro-service framework written in Go. Support Nacos ,Consoul ,Etcd ,Eureka ,kubernetes.