Go 泛型和非泛型代碼詳解
1. 開(kāi)啟泛型
在 Go1.17 版本中,可以通過(guò):
export GOFLAGS="-gcflags=-G=3"
或者在編譯運(yùn)行程序時(shí)加上:
go run -gcflags=-G=3 main.go
2.無(wú)泛型代碼和泛型代碼
2.1. AddSlice
首先看現(xiàn)在沒(méi)有泛型的代碼:
package main
import (
"fmt"
)
func AddIntSlice(input []int, diff int) []int {
output := make([]int, 0, len(input))
for _, item := range input {
output = append(output, item+diff)
}
return output
}
func AddStrSlice(input []string, diff string) []string {
output := make([]string, 0, len(input))
for _, item := range input {
output = append(output, item+diff)
}
return output
}
func main() {
intSlice := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("intSlice [%+v] + 2 = [%+v]\n", intSlice, AddIntSlice(intSlice, 2))
strSlice := []string{"hi,", "hello,", "bye,"}
fmt.Printf("strSlice [%+v] + man = [%+v]\n", strSlice, AddStrSlice(strSlice, "man"))
}
//output
//intSlice [[1 2 3 4 5 6]] + 2 = [[3 4 5 6 7 8]]
//strSlice [[hi, hello, bye,]] + man = [[hi,man hello,man bye,man]]
上面沒(méi)有使用泛型的代碼中,對(duì) intSlice 和 strSlice,需要構(gòu)造兩個(gè)函數(shù)對(duì)它們進(jìn)行處理;而如果后續(xù)還有 float64、uint32 等類(lèi)型就需要更多地 Add...Slice 函數(shù)。
而如果使用泛型之后,這些 Add...Slice 函數(shù)就可以合并為一個(gè)函數(shù)了,在這個(gè)函數(shù)中,對(duì)那些可以使用 + 操作符的類(lèi)型進(jìn)行加操作(無(wú)論是數(shù)學(xué)的加還是字符串的連接)。
泛型代碼如下:
package main
import (
"fmt"
)
type PlusConstraint interface {
type int, string
}
func AddSlice[T PlusConstraint](input []T, diff T) []T {
output := make([]T, 0, len(input))
for _, item := range input {
output = append(output, item+diff)
}
return output
}
func main() {
intSlice := []int{1, 2, 3, 4, 5}
fmt.Printf("intSlice [%+v] + 2 = [%v]\n", intSlice, AddSlice(intSlice, 2))
strSlice := []string{"hi,", "hello,", "bye,"}
fmt.Printf("strSlice [%v] + man = [%v]\n", strSlice, AddSlice(strSlice, "man"))
}
//output
//intSlice [[1 2 3 4 5]] + 2 = [[3 4 5 6 7]]
//strSlice [[hi, hello, bye,]] + man = [[hi,man hello,man bye,man]]
是不是超級(jí)簡(jiǎn)單,但是 AddSlice 函數(shù)中引入了約束的概念,即 PlusConstraint。AddSlice 的方括號(hào)中是類(lèi)型參數(shù),T 就是這個(gè)類(lèi)型參數(shù)的形參,后面的 PlusConstraint 就是 T 的約束條件,意思是只有滿(mǎn)足約束條件的 T 類(lèi)型才可以在這個(gè)函數(shù)中使用。
AddSlice 后面圓括號(hào)中的參數(shù)是常規(guī)參數(shù)也稱(chēng)為非類(lèi)型參數(shù),它們可以不制定具體類(lèi)型(int、string 等),可以使用 T 來(lái)代替。
而在 AddSlice 中,對(duì)于 T 類(lèi)型的值 item,它會(huì)將 item 和 diff 進(jìn)行 + 操作,可能是數(shù)學(xué)上的累加,也可能是字符串的連接。
那現(xiàn)在你可能要問(wèn)了,T 類(lèi)型就一定是支持 + 操作符的嗎,有沒(méi)有可能是一個(gè) struct 呢?
答案是:不可能。
前面說(shuō)過(guò),只有滿(mǎn)足約束條件的 T 才可以在 AddSlice 中使用,而約束條件就是上面的 PlusConstraint。
PlusConstraint 定義的方式和接口類(lèi)型的定義是一樣的,只不過(guò)內(nèi)部多了一行:
type int, string
這句話(huà)就是說(shuō),只有 int、string 這兩個(gè)類(lèi)型才滿(mǎn)足這個(gè)約束,這里涉及到類(lèi)型集的概念,后續(xù)會(huì)提到。
因此,有了這個(gè)約束條件,傳入到 AddSlice 的參數(shù) input 和 diff 都是可以使用 + 操作符的。如果你的 AddSlice 函數(shù)中想傳入 float46、uint64 等類(lèi)型,就在 PlusConstraint 中加上這兩個(gè)類(lèi)型即可。
上面的代碼中,只是對(duì) int 和 string 兩種基礎(chǔ)類(lèi)型進(jìn)行約束。實(shí)際開(kāi)發(fā)中,我們可能會(huì)定義自己的類(lèi)型:
type MyInt int type MyStr string
那如果在 AddSlice 中使用這兩種類(lèi)型可以編譯通過(guò)嗎?答案是可以的。在泛型草案中,這種情況是無(wú)法編譯通過(guò)的,需要在約束條件中添加~int | ~string,表示底層類(lèi)型是 int 或 string 的類(lèi)型。而在 Go1.17 中,上面的 PlusConstraint 就包括了 int、string、以及以這兩者為底層類(lèi)型的類(lèi)型。
package main
import (
"fmt"
)
type MyInt int
type MyStr string
type PlusConstraint interface {
type int, string
}
func AddSlice[T PlusConstraint](input []T, diff T) []T {
output := make([]T, 0, len(input))
for _, item := range input {
output = append(output, item+diff)
}
return output
}
func main() {
intSlice := []MyInt{1, 2, 3, 4, 5}
fmt.Printf("intSlice [%+v] + 2 = [%v]\n", intSlice, AddSlice(intSlice, 2))
strSlice := []MyStr{"hi,", "hello,", "bye,"}
fmt.Printf("strSlice [%v] + man = [%v]\n", strSlice, AddSlice(strSlice, "man"))
}
//output
//intSlice [[1 2 3 4 5]] + 2 = [[3 4 5 6 7]]
//strSlice [[hi, hello, bye,]] + man = [[hi,man hello,man bye,man]]
2.2. 帶方法的約束 StringConstraint
前面說(shuō)到,約束的定義和接口很像,那如果約束中有方法呢,那不就是妥妥的接口嗎?
兩者還是有區(qū)別的:
- 接口的成員只有方法和內(nèi)嵌的接口類(lèi)型
- 約束的成員有方法、內(nèi)嵌約束類(lèi)型、類(lèi)型(int、string等)
看下面一個(gè)沒(méi)有使用泛型的例子:
package main
import (
"fmt"
)
func ConvertSliceToStrSlice(input []fmt.Stringer) []string {
output := make([]string, 0, len(input))
for _, item := range input {
output = append(output, item.String())
}
return output
}
type MyInt int
func (mi MyInt) String() string {
return fmt.Sprintf("[%d]th", mi)
}
func ConvertIntSliceToStrSlice(input []MyInt) []string {
output := make([]string, 0, len(input))
for _, item := range input {
output = append(output, item.String())
}
return output
}
type MyStr string
func (ms MyStr) String() string {
return string(ms) + "!!!"
}
func ConvertStrSliceToStrSlice(input []MyStr) []string {
output := make([]string, 0, len(input))
for _, item := range input {
output = append(output, item.String())
}
return output
}
func main() {
intSlice := []MyInt{1, 2, 3, 4}
// compile error, []MyInt not match []fmt.Stringer
//fmt.Printf("%v convert %v", intSlice, ConvertSliceToStrSlice(intSlice))
fmt.Printf("%v convertIntToStr %v \n", intSlice, ConvertIntSliceToStrSlice(intSlice))
strSlice := []MyStr{"111", "222", "333"}
fmt.Printf("%v convertStrToStr %v \n", strSlice, ConvertStrSliceToStrSlice(strSlice))
// output
//[[1]th [2]th [3]th [4]th] convertIntToStr [[1]th [2]th [3]th [4]th]
//[111!!! 222!!! 333!!!] convertStrToStr [111!!! 222!!! 333!!!]
}
上面代碼中,MyInt 和 MyStr 都實(shí)現(xiàn)了 fmt.Stringer 接口,但是兩個(gè)都無(wú)法調(diào)用 ConvertSliceToStrSlice 函數(shù),因?yàn)樗娜雲(yún)⑹?[]fmt.Stringer 類(lèi)型,[]MyInt 和它不匹配,這在編譯的時(shí)候就是會(huì)報(bào)錯(cuò)的,而如果我們想要把[]MyInt 轉(zhuǎn)換為 []string,就需要定義一個(gè)入?yún)閇]MyInt 的函數(shù),如 ConvertIntSliceToStrSlice;對(duì)于 []MyStr,則需要另一個(gè)函數(shù)。。。那明明兩者都實(shí)現(xiàn)了 fmt.Stringer,理論上應(yīng)該都可以通過(guò) ConvertSliceToStrSlice 啊,這也太反人類(lèi)了。
哈哈,泛型實(shí)現(xiàn)了這個(gè)功能。
package main
import (
"fmt"
)
type StringConstraint interface {
String() string
}
func ConvertSliceToStrSlice[T StringConstraint](input []T) []string {
output := make([]string, 0, len(input))
for _, item := range input {
output = append(output, item.String())
}
return output
}
type MyInt int
func (mi MyInt) String() string {
return fmt.Sprintf("[%d]th", mi)
}
type MyStr string
func (ms MyStr) String() string {
return string(ms) + "!!!"
}
func main() {
intSlice := []MyInt{1, 2, 3, 4}
// compile error, []MyInt not match []fmt.Stringer
fmt.Printf("%v convert %v\n", intSlice, ConvertSliceToStrSlice(intSlice))
strSlice := []MyStr{"111", "222", "333"}
fmt.Printf("%v convert %v\n", strSlice, ConvertSliceToStrSlice(strSlice))
// output
//[[1]th [2]th [3]th [4]th] convert [[1]th [2]th [3]th [4]th]
//[111!!! 222!!! 333!!!] convert [111!!! 222!!! 333!!!]
}
簡(jiǎn)單吧,在 StringConstraint 約束中定義一個(gè) String() string,這樣只要有這個(gè)方法的類(lèi)型都可以作為 T 在 ConvertSliceToStrSlice 使用。在這個(gè)約束條件下,所有具有 String() string 方法的類(lèi)型都可以進(jìn)行轉(zhuǎn)換,但是我們?nèi)绻氚鸭s束條件定的更加苛刻,例如只有底層類(lèi)型為 int 或者 string 的類(lèi)型才可以調(diào)用這個(gè)函數(shù)。 那么我們可以進(jìn)一步在 StringConstraint 中添加約束條件:
type StringConstraint interface {
type int, string
String() string
}
這樣滿(mǎn)足這個(gè)約束的類(lèi)型集合就是底層類(lèi)型是 int 或者 string,并且,具有 String() string 方法的類(lèi)型。而這個(gè)類(lèi)型集合就是 type int, string 的類(lèi)型集合與 String() string 的類(lèi)型集合的交集。具體的概念后續(xù)介紹。
這樣,MyFloat、MyUint 就無(wú)法調(diào)用 ConvertSliceToStrSlice 這個(gè)函數(shù)了。
package main
import (
"fmt"
)
type StringConstraint interface {
type int, string
String() string
}
func ConvertSliceToStrSlice[T StringConstraint](input []T) []string {
output := make([]string, 0, len(input))
for _, item := range input {
output = append(output, item.String())
}
return output
}
type MyFloat float64
func (mf MyFloat) String() string {
return fmt.Sprintf("%fth", mf)
}
type MyInt int
func (mi MyInt) String() string {
return fmt.Sprintf("[%d]th", mi)
}
type MyStr string
func (ms MyStr) String() string {
return string(ms) + "!!!"
}
func main() {
intSlice := []MyInt{1, 2, 3, 4}
// compile error, []MyInt not match []fmt.Stringer
fmt.Printf("%v convert %v\n", intSlice, ConvertSliceToStrSlice(intSlice))
strSlice := []MyStr{"111", "222", "333"}
fmt.Printf("%v convert %v\n", strSlice, ConvertSliceToStrSlice(strSlice))
// output
//[[1]th [2]th [3]th [4]th] convert [[1]th [2]th [3]th [4]th]
//[111!!! 222!!! 333!!!] convert [111!!! 222!!! 333!!!]
floatSlice := []MyFloat{1.1, 2.2, 3.3}
//type checking failed for main
//prog.go2:48:44: MyFloat does not satisfy StringConstraint (MyFloat or float64 not found in int, string)
fmt.Printf("%v convert %v\n", floatSlice, ConvertSliceToStrSlice(floatSlice))
}
小結(jié):
總的來(lái)說(shuō),泛型可以簡(jiǎn)化代碼的編寫(xiě),同時(shí)在編譯時(shí)進(jìn)行類(lèi)型檢查,如果類(lèi)型不滿(mǎn)足約束,就會(huì)在編譯時(shí)報(bào)錯(cuò);這樣就避免了運(yùn)行時(shí)不可控的錯(cuò)誤了。
到此這篇關(guān)于Go 泛型和非泛型代碼詳解的文章就介紹到這了,更多相關(guān)Go 泛型和非泛型代碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang通道阻塞情況與通道無(wú)阻塞實(shí)現(xiàn)小結(jié)
本文主要介紹了Golang通道阻塞情況與通道無(wú)阻塞實(shí)現(xiàn)小結(jié),詳細(xì)解析了通道的類(lèi)型、操作方法以及垃圾回收機(jī)制,從基礎(chǔ)概念到高級(jí)應(yīng)用,具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03
golang中cache組件的使用及groupcache源碼解析
本篇主要解析groupcache源碼中的關(guān)鍵部分, lru的定義以及如何做到同一個(gè)key只加載一次。緩存填充以及加載抑制的實(shí)現(xiàn)方法,本文重點(diǎn)給大家介紹golang中cache組件的使用及groupcache源碼解析,感興趣的朋友一起看看吧2021-06-06
使用gin框架搭建簡(jiǎn)易服務(wù)的實(shí)現(xiàn)方法
go語(yǔ)言web框架挺多的,本文就介紹了一下如何使用gin框架搭建簡(jiǎn)易服務(wù)的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
golang調(diào)用shell命令(實(shí)時(shí)輸出,終止)
本文主要介紹了golang調(diào)用shell命令(實(shí)時(shí)輸出,終止),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
go語(yǔ)言中fallthrough的用法說(shuō)明
這篇文章主要介紹了go語(yǔ)言中fallthrough的用法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-05-05
詳解Go語(yǔ)言中自定義結(jié)構(gòu)體能作為map的key嗎
在Go中,引用類(lèi)型具有動(dòng)態(tài)的特性,可能會(huì)被修改或指向新的數(shù)據(jù),這就引發(fā)了一個(gè)問(wèn)題—能否將包含引用類(lèi)型的自定義結(jié)構(gòu)體作為map的鍵呢,本文就來(lái)和大家想想講講2023-06-06

