泛型
泛型,或者更學術化的名稱 —— 參數化多態(Parameterized Polymorphism),是指通過類型參數化來實現代碼的復用與靈活性。在許多編程語言中,參數化多態是一個重要的概念,它使得函數或數據結構可以處理不同類型的數據,而無需為每種類型編寫單獨的代碼。最初的 Go 是沒有泛型這一說法的,但自從誕生以來,社區關於 Go 呼聲最高的事情就是希望加入泛型,終於 Go 語言於 2022 年在 1.18 版本加入了對泛型的支持。
設計
Go 語言當初在設計泛型時,考慮過了以下的方案
- stenciling :單態化,比較典型的像 C++,Rust 這種,為每一個用到的類型都生成一份模板代碼,這種的性能是最好的,完全沒有任何運行時開銷,性能等於直接調用,缺點就是大幅度拖慢編譯速度(相較於 Go 自己而言),同時由於為每一個類型生成了代碼,也會導致編譯的二進制文件體積膨脹。
- dictionaries :它只會生成一套代碼,同時在編譯時生成一個類型字典存儲在只讀數據段,它存儲了所有會用到的類型信息,在調用函數時,則會根據字典去查詢類型信息。這種方式不會拖慢編譯速度,也不會造成體積膨脹,但是會造成巨大的運行時開銷,泛型的性能很差。
上面兩個方法代表著兩種極端,Go 語言最終選擇的實現方案是 Gcshape stenciling ,它是一個折中的選擇,對於同種內存形狀(形狀怎麼看由內存分配器決定)的類型會使用單態化,為其生成同一份代碼,比如 type Int int 與 int 其實是同一個類型,所以共用一套代碼。但是對於指針而言,雖然所有的指針類型都是同一個內存形狀,比如 *int, *Person 都是一個內存形狀,但是它們是無法共用一套代碼的,因為解引用時操作的目標類型內存布局完全不同,為此,Go 同時也會使用字典在運行時獲取類型信息,所以 Go 的泛型也存在運行時開銷。
引子
先來看一個簡單的例子。
func Sum(a, b int) int {
return a + b
}這是一個功能十分簡單的函數,作用就是將兩個 int 類型的整數相加並返回結果,倘若想要傳入兩個 float64 類型的浮點數求和的話,顯然是不可以的,因為類型不匹配。一種解決辦法就是再定義一個新的函數,如下
func SumFloat64(a, b float64) float64 {
return a + b
}那麼問題來了,如果開發一個數學工具包,計算所有數字類型的兩數之和,難道要每一個類型都要編寫一個函數嗎?顯然是不太可能的,或者可以使用 any 類型加反射來判斷,如下
func SumAny(a, b any) (any, error) {
tA, tB := reflect.ValueOf(a), reflect.ValueOf(b)
if tA.Kind() != tB.Kind() {
return nil, errors.New("disMatch type")
}
switch tA.Kind() {
case reflect.Int:
case reflect.Int32:
...
}
}但是這樣寫很繁瑣,而且性能低下。但是 Sum 函數的邏輯都是一模一樣的,都只不過是將兩個數相加而已,這時候就需要用到了泛型,所以為什麼需要泛型, 泛型是為了解決執行邏輯與類型無關的問題,這類問題不關心給出的類型是什麼,只需要完成對應的操作就足夠。
語法
泛型的寫法如下
func Sum[T int | float64](a, b T) T {
return a + b
}類型形參:T 就是一個類型形參,形參具體是什麼類型取決於傳進來什麼類型
類型約束:int | float64 構成了一個類型約束,這個類型約束內規定了哪些類型是允許的,約束了類型形參的類型范圍
類型實參:Sum[int](1,2),手動指定了 int 類型,int 就是類型實參。
第一種用法,顯式的指明使用哪種類型,如下
Sum[int](2012, 2022)第二種用法,不指定類型,讓編譯器自行推斷,如下
Sum(3.1415926, 1.114514)這是一個泛型切片,類型約束為 int | int32 | int64
type GenericSlice[T int | int32 | int64] []T這裡使用時就不能省略掉類型實參
GenericSlice[int]{1, 2, 3}這是一個泛型哈希表,鍵的類型必須是可比較的,所以使用 comparable 接口,值的類型約束為 V int | string | byte
type GenericMap[K comparable, V int | string | byte] map[K]V使用
gmap1 := GenericMap[int, string]{1: "hello world"}
gmap2 := make(GenericMap[string, byte], 0)這是一個泛型結構體,類型約束為 T int | string
type GenericStruct[T int | string] struct {
Name string
Id T
}使用
GenericStruct[int]{
Name: "jack",
Id: 1024,
}
GenericStruct[string]{
Name: "Mike",
Id: "1024",
}這是一個泛型切片形參的例子
type Company[T int | string, S []T] struct {
Name string
Id T
Stuff S
}
//也可以如下
type Company[T int | string, S []int | []string] struct {
Name string
Id T
Stuff S
}使用
Company[int, []int]{
Name: "lili",
Id: 1,
Stuff: []int{1},
}TIP
在泛型結構體中,更推薦這種寫法
type Company[T int | string, S int | string] struct {
Name string
Id T
Stuff []S
}SayAble 是一個泛型接口,Person 實現了該接口。
type SayAble[T int | string] interface {
Say() T
}
type Person[T int | string] struct {
msg T
}
func (p Person[T]) Say() T {
return p.msg
}
func main() {
var s SayAble[string]
s = Person[string]{"hello world"}
fmt.Println(s.Say())
}泛型接口
泛型接口可以提供更好抽象約束能力,下面是一個例子
func PrintObj[T fmt.Stringer](s T) {
fmt.Println(s.String())
}
type Person struct {
Name string
}
func (p Person) String() string {
return fmt.Sprintf("Person: %s", p.Name)
}
func main() {
PrintObj(Person{Name: "Alice"})
}也可以將非泛型接口作為泛型的類型形參
func Write[W io.Writer](w W, bs []byte) (int, error) {
return w.Write(bs)
}泛型斷言
我們可以使用泛型來對 any 類型進行類型斷言,比如下面一個函數就可以斷言所有的類型。
func Assert[T any](v any) (bool, T) {
var av T
if v == nil {
return false, av
}
av, ok := v.(T)
return ok, av
}類型集
在 1.18 以後,接口的定義變為了類型集 (type set),含有類型集的接口又稱為 General interfaces 即通用接口。
An interface type defines a type set
類型集只能用於泛型中的類型約束,不能用作類型聲明,類型轉換,類型斷言。類型集作為一個集合,就會有空集,並集,交集,接下來將會講解這三種情況。
並集
接口類型 SignedInt 是一個類型集,有符號整數類型的並集就是 SignedInt,反過來 SignedInt 就是它們的超集。
type SignedInt interface {
int8 | int16 | int | int32 | int64
}基本數據類型如此,對待其它通用接口也是如此
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnSignedInt interface {
uint8 | uint16 | uint32 | uint64
}
type Integer interface {
SignedInt | UnSignedInt
}交集
非空接口的類型集是其所有元素的類型集的交集,翻譯成人話就是:如果一個接口包含多個非空類型集,那麼該接口就是這些類型集的交集,例子如下
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type Integer interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type Number interface {
SignedInt
Integer
}例子中的交集肯定就是 SignedInt,
func Do[T Number](n T) T {
return n
}
Do[int](2)
DO[uint](2) //無法通過編譯空集
空集就是沒有交集,例子如下,下面例子中的 Integer 就是一個類型空集。
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnsignedInt interface {
uint8 | uint16 | uint | uint32 | uint64
}
type Integer interface {
SignedInt
UnsignedInt
}因為無符號整數和有符號整數兩個肯定沒有交集,所以交集就是個空集,下方例子中不管傳什麼類型都無法通過編譯。
Do[Integer](1)
Do[Integer](-100)空接口
空接口與空集並不同,空接口是所有類型集的集合,即包含所有類型。
func Do[T interface{}](n T) T {
return n
}
func main() {
Do[struct{}](struct{}{})
Do[any]("abc")
}不過我們一般會使用 any 來作為泛型形參,因為 inerface{} 不好看。
底層類型
當使用 type 關鍵字聲明了一個新的類型時,即便其底層類型包含在類型集內,當傳入時也依舊會無法通過編譯。
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type TinyInt int8
func Do[T Int](n T) T {
return n
}
func main() {
Do[TinyInt](1) // 無法通過編譯,即便其底層類型屬於Int類型集的范圍內
}有兩種解決辦法,第一種是往類型集中並入該類型,但是這毫無意義,因為 TinyInt 與 int8 底層類型就是一致的,所以就有了第二種解決辦法。
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64 | TinyInt
}使用 ~ 符號,來表示底層類型,如果一個類型的底層類型屬於該類型集,那麼該類型就屬於該類型集,如下所示
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64
}修改過後就可以通過編譯了。
func main() {
Do[TinyInt](1) // 可以通過編譯,因為TinyInt在類型集Int內
}注意點
泛型不能作為一個類型的基本類型
以下寫法是錯誤的,泛型形參 T 是不能作為基礎類型的
type GenericType[T int | int32 | int64] T雖然下列的寫法是允許的,不過毫無意義而且可能會造成數值溢出的問題,所以並不推薦
type GenericType[T int | int32 | int64] int泛型類型無法使用類型斷言
對泛型類型使用類型斷言將會無法通過編譯,泛型要解決的問題是 類型無關 的,如果一個問題需要根據不同類型做出不同的邏輯,那麼就根本不應該使用泛型,應該使用 interface{} 或者 any。
func Sum[T int | float64](a, b T) T {
ints,ok := a.(int) // 不被允許
switch a.(type) { // 不被允許
case int:
case bool:
...
}
return a + b
}匿名結構不支持泛型
匿名結構體是不支持泛型的,如下的代碼將無法通過編譯
testStruct := struct[T int | string] {
Name string
Id T
}[int]{
Name: "jack",
Id: 1
}匿名函數不支持自定義泛型
以下兩種寫法都將無法通過編譯
var sum[T int | string] func (a, b T) T
sum := func[T int | string](a,b T) T{
...
}但是可以 使用 已有的泛型類型,例如閉包中
func Sum[T int | float64](a, b T) T {
sub := func(c, d T) T {
return c - d
}
return sub(a,b) + a + b
}不支持泛型方法
方法是不能擁有泛型形參的,但是 receiver 可以擁有泛型形參。如下的代碼將會無法通過編譯
type GenericStruct[T int | string] struct {
Name string
Id T
}
func (g GenericStruct[T]) name[S int | float64](a S) S {
return a
}類型集無法作為類型實參
只要是帶有類型集的接口,都無法當作類型實參。
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
func Do[T SignedInt](n T) T {
return n
}
func main() {
Do[SignedInt](1) // 無法通過編譯
}類型集中的交集問題
對於非接口類型,類型並集中不能有交集,例如下例中的 TinyInt 與 ~int8 有交集。
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // 無法通過編譯
}
type TinyInt int8但是對於接口類型的話,就允許有交集,如下例
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // 可以通過編譯
}
type TinyInt interface {
int8
}類型集無法直接或間接的並入自身
以下示例中,Floats 直接的並入了自身,而 Double 又並入了 Floats,所以又間接的並入了自身。
type Floats interface { // 代碼無法通過編譯
Floats | Double
}
type Double interface {
Floats
}comparable 接口無法並入類型集
同樣的,也無法並入類型約束中,所以基本上都是單獨使用。
func Do[T comparable | Integer](n T) T { //無法通過編譯
return n
}
type Number interface { // 無法通過編譯
Integer | comparable
}
type Comparable interface { // 可以通過編譯但是毫無意義
comparable
}方法集無法並入類型集
任何包含方法的接口,都不能並入類型集合
type I interface {
int | fmt.Stringer // cannot use fmt.Stringer in union (fmt.Stringer contains methods)
}但是它們可以做交集,不過這樣做沒有任何意義
type I interface {
int
fmt.Stringer
}使用
數據結構是泛型最常見的使用場景,下面借由兩個數據結構來展示下泛型如何使用。
隊列
下面用泛型實現一個簡單的隊列,首先聲明隊列類型,隊列中的元素類型可以是任意的,所以類型約束為 any
type Queue[T any] []T總共只有四個方法 Pop ,Peek,Push,Size,代碼如下。
type Queue[T any] []T
func (q *Queue[T]) Push(e T) {
*q = append(*q, e)
}
func (q *Queue[T]) Pop(e T) (_ T) {
if q.Size() > 0 {
res := q.Peek()
*q = (*q)[1:]
return res
}
return
}
func (q *Queue[T]) Peek() (_ T) {
if q.Size() > 0 {
return (*q)[0]
}
return
}
func (q *Queue[T]) Size() int {
return len(*q)
}在 Pop 和 Peek 方法中,可以看到返回值是 _ T,這是具名返回值的使用方式,但是又采用了下劃線 _ 表示這是匿名的,這並非多此一舉,而是為了表示泛型零值。由於采用了泛型,當隊列為空時,需要返回零值,但由於類型未知,不可能返回具體的類型,借由上面的那種方式就可以返回泛型零值。也可以聲明泛型變量的方式來解決零值問題,對於一個泛型變量,其默認的值就是該類型的零值,如下
func (q *Queue[T]) Pop(e T) T {
var res T
if q.Size() > 0 {
res = q.Peek()
*q = (*q)[1:]
return res
}
return res
}堆
上面隊列的例子,由於對元素沒有任何的要求,所以類型約束為 any。但堆就不一樣了,堆是一種特殊的數據結構,它可以在 O(1) 的時間內判斷最大或最小值,所以它對元素有一個要求,那就是必須是可以排序的類型,但內置的可排序類型只有數字和字符串,所以在堆的初始化時,需要傳入一個自定義的比較器,比較器由調用者提供,並且比較器也必須使用泛型,如下
type Comparator[T any] func(a, b T) int下面是一個簡單二項堆的實現,先聲明泛型結構體,依舊采用 any 進行約束,這樣可以存放任意類型
type Comparator[T any] func(a, b T) int
type BinaryHeap[T any] struct {
s []T
c Comparator[T]
}幾個方法實現
func (heap *BinaryHeap[T]) Peek() (_ T) {
if heap.Size() > 0 {
return heap.s[0]
}
return
}
func (heap *BinaryHeap[T]) Pop() (_ T) {
size := heap.Size()
if size > 0 {
res := heap.s[0]
heap.s[0], heap.s[size-1] = heap.s[size-1], heap.s[0]
heap.s = heap.s[:size-1]
heap.down(0)
return res
}
return
}
func (heap *BinaryHeap[T]) Push(e T) {
heap.s = append(heap.s, e)
heap.up(heap.Size() - 1)
}
func (heap *BinaryHeap[T]) up(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
for parentIndex := i>>1 - 1; parentIndex >= 0; parentIndex = i>>1 - 1 {
// greater than or equal to
if heap.compare(heap.s[i], heap.s[parentIndex]) >= 0 {
break
}
heap.s[i], heap.s[parentIndex] = heap.s[parentIndex], heap.s[i]
i = parentIndex
}
}
func (heap *BinaryHeap[T]) down(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
size := heap.Size()
for lsonIndex := i<<1 + 1; lsonIndex < size; lsonIndex = i<<1 + 1 {
rsonIndex := lsonIndex + 1
if rsonIndex < size && heap.compare(heap.s[rsonIndex], heap.s[lsonIndex]) < 0 {
lsonIndex = rsonIndex
}
// less than or equal to
if heap.compare(heap.s[i], heap.s[lsonIndex]) <= 0 {
break
}
heap.s[i], heap.s[lsonIndex] = heap.s[lsonIndex], heap.s[i]
i = lsonIndex
}
}
func (heap *BinaryHeap[T]) Size() int {
return len(heap.s)
}
func NewHeap[T any](n int, c Comparator[T]) BinaryHeap[T] {
var heap BinaryHeap[T]
heap.s = make([]T, 0, n)
heap.Comparator = c
return heap
}使用起來如下
type Person struct {
Age int
Name string
}
func main() {
heap := NewHeap[Person](10, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
heap.Push(Person{Age: 10, Name: "John"})
heap.Push(Person{Age: 18, Name: "mike"})
heap.Push(Person{Age: 9, Name: "lili"})
heap.Push(Person{Age: 32, Name: "miki"})
fmt.Println(heap.Peek())
fmt.Println(heap.Pop())
fmt.Println(heap.Peek())
}輸出
{9 lili}
{9 lili}
{10 John}有泛型的加持,原本不可排序的類型傳入比較器後也可以使用堆了,這樣做肯定比以前使用 interface{} 來進行類型轉換和斷言要優雅和方便很多。
對象池
原版對象池只能用 any 類型,每次取出來都要進行類型斷言,用泛型簡單改造後,就可以省去這個工作。
package main
import (
"bytes"
"fmt"
"sync"
)
func NewPool[T any](newFn func() T) *Pool[T] {
return &Pool[T]{
pool: &sync.Pool{
New: func() interface{} {
return newFn()
},
},
}
}
type Pool[T any] struct {
pool *sync.Pool
}
func (p *Pool[T]) Put(v T) {
p.pool.Put(v)
}
func (p *Pool[T]) Get() T {
var v T
get := p.pool.Get()
if get != nil {
v, _ = get.(T)
}
return v
}
func main() {
bufferPool := NewPool(func() *bytes.Buffer {
return bytes.NewBuffer(nil)
})
for range 100 {
buffer := bufferPool.Get()
buffer.WriteString("Hello, World!")
fmt.Println(buffer.String())
buffer.Reset()
bufferPool.Put(buffer)
}
}小結
go 的一大特點就是編譯速度非常快,編譯快是因為編譯期做的優化少,泛型的加入會導致編譯器的工作量增加,工作更加復雜,這必然會導致編譯速度變慢,事實上當初 go1.18 剛推出泛型的時候確實導致編譯更慢了,go 團隊既想加入泛型又不想太拖累編譯速度,開發者用的順手,編譯器就難受,反過來編譯器輕松了(最輕松的當然是直接不要泛型),開發者就難受了,現如今的泛型就是這兩者之間妥協後的產物。
