通過引入 類型形參 和 類型實參 這兩個概念,我們讓一個函數獲得了處理多種不同類型數據的能力,這種編程方式被稱為 泛型編程。
2. Go的泛型
- 類型形參 (Type parameter)
- 類型實參(Type argument)
- 類型形參列表( Type parameter list)
- 類型約束(Type constraint)
- 實例化(Instantiations)
- 泛型類型(Generic type)
- 泛型接收器(Generic receiver)
- 泛型函數(Generic function)
基本格式:
type Slice[T int|float32|float64 ] []T
- T 就是上面介紹過的類型形參(Type parameter),在定義Slice類型的時候 T 代表的具體類型并不確定,類似一個占位符
- int|float32|float64 這部分被稱為類型約束(Type constraint),中間的 | 的意思是告訴編譯器,類型形參 T 只可以接收 int 或 float32 或 float64 這三種類型的實參
- 中括號里的 T int|float32|float64 這一整串因為定義了所有的類型形參(在這個例子里只有一個類型形參T),所以我們稱其為 類型形參列表(type parameter list)
- 這里新定義的類型名稱叫 Slice[T]
泛型類型不能直接拿來使用,必須傳入類型實參(Type argument) 將其確定為具體的類型之后才可使用。而傳入類型實參確定具體類型的操作被稱為 實例化(Instantiations)
// 聲明一個泛型
type slice[T int | float32 | float64] []Tfunc main() {// 這里傳入了類型實參int,泛型類型Slice[T]被實例化為具體的類型 Slice[int]var a slice[int] = []int{1, 2, 3}fmt.Println(a) //[1 2 3]// 傳入類型實參float32, 將泛型類型Slice[T]實例化為具體的類型 Slice[float32]var b slice[float32] = []float32{1.2, 123.123, 2123.1}fmt.Println(b) //[1.2 123.123 2123.1]
}
其他類型的泛型
//泛型map
type myMap[KEP int | string, VAL int | float32] map[KEP]VALfunc main() {table := myMap[string, int]{"xiaoming": 190,"xiaohong": 150,}fmt.Printf("%+v", table) //map[xiaohong:150 xiaoming:190]
}
//泛型結構體
type myStruct[T int|string] struct {name stringdata T
}
// 一個泛型接口(關于泛型接口在后半部分會詳細講解)
type IPrintData[T int | float32 | string] interface {Print(data T)
}
// 一個泛型通道,可用類型實參 int 或 string 實例化
type MyChan[T int | string] chan T
3 類型形參的互相套用
// 泛型嵌套
type woStruct[T int | string, s []T] struct {Data smaxval Tminval T
}func main() {var test woStruct[int, []int] = woStruct[int, []int]{[]int{1, 2, 3},12,31,}fmt.Printf("%+v", test) //{Data:[1 2 3] maxval:12 minval:31}
}
任何泛型類型都必須傳入類型實參實例化才可以使用。上面的代碼中,我們為T傳入了實參 int,然后因為 S 的定義是 []T ,所以 S 的實參自然是 []int
因為 S 的定義是 []T ,所以 T 一定決定了的話 S 的實參就不能隨便亂傳了
幾種語法錯誤
- 定義泛型類型的時候,基礎類型不能只有類型形參,如下:
- 當類型約束的一些寫法會被編譯器誤認為是表達式時會報錯。如下:
go
復制代碼// 錯誤,類型形參不能單獨使用
type CommonType[T int|string|float32] T
go
復制代碼//? 錯誤。T *int會被編譯器誤認為是表達式 T乘以int,而不是int指針
type NewType[T *int] []T
// 上面代碼再編譯器眼中:它認為你要定義一個存放切片的數組,數組長度由 T 乘以 int 計算得到
type NewType [T * int][]T //? 錯誤。和上面一樣,這里不光*被會認為是乘號,| 還會被認為是按位或操作
type NewType2[T *int|*float64] []T //? 錯誤
type NewType2 [T (int)] []T
為了避免這種誤解,解決辦法就是給類型約束包上 interface{} 或加上逗號消除歧義(關于接口具體的用法會在后半篇提及)
go
復制代碼type NewType[T interface{*int}] []T
type NewType2[T interface{*int|*float64}] []T // 如果類型約束中只有一個類型,可以添加個逗號消除歧義
type NewType3[T *int,] []T//? 錯誤。如果類型約束不止一個類型,加逗號是不行的
type NewType4[T *int|*float32,] []T
因為上面逗號的用法限制比較大,這里推薦統一用 interface{} 解決問題
匿名結構體不支持泛型
4. 泛型receiver
type mySlice[T int | string | float32] []T//泛型方法
func (m mySlice[T]) sum() T {var sum Tfor _, v := range m {sum += v}return sum
}func main() {var t mySlice[int] = []int{1, 2, 3, 4, 5}fmt.Println(t.sum()) //15var f mySlice[float32] = []float32{1.2, 3.4, 5.6}fmt.Println(f.sum()) //10.200001}
- 首先看receiver (s MySlice[T]) ,所以我們直接把類型名稱 MySlice[T] 寫入了receiver中
- 然后方法的返回參數我們使用了類型形參 T ****(實際上如果有需要的話,方法的接收參數也可以實用類型形參)
- 在方法的定義中,我們也可以使用類型形參 T (在這個例子里,我們通過 var sum T 定義了一個新的變量 sum )
動態判斷變量的類型
泛型不像接口一樣可以通過類型斷言來判斷其類型
但可以通過反射來實現動態判斷其類型
func (receiver Queue[T]) Put(value T) {// Printf() 可輸出變量value的類型(底層就是通過反射實現的)fmt.Printf("%T", value) // 通過反射可以動態獲得變量value的類型從而分情況處理v := reflect.ValueOf(value)switch v.Kind() {case reflect.Int:// do somethingcase reflect.String:// do something}// ...
}
泛型函數
func main() {//在調用函數的時候聲明類型fmt.Println(add[float64](float64(6.123), float64(8.12312)))//自動類型推斷fmt.Println(add(19, 123)) //142
}
//定義一個函數泛型
func add[T int | float32 | float64](a T, b T) T {return a + b
}
匿名函數不能自己定義類型形參:
但是匿名函數可以使用別處定義好的類型實參,如:
go
復制代碼func MyFunc[T int | float32 | float64](a, b T) {// 匿名函數可使用已經定義好的類型形參fn2 := func(i T, j T) T {return i*2 - j*2}fn2(a, b)
}
?
既然函數都支持泛型了,那你應該自然會想到,方法支不支持泛型?很不幸,目前Go的方法并不支持泛型
6. 變得復雜的接口
type IntUintFloat interface {int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}type Slice[T IntUintFloat] []T
這段代碼把類型約束給單獨拿出來,寫入了接口類型 IntUintFloat 當中。需要指定類型約束的時候直接使用接口 IntUintFloat 即可。
不過這樣的代碼依舊不好維
6.1 ~ : 指定底層類型
上面定義的 Slie[T] 雖然可以達到目的,但是有一個缺點:
go
復制代碼var s1 Slice[int] // 正確 type MyInt int
var s2 Slice[MyInt] // ? 錯誤。MyInt類型底層類型是int但并不是int類型,不符合 Slice[T] 的類型約束
這里發生錯誤的原因是,泛型類型 Slice[T] 允許的是 int 作為類型實參,而不是 MyInt (雖然 MyInt 類型底層類型是 int ,但它依舊不是 int 類型)。
為了從根本上解決這個問題,Go新增了一個符號 ~ ,在類型約束中使用類似 ~int 這種寫法的話,就代表著不光是 int ,所有以 int 為底層類型的類型也都可用于實例化。
限制:使用 ~ 時有一定的限制:
- ~后面的類型不能為接口
- ~后面的類型必須為基本類型
type MyInt inttype _ interface {~[]byte // 正確~MyInt // 錯誤,~后的類型必須為基本類型~error // 錯誤,~后的類型不能為接口
}
6.2 從方法集(Method set)到類型集(Type set)
當滿足以下條件時,我們可以說 類型 T 實現了接口 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)
6.2.2 類型的并集
并集我們已經很熟悉了,之前一直使用的 | 符號就是求類型的并集( union )
6.2.3 類型的交集
接口可以不止書寫一行,如果一個接口有多行類型定義,那么取它們之間的 交集
go
復制代碼type AllInt interface {~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}type Uint interface {~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}type A interface { // 接口A代表的類型集是 AllInt 和 Uint 的交集AllIntUint
}type B interface { // 接口B代表的類型集是 AllInt 和 ~int 的交集AllInt~int
}
上面這個例子中
- 接口 A 代表的是 AllInt 與 Uint 的 交集,即 ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
- 接口 B 代表的則是 AllInt 和 ~int 的交集,即 ~int
除了上面的交集,下面也是一種交集:
go
復制代碼type C interface {~intint
}
很顯然,~int 和 int 的交集只有int一種類型,所以接口C代表的類型集中只有int一種類型
?
6.2.4 空集
當多個類型的交集如下面 Bad 這樣為空的時候, Bad 這個接口代表的類型集為一個空集:
go
復制代碼type Bad interface {intfloat32
} // 類型 int 和 float32 沒有相交的類型,所以接口 Bad 代表的類型集為空
?
6.2.5 空接口和 any
上面說了空集,接下來說一個特殊的類型集——空接口 interface{} 。因為,Go1.18開始接口的定義發生了改變,所以 interface{} 的定義也發生了一些變更:
空接口代表了所有類型的集合
// 空接口代表所有類型的集合。寫入類型約束意味著所有類型都可拿來做類型實參
type Slice[T interface{}] []T
因為空接口是一個包含了所有類型的類型集,所以我們經常會用到它。于是,Go1.18開始提供了一個和空接口 interface{} 等價的新關鍵詞 any ,用來使代碼更簡單:
type Slice[T any] []T // 代碼等價于 type Slice[T interface{}] []T
?
6.2.6 comparable(可比較) 和 可排序(ordered)
Go直接內置了一個叫 comparable 的接口,它代表了所有可用 != 以及 == 對比的類型
6.3.1 基本接口(Basic interface)
接口定義中如果只有方法的話,那么這種接口被稱為基本接口(Basic interface)。這種接口就是Go1.18之前的接口,用法也基本和Go1.18之前保持一致
6.3.2 一般接口(General interface)
如果接口內不光只有方法,還有類型的話,這種接口被稱為 一般接口(General interface) ,如下例子都是一般接口:
go
復制代碼type Uint interface { // 接口 Uint 中有類型,所以是一般接口~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}type ReadWriter interface { // ReadWriter 接口既有方法也有類型,所以是一般接口~string | ~[]runeRead(p []byte) (n int, err error)Write(p []byte) (n int, err error)
}
一般接口類型不能用來定義變量,只能用于泛型的類型約束中。所以以下的用法是錯誤的:
go
復制代碼type Uint interface {~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}var uintInf Uint // 錯誤。Uint是一般接口,只能用于類型約束,不得用于變量定義
這一限制保證了一般接口的使用被限定在了泛型之中,不會影響到Go1.18之前的代碼,同時也極大減少了書寫代碼時的心智負擔
?
?