Go 泛型和非泛型代碼詳解
1. 開啟泛型
在 Go1.17 版本中,可以通過:
export GOFLAGS="-gcflags=-G=3"
或者在編譯運行程序時加上:
go run -gcflags=-G=3 main.go
2.無泛型代碼和泛型代碼
2.1. AddSlice
首先看現在沒有泛型的代碼:
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]]
上面沒有使用泛型的代碼中,對 intSlice
和 strSlice
,需要構造兩個函數對它們進行處理;而如果後續還有 float64
、uint32
等類型就需要更多地 Add...Slice
函數。
而如果使用泛型之後,這些 Add...Slice
函數就可以合並為一個函數瞭,在這個函數中,對那些可以使用 + 操作符的類型進行加操作(無論是數學的加還是字符串的連接)。
泛型代碼如下:
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]]
是不是超級簡單,但是 AddSlice
函數中引入瞭約束的概念,即 PlusConstraint
。AddSlice
的方括號中是類型參數,T 就是這個類型參數的形參,後面的 PlusConstraint
就是 T 的約束條件,意思是隻有滿足約束條件的 T 類型才可以在這個函數中使用。
AddSlice
後面圓括號中的參數是常規參數也稱為非類型參數,它們可以不制定具體類型(int、string 等),可以使用 T 來代替。
而在 AddSlice
中,對於 T 類型的值 item,它會將 item
和 diff 進行 + 操作,可能是數學上的累加,也可能是字符串的連接。
那現在你可能要問瞭,T 類型就一定是支持 + 操作符的嗎,有沒有可能是一個 struct
呢?
答案是:不可能。
前面說過,隻有滿足約束條件的 T 才可以在 AddSlice
中使用,而約束條件就是上面的 PlusConstraint
。
PlusConstraint
定義的方式和接口類型的定義是一樣的,隻不過內部多瞭一行:
type int, string
這句話就是說,隻有 int
、string
這兩個類型才滿足這個約束,這裡涉及到類型集的概念,後續會提到。
因此,有瞭這個約束條件,傳入到 AddSlice
的參數 input
和 diff
都是可以使用 + 操作符的。如果你的 AddSlice
函數中想傳入 float46
、uint64
等類型,就在 PlusConstraint
中加上這兩個類型即可。
上面的代碼中,隻是對 int 和 string
兩種基礎類型進行約束。實際開發中,我們可能會定義自己的類型:
type MyInt int type MyStr string
那如果在 AddSlice
中使用這兩種類型可以編譯通過嗎?答案是可以的。在泛型草案中,這種情況是無法編譯通過的,需要在約束條件中添加~int | ~string
,表示底層類型是 int 或 string
的類型。而在 Go1.17 中,上面的 PlusConstraint
就包括瞭 int
、string
、以及以這兩者為底層類型的類型。
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
前面說到,約束的定義和接口很像,那如果約束中有方法呢,那不就是妥妥的接口嗎?
兩者還是有區別的:
- 接口的成員隻有方法和內嵌的接口類型
- 約束的成員有方法、內嵌約束類型、類型(int、string等)
看下面一個沒有使用泛型的例子:
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
都實現瞭 fmt.Stringer
接口,但是兩個都無法調用 ConvertSliceToStrSlice
函數,因為它的入參是 []fmt.Stringer 類型,[]MyInt 和它不匹配,這在編譯的時候就是會報錯的,而如果我們想要把[]MyInt 轉換為 []string,就需要定義一個入參為[]MyInt 的函數,如 ConvertIntSliceToStrSlice
;對於 []MyStr,則需要另一個函數。。。那明明兩者都實現瞭 fmt.Stringer
,理論上應該都可以通過 ConvertSliceToStrSlice
啊,這也太反人類瞭。
哈哈,泛型實現瞭這個功能。
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!!!] }
簡單吧,在 StringConstraint
約束中定義一個 String() string
,這樣隻要有這個方法的類型都可以作為 T 在 ConvertSliceToStrSlice
使用。在這個約束條件下,所有具有 String() string
方法的類型都可以進行轉換,但是我們如果想把約束條件定的更加苛刻,例如隻有底層類型為 int 或者 string 的類型才可以調用這個函數。 那麼我們可以進一步在 StringConstraint
中添加約束條件:
type StringConstraint interface { type int, string String() string }
這樣滿足這個約束的類型集合就是底層類型是 int 或者 string
,並且,具有 String() string
方法的類型。而這個類型集合就是 type int
, string
的類型集合與 String() string
的類型集合的交集。具體的概念後續介紹。
這樣,MyFloat
、MyUint
就無法調用 ConvertSliceToStrSlice
這個函數瞭。
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)) }
小結:
總的來說,泛型可以簡化代碼的編寫,同時在編譯時進行類型檢查,如果類型不滿足約束,就會在編譯時報錯;這樣就避免瞭運行時不可控的錯誤瞭。
到此這篇關於Go 泛型和非泛型代碼詳解的文章就介紹到這瞭,更多相關Go 泛型和非泛型代碼內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!