Golang泛型與類(lèi)型約束的用法詳解
一、環(huán)境
Go 1.20.2
二、沒(méi)有泛型的Go
假設(shè)現(xiàn)在我們需要寫(xiě)一個(gè)函數(shù),實(shí)現(xiàn):
1)輸入一個(gè)切片參數(shù),切片類(lèi)型可以是[]int或[]float64,然后將所有元素相加的“和”返回
2)如果是int切片,返回int類(lèi)型;如果是float64切片,返回float64類(lèi)型
當(dāng)然,最簡(jiǎn)單的方法是寫(xiě)兩個(gè)函數(shù)SumSliceInt(s []int)、SumSliceFloat64(s []float64)來(lái)分別支持不同類(lèi)型的切片,但是這樣會(huì)導(dǎo)致大部分代碼重復(fù)冗余,不是很優(yōu)雅。那么有沒(méi)有辦法只寫(xiě)一個(gè)函數(shù)呢?
我們知道,在Go中所有的類(lèi)型都實(shí)現(xiàn)了interface{}接口,所以如果想讓一個(gè)變量支持多種數(shù)據(jù)類(lèi)型,我們可以將這個(gè)變量聲明為interface{}類(lèi)型,例如var slice interface{},然后使用類(lèi)型斷言(.(type))來(lái)判斷這個(gè)變量的類(lèi)型。
interface{} + 類(lèi)型斷言:
// any是inerface{}的別名,兩者是完全相同的:type any = interface{}
func SumSlice(slice any) (any, error) {
switch s := slice.(type) {
case []int:
sum := 0
for _, v := range s {
sum += v
}
return sum, nil
case []float64:
sum := float64(0)
for _, v := range s {
sum += v
}
return sum, nil
default:
return nil, fmt.Errorf("unsupported slice type: %T", slice)
}
}從上述代碼可見(jiàn),雖然使用interface{}類(lèi)型可以實(shí)現(xiàn)在同一個(gè)函數(shù)內(nèi)支持兩種不同切片類(lèi)型,但是每個(gè)case塊內(nèi)的代碼仍然是高度相似和重復(fù)的,代碼冗余的問(wèn)題沒(méi)有得到根本的解決。
三、泛型的優(yōu)點(diǎn)
幸運(yùn)的是,在Go 1.18之后開(kāi)始支持了泛型(Generics),我們可以使用泛型來(lái)解決這個(gè)問(wèn)題:
func SumSlice[T interface{ int | float64 }](slice []T) T {
var sum T = 0
for _, v := range slice {
sum += v
}
return sum
}是不是簡(jiǎn)潔了很多?而且,泛型相比interface{}還有以下優(yōu)勢(shì):
- 可復(fù)用性:提高了代碼的可復(fù)用性,減少代碼冗余。
- 類(lèi)型安全性:泛型在編譯時(shí)就會(huì)進(jìn)行類(lèi)型安全檢查,可以確保編譯出來(lái)的代碼就是類(lèi)型安全的;而
interface{}是在運(yùn)行時(shí)才進(jìn)行類(lèi)型判斷,如果編寫(xiě)的代碼在類(lèi)型判斷上有bug或缺漏,就會(huì)導(dǎo)致Go在運(yùn)行過(guò)程中報(bào)錯(cuò)。 - 性能:不同類(lèi)型的數(shù)據(jù)在賦值給
interface{}變量時(shí),會(huì)有一個(gè)隱式的裝箱操作,從interface{}取數(shù)據(jù)時(shí)也會(huì)有一個(gè)隱式的拆箱操作,而泛型就不存在裝箱拆箱過(guò)程,沒(méi)有額外的性能開(kāi)銷(xiāo)。
四、理解泛型
(一)泛型函數(shù)(Generic function)
1)定義
編寫(xiě)一個(gè)函數(shù),輸入a、b兩個(gè)泛型參數(shù),返回它們的和:
// T的名字可以更改,改成K、V、MM之類(lèi)的都可以,只是一般比較常用的是T
// 這是一個(gè)不完整的錯(cuò)誤例子
func Sum(a, b T) T {
return a + b
}大寫(xiě)字母T的名字叫類(lèi)型形參(Type parameter),代表a、b參數(shù)是泛型,可以接受多種類(lèi)型,但具體可以接受哪些類(lèi)型呢?在上面的定義中并沒(méi)有給出這部分信息,要知道,并不是所有的類(lèi)型都可以相加的,因此這里就引出了約束的概念,我們需要對(duì)T可以接受的類(lèi)型范圍作出約束:
// 正確例子
func Sum[T interface{ int | float64 }](a, b T) T {
return a + b
}中括號(hào)[]之間的空間用于定義類(lèi)型形參,支持定義一個(gè)或多個(gè)
T:類(lèi)型形參的名字interface{ int | float64 }:對(duì)T的類(lèi)型約束(Type Constraint),必須是一個(gè)接口,約束T只可以是int或float64
為了簡(jiǎn)化寫(xiě)法,類(lèi)型約束中的interface{}在某些情況下是可以省略的,所以可以簡(jiǎn)寫(xiě)成:
func Sum[T int | float64](a, b T) T {
return a + b
}interface{}不能省略的一些情況:
// 當(dāng)接口中包含方法時(shí),不能省略
func Contains[T interface{ Equal() bool }](num T) {
}可以定義多個(gè)類(lèi)型形參:
func Add[T int, E float64](a T, b E) E {
return E(a) + b
}2)調(diào)用
以上面的Sum泛型函數(shù)為例,完整的調(diào)用寫(xiě)法為:
Sum[int](1, 2) Sum[float64](1.1, 2.2)
[]之間的內(nèi)容稱(chēng)為類(lèi)型實(shí)參(Type argument),是函數(shù)定義中的類(lèi)型形參T的實(shí)際值,例如傳int過(guò)去,那么T的實(shí)際值就是int。
類(lèi)型形參確定為具體類(lèi)型的過(guò)程稱(chēng)為實(shí)例化(Instantiations),可以簡(jiǎn)單理解為將函數(shù)定義中的T替換為具體類(lèi)型:

