1. 概述#
泛型是很多語言的標配,合理的使用泛型便於開發,然而,Golang 一開始並不支持泛型,因為開發 Golang 的人認為泛型重要但不是必須的,所以 Golang 一開始發布的時候沒有泛型。今年,在 Go 社區的呼籲下,Go1.17 發布了泛型的體驗版,為 Go1.18 的正式版泛型做了鋪墊。本文介紹一下 Go 中的泛型和一些使用方法。
2. 什麼是泛型#
在介紹泛型之前先了解一下多態
如:
動物類
有三種對象 -> 狗,貓,雞
有一個相同的事件 -> 叫
有三種不同的表現 -> 汪汪,喵喵,咯咯達
多態分為兩類:
- 臨時性多態(根據實參類型調用對應的版本,支持調用的數量十分有限,如:函數重載)
- 參數化多態(根據實參類型生成不同的版本,支持任意數量的調用,這就是泛型)
3. 泛型困境#
泛型有好處但也有壞處,怎麼平衡泛型的好處和壞處就成了泛型設計者要面臨的問題了。
泛型的困境:
簡單來說就是:編碼效率低(程序員開發慢)、編譯效率低(編譯器編譯慢)、運行效率低(用戶體驗差)
4. Go1.17 中泛型的要點#
-
函數可以通過 type 關鍵字引入額外的類型參數 (type parameters) 列表:func F (type T)(p T) { ... } 。
-
這些類型參數可以像一般的參數一樣在函數體中使用。
-
類型也可以擁有類型參數列表:type M (type T) [] T。
-
每個類型參數可以擁有一個約束:func F (type T Constraint)(p T) { ... }。
-
使用 interface 來描述類型的約束。
-
被用作類型約束的 interface 可以擁有一個預聲明類型列表,限制了實現此接口的類型的基礎類型。
-
使用泛型函數或類型時需要傳入類型實參。
-
一般情況下,類型推斷允許用戶在調用泛型函數時省略類型實參。
-
如果類型參數具有類型約束,則類型實參必須實現接口。
-
泛型函數只允許進行類型約束所規定的操作。
5. 如何使用泛型#
5.1 如何輸出泛型#
package main
import (
"fmt"
)
//使用[]來保存額外的類型參數列表
//[T any]是類型參數,表示該函數支持任意T類型
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Print("\n")
}
func main() {
printSlice[int]([]int{1, 2, 3, 4, 5})
printSlice[float64]([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
//調用printSlice[string]([]string{“Hello”,“World”})時,會被類型推導為string類型
//如果編譯器可以實現類型推導,printSlice([]string{“Hello”,“World”})
printSlice([]string{"Hello", "World"})
printSlice[int64]([]int64{5, 4, 3, 2, 1})
}
使用泛型首先要 Go1.17 版本以上
如果直接運行上面的代碼可能會報錯
.\main.go:9:6: missing function body
.\main.go:9:16: syntax error: unexpected [, expecting (
要想順利運行,需要加上參數:-gcflags=-G=3
5.2 如何約束泛型的類型範圍#
每個類型都有一個類型約束,就像每個普通參數都有一個類型一樣。
在 Go1.17 版本中,泛型函數只能使用類型參數所能實例化出的任意類型 都能 支持的操作
(對於下代碼,add 函數中的 + 應是 int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64,uintptr,float32,float64, complex64, complex128,string
都能支持的操作)
package main
import (
"fmt"
)
//限定泛型類型範圍
type Addable interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64, complex64, complex128,
string
}
func add[T Addable] (a, b T) T {
return a + b
}
func main() {
fmt.Println(add(1,2))
fmt.Println(add("hello","world"))
}
對於沒有任何約束的類型參數實例,允許對其的操作有:
- 聲明這些類型的變量。
- 使用相同類型的值為這些變量賦值。
- 將這些類型的變量以實參形式傳給函數或從作為函數返回值。
- 取這些變量的地址。
- 將這些類型的值轉換或賦值給 interface {} 類型變量。
- 通過類型斷言將一個接口值賦值給這類型的變量。
- 在 type switch 塊中作為一個 case 分支。
- 定義和使用由該類型組成的複合類型,比如:元素類型為該類型的切片。
- 將該類型傳遞給一些內置函數,比如 new。
5.21 comparable 約束#
不是所有的類型都可以用 ==
進行比較,Go 內置了一個 comparable 約束,表示可比較
package main
import (
"fmt"
)
func findFunc[T comparable](a []T, v T) int {
for i, e := range a {
if e == v {
return i
}
}
return -1
}
func main() {
fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}
5.3 泛型中的切片#
和泛型函數一樣,使用泛型類型時,首要要對其進行實例化(顯示為類型參數賦值類型)如:vs:=slice{5,4,2,1}
package main
import (
"fmt"
)
type slice[T any] []T
/*
type any interface {
type int, string
}*/
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Print("\n")
}
func main() {
// note1: cannot use generic type slice[T interface{}] without instantiation
// note2: cannot use generic type slice[T any] without instantiation
vs := slice[int]{5, 4, 2, 1}
printSlice(vs)
5.4 泛型中的指針#
package main
import (
"fmt"
)
func pointerOf[T any](v T) *T {
return &v
}
func main() {
sp := pointerOf("foo")
fmt.Println(*sp)
ip := pointerOf(123)
fmt.Println(*ip)
*ip = 234
fmt.Println(*ip)
}
5.5 泛型中的 map#
package main
import (
"fmt"
)
func mapFunc[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a), cap(a))
for i, e := range a {
n[i] = f(e)
}
return n
}
func main() {
vi := []int{1, 2, 3, 4, 5, 6}
vs := mapFunc(vi, func(v int) string {
return "<" + fmt.Sprint(v*v) + ">"
})
fmt.Println(vs)
}
5.6 泛型中的隊列#
package main
import (
"fmt"
)
type queue[T any] []T
func (q *queue[T]) enqueue(v T) {
*q = append(*q, v)
}
func (q *queue[T]) dequeue() (T, bool) {
if len(*q) == 0 {
var zero T
return zero, false
}
r := (*q)[0]
*q = (*q)[1:]
return r, true
}
func main() {
q := new(queue[int])
q.enqueue(5)
q.enqueue(6)
fmt.Println(q)
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
}
6. 總結#
本文簡單介紹了 Go 中的泛型用法,合理地使用泛型有利於提高開發效率。