泛型函數(shù)實(shí)例化后,就可以像普通函數(shù)那樣調(diào)用了。
但大多數(shù)時(shí)候,編譯器都可以自動(dòng)推導(dǎo)出該具體類(lèi)型,無(wú)需我們主動(dòng)告知,這個(gè)功能叫函數(shù)實(shí)參類(lèi)型推導(dǎo)(Function argument type inference)。所以可以簡(jiǎn)寫(xiě)成:
// 簡(jiǎn)寫(xiě),跟調(diào)用普通函數(shù)一樣的寫(xiě)法 Sum(1, 2) Sum(1.1, 2.2)
需要注意的是,在調(diào)用這個(gè)函數(shù)時(shí),a、b兩個(gè)參數(shù)的類(lèi)型必須一致,要么兩個(gè)都是int,要么都是float64,不能一個(gè)是int一個(gè)是float64:
Sum(1, 2.3) // 編譯會(huì)報(bào)錯(cuò)
什么時(shí)候不能簡(jiǎn)寫(xiě)?
// 當(dāng)類(lèi)型形參T僅用在返回值,沒(méi)有用在函數(shù)參數(shù)列表時(shí)
func Foo[T int | float64]() T {
return 1
}
Foo() // 報(bào)錯(cuò):cannot infer T
Foo[int]() // OK
Foo[float64]() // OK(二)類(lèi)型約束(Type constraint)
1)接口與約束
Go 使用interface定義類(lèi)型約束。我們知道,在引入泛型之前,interface中只可以聲明一組未實(shí)現(xiàn)的方法,或者內(nèi)嵌其它interface,例如:
// 普通接口
type Driver interface {
SetName(name string) (int, error)
GetName() string
}
// 內(nèi)嵌接口
type ReaderStringer interface {
io.Reader
fmt.Stringer
}接口里的所有方法稱(chēng)之為方法集(Method set)。
引入泛型之后,interface里面可以聲明的元素豐富了很多,可以是任何 Go 類(lèi)型,除了方法、接口以外,還可以是基本類(lèi)型,甚至struct結(jié)構(gòu)體都可以,接口里的這些元素稱(chēng)為類(lèi)型集(Type set):
// 基本類(lèi)型約束
type MyInt interface {
int
}
// 結(jié)構(gòu)體類(lèi)型約束
type Point interface {
struct{ X, Y int }
}
// 內(nèi)嵌其它約束
type MyNumber interface {
MyInt
}
// 聯(lián)合(Unions)類(lèi)型約束,不同類(lèi)型元素之間是“或”的關(guān)系
// 如果元素是一個(gè)接口,這個(gè)接口不能包含任何方法!
type MyFloat interface {
float32 | float64
}有了豐富的類(lèi)型集支持,我們就可以更加方便的使用接口對(duì)類(lèi)型形參T的類(lèi)型作出約束,既可以約束為基本類(lèi)型(int、float32、string…),也可以約束它必須實(shí)現(xiàn)一組方法,靈活性大大增加。
因此前面的Sum函數(shù)還可以改寫(xiě)成:
// 原始例子:
// func Sum[T int | float64](a, b T) T {
// return a + b
// }
type MyNumber interface {
int | float64
}
func Sum[T MyNumber](a, b T) T {
return a + b
}2)結(jié)構(gòu)體類(lèi)型約束
Go 還允許我們使用復(fù)合類(lèi)型字面量來(lái)定義約束。例如,我們可以定義一個(gè)約束,類(lèi)型元素是一個(gè)具有特定結(jié)構(gòu)的struct:
type Point interface {
struct{ X, Y int }
}然而,需要注意的是,雖然我們可以編寫(xiě)受此類(lèi)結(jié)構(gòu)體類(lèi)型約束的泛型函數(shù),但在當(dāng)前版本的 Go 中,函數(shù)無(wú)法訪問(wèn)結(jié)構(gòu)體的字段,例如:
func GetX[T Point](p T) int {
return p.X // p.X undefined (type T has no field or method X)
}3)類(lèi)型近似(Type approximations)
我們知道,在Go中可以創(chuàng)建新的類(lèi)型,例如:
type MyString string
MyString是一個(gè)新的類(lèi)型,底層類(lèi)型是string。
在類(lèi)型約束中,有時(shí)候我們可能并不關(guān)心上層類(lèi)型,只要底層類(lèi)型符合要求就可以,這時(shí)候就可以使用類(lèi)型近似符號(hào):~。
// 創(chuàng)建新類(lèi)型
type MyString string
// 定義類(lèi)型約束
type AnyStr interface {
~string
}
// 定義泛型函數(shù)
func Foo[T AnyStr](param T) T {
return param
}
func main() {
var p1 string = "aaa"
var p2 MyString = "bbb"
Foo(p1)
Foo(p2) // 雖然p2是MyString類(lèi)型,但也可以通過(guò)泛型函數(shù)的類(lèi)型約束檢查
}需要注意的是,類(lèi)型近似中的類(lèi)型,必須是底層類(lèi)型,而且不能是接口類(lèi)型:
type MyInt int
type I0 interface {
~MyInt // 錯(cuò)誤! MyInt不是底層類(lèi)型, int才是
~error // 錯(cuò)誤! error是接口
}(三)泛型類(lèi)型(Generic type)
1)泛型切片
假設(shè)現(xiàn)在有一個(gè)IntSlice類(lèi)型:
type IntSlice []int
var s1 IntSlice = []int{1, 2, 3} // 正常
var s2 IntSlice = []string{"a", "b", "c"} // 報(bào)錯(cuò),因?yàn)镮ntSlice底層類(lèi)型是[]int,字符串無(wú)法賦值很顯然,因?yàn)轭?lèi)型不一致,s2是無(wú)法賦值的,如果想要支持其它類(lèi)型,需要定義新類(lèi)型:
type StringSlice []string type Float32Slice []float32 type Float64Slice []float64 // ...
但是這樣做的問(wèn)題也顯而易見(jiàn),它們結(jié)構(gòu)都是一樣的,只是元素類(lèi)型不同就需要重新定義這么多新類(lèi)型,導(dǎo)致代碼復(fù)雜度增加。
這時(shí)候就可以用泛型類(lèi)型來(lái)解決這個(gè)問(wèn)題:
// 只需定義一種新類(lèi)型,就可以同時(shí)支持[]int/[]string/[]float32多種切片類(lèi)型 // 新類(lèi)型的名字叫 MySlice[T] type MySlice[T int|string|float32] []T
類(lèi)型定義中帶 類(lèi)型形參 的類(lèi)型,稱(chēng)之為泛型類(lèi)型(Generic type)
泛型切片的初始化:
var s1 MySlice[int] = MySlice[int]{1, 2, 3}
var s2 MySlice[string] = MySlice[string]{"a", "b", "c"}
s3 := MySlice[string]{"a", "b", "c"} // 簡(jiǎn)寫(xiě)其它一些例子:
// 泛型Map
type MyMap[K int | string, V any] map[K]V
var m1 MyMap[string, int] = MyMap[string, int]{"a": 1, "b": 2} // 完整寫(xiě)法
m2 := MyMap[int, string]{1: "a", 2: "b"} // 簡(jiǎn)寫(xiě)
// 泛型通道
type MyChan[T int | float32] chan T
var c1 MyChan[int] = make(MyChan[int]) // 完整寫(xiě)法
c2 := make(MyChan[float32]) // 簡(jiǎn)寫(xiě)2)泛型結(jié)構(gòu)體
假設(shè)現(xiàn)在要?jiǎng)?chuàng)建一個(gè)struct結(jié)構(gòu)體,里面含有一個(gè)data泛型屬性,類(lèi)型是一個(gè)int或float64的切片:
type List[T int | float64] struct {
data []T
}給這個(gè)結(jié)構(gòu)體增加一個(gè)Sum方法,用于對(duì)切片求和:
func (l *List[T]) Sum() T {
var sum T
for _, v := range l.data {
sum += v
}
return sum
}實(shí)例化結(jié)構(gòu)體,并調(diào)用Sum方法:
// var list *List[int] = &List[int]{data: []int{1, 2, 3}} // 完整寫(xiě)法
list := &List[int]{data: []int{1, 2, 3}}
sum := list.Sum()
fmt.Println(sum) // 輸出:63)泛型接口
泛型也可以用在接口上:
type Human[T float32] interface {
GetWeight() T
}假設(shè)現(xiàn)在有兩個(gè)結(jié)構(gòu)體,它們都有GetWeight()方法,哪個(gè)結(jié)構(gòu)體實(shí)現(xiàn)了上面Human[T]接口?
// 結(jié)構(gòu)體1
type Person1 struct {
Name string
}
func (p Person1) GetWeight() float32 {
return 66.6
}
// 結(jié)構(gòu)體2
type Person2 struct {
Name string
}
func (p Person2) GetWeight() int {
return 66
}注意觀察兩個(gè)GetWeight()方法的返回值類(lèi)型,因?yàn)槲覀冊(cè)?code>Human[T]接口中約束了T的類(lèi)型只能是float32,而只有Person1結(jié)構(gòu)體的返回值類(lèi)型符合約束,所以實(shí)際上只有Person1結(jié)構(gòu)體實(shí)現(xiàn)了Human[T]接口。
p1 := Person1{Name: "Tim"}
var iface1 Human[float32] = p1 // 正常,因?yàn)镻erson1實(shí)現(xiàn)了接口,所以可以賦值成功
p2 := Person2{Name: "Tim"}
var iface2 Human[float32] = p2 // 報(bào)錯(cuò),因?yàn)镻erson2沒(méi)有實(shí)現(xiàn)接口(五)一些錯(cuò)誤示例
下面列出一些錯(cuò)誤使用泛型的例子。
1)聯(lián)合約束中的類(lèi)型元素限制
聯(lián)合約束中的類(lèi)型元素不能是包含方法的接口:
// 錯(cuò)誤
type ReaderStringer interface {
io.Reader | fmt.Stringer // 錯(cuò)誤,io.Reader和fmt.Stringer是包含方法的接口
}
// 正確
type MyInt interface {
int
}
type MyFloat interface {
float32
}
type MyNumber interface {
MyInt | MyFloat // 正確,MyInt和MyFloat接口里面沒(méi)有包含方法
}聯(lián)合約束中的類(lèi)型元素不能含有comparable接口:
type Number interface {
comparable | int // 含有comparable,報(bào)錯(cuò)
}2)一般接口只能用于泛型的類(lèi)型約束
先解釋下相關(guān)概念,引入泛型后,Go的接口分為兩種類(lèi)型:
- 基本接口(Basic interface)
- 只包含方法的接口,稱(chēng)為基本接口,其實(shí)就是引入泛型之前的那種傳統(tǒng)接口。
- 一般接口(General interface)
- 由于引入泛型后,接口可以定義的元素大大豐富,如果一個(gè)接口里含有除了方法以外的元素,那么這個(gè)接口就稱(chēng)為一般接口。
一般接口只能用于泛型的類(lèi)型約束,不能用于變量、函數(shù)參數(shù)、返回值的類(lèi)型聲明,而基本接口則沒(méi)有此限制:
type NoMethods interface {
int
}
// 錯(cuò)誤,不能用于函數(shù)參數(shù)列表、返回值
func Foo(param NoMethods) NoMethods {
return param
}
// 錯(cuò)誤,不能用來(lái)聲明變量的類(lèi)型
var param NoMethods
// 正確
func Foo[T NoMethods](param T) T {
return param
}總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Golang使用Apache PLC4X連接modbus的示例代碼
Modbus是一種串行通信協(xié)議,是Modicon公司于1979年為使用可編程邏輯控制器(PLC)通信而發(fā)表,這篇文章主要介紹了Golang使用Apache PLC4X連接modbus的示例代碼,需要的朋友可以參考下2024-07-07
詳解golang避免循環(huán)import問(wèn)題(“import cycle not allowed”)
這篇文章主要給大家介紹了關(guān)于golang中不允許循環(huán)import問(wèn)題("import cycle not allowed")的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08
Golang try catch與錯(cuò)誤處理的實(shí)現(xiàn)
社區(qū)不少人在談?wù)?nbsp;golang 為毛不用try/catch模式,而采用苛刻的recovery、panic、defer組合,本文就來(lái)詳細(xì)的介紹一下,感興趣的可以了解一下2021-07-07
Golang實(shí)現(xiàn)組合模式和裝飾模式實(shí)例詳解
這篇文章主要介紹了Golang實(shí)現(xiàn)組合模式和裝飾模式,本文介紹組合模式和裝飾模式,golang實(shí)現(xiàn)兩種模式有共同之處,但在具體應(yīng)用場(chǎng)景有差異。通過(guò)對(duì)比兩個(gè)模式,可以加深理解,需要的朋友可以參考下2022-11-11
go?zero微服務(wù)實(shí)戰(zhàn)處理每秒上萬(wàn)次的下單請(qǐng)求
這篇文章主要為大家介紹了go?zero微服務(wù)實(shí)戰(zhàn)處理每秒上萬(wàn)次的下單請(qǐng)求示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
Golang基于內(nèi)存的鍵值存儲(chǔ)緩存庫(kù)go-cache
go-cache是一個(gè)內(nèi)存中的key:value store/cache庫(kù),適用于單機(jī)應(yīng)用程序,本文主要介紹了Golang基于內(nèi)存的鍵值存儲(chǔ)緩存庫(kù)go-cache,具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
Golang項(xiàng)目在github創(chuàng)建release后自動(dòng)生成二進(jìn)制文件的方法
這篇文章主要介紹了Golang項(xiàng)目在github創(chuàng)建release后如何自動(dòng)生成二進(jìn)制文件,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03

