go泛型教程

科科人神發表於2022-03-07

泛型

原文
線上閱讀

導讀:

  • 約束
  • 使用方法
  • 實現原理
  • 跟其它語言的泛型進行對比
  • 用例子學泛型
  • issues

泛型需滿足 go1.18+

約束

go使用interface作為約束,約束的意思是約束了這個泛型都具有哪些實際型別。所以可以理解為,go將interface的職責給擴充套件了,讓介面不僅僅作為介面 --- 解耦的,抽象化的結構體,還具有了約束,對於型別的約束作用。

type st interface{
  int | string
}

這裡 st約束擁有int和string,請注意這裡的st是約束,不是泛型型別

go內建了很多約束,比如說 any 和 comparable ,意思是任何型別和可以比較的型別。以後應該會有一個新的內建約束的包叫做package constraints 例如any comparable ,Ordered 等等約束都會內建到標準庫中

約束不僅僅可以單獨寫出來,還可以內建於函式內部。

func Age[T int| string,B float64| string](i T,j B){}

這種形式下,T 和 B 的約束就是僅限此函式使用

下面我們看一種形式,這種情況下約束的不僅僅是string和int,而是包含了底層是他們的所有資料,比如說 type DD int 也符合這個約束,請記住只能使用在底層型別上,如果使用~DD是不被允許的

type st interface{
	~string | ~int
}

於此同時,約束也不僅僅是基礎型別,約束的內容是方法也是可以的


func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
	r := make([]string, len(s))
	for i, v := range s {
		r[i] = p[i].Plus(v.String())
	}
	return r
}

type Plusser interface {
	Plus(string) string
}
type Stringer interface {
	String() string
}

所有說這裡就可以看出來,在引入泛型之後,go的interface的功能擴充了。

約束跟介面是一樣的也是可以巢狀的

type ComparableHasher interface {
	comparable
	Hash() uintptr
}

// or

type ImpossibleConstraint interface {
	comparable
	[]int
}

這裡的意義就是 and的意思 就是說這個約束是可以比較的還是必須得支援hash()uintptr

下面這種方法也是可以的

type NumericAbs[T any] interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64 |
		~complex64 | ~complex128
	Abs() T
}

上面的型別意思是滿足數字型別,下面的意思是滿足這個方法,所以最終實現這個約束,就是一個物件是數字型別,並且實現了這個介面

那麼這裡有一個疑問,給約束嵌入泛型,應該如何操作

type EmbeddedParameter[T any] interface {
	T 
}

cannot embed a type parameter 這種方法,go不允許這麼做。因為T已經是泛型了,而約束裡的型別應該是實際型別,所以T不能這麼用,

不過如果約束裡面是方法就可以這麼做,這是因為T 這裡只是方法的一個引數,比如說

type EmbeddedParameter[T any] interface {
	~int | ~uint 
	me() T 

如果想使用這種約束,可以這麼使用

func Abs[T EmbeddedParameter[T]](t T)T{}

解釋一下,中括號裡面泛型的兩個T表達的意思是不一樣的,後面的T表達的是 約束裡的泛型,表示 any,前面的T表示的是滿足後面的這個約束的型別T,但是這裡注意,後面T雖然之前定義的時候是any但是這裡被改變了,改變為了必須滿足約束 EmbeddedParameter的型別,如果說的通俗點,從any變成了,滿足 int | uint and 實現 me()T方法 後文會有程式碼進行解釋。

當然了,後面的T沒有也行,如果沒有後面的T就是相當於不改變後面的T的約束型別了

type Differ[T1 any] interface {
	Diff(T1) int
}

func IsClose[T2 Differ](a, b T2) bool {
	return a.Diff(b) < 2
}

當結構體中使用泛型的時候,泛型可以直接作為嵌入使用

type Lockable[T any] struct {
	T
	mu sync.Mutex
}

請注意,type a[T any] interface 這種寫法有可能在go1.18還不支援

當使用了泛型之後,是無法使用斷言的,這是非法的,那麼如果一定要在執行時的時候去判斷型別怎麼辦呢?答案就是轉變成interface{}即可,因為我們知道任何物件都已經實現了空介面,那麼就可以被空介面去轉化

func GeneralAbsDifference[T Numeric](a, b T) T {
	switch (interface{})(a).(type) {
	case int, int8, int16, int32, int64,
		uint, uint8, uint16, uint32, uint64, uintptr,
		float32, float64:
		return OrderedAbsDifference(a, b) 
	case complex64, complex128:
		return ComplexAbsDifference(a, b) 
	}
}

下面看一下別名的真實型別是泛型的情況

type A[T any] []T

type AliasA = A // 錯誤 ❌

type AliasA = A[int] // 正確

其中錯誤的問題是 別名不能直接使用泛型型別 cannot use generic type A[T any] without instantiation,它需要泛型的例項化

使用方法

下面展示一下go泛型的基本使用方法

package main

import "fmt"

func main() {
	fmt.Printf("%v",Age[int](12))
}

func Age[T any](t T) T{
	return t
}

這是函式使用泛型的寫法,當函式使用泛型的時候,需要在變數前使用中括號標註泛型的具體約束,然後後面才能使用這個泛型型別,使用泛型函式的時候,中括號是可以省略的Age(12) 系統會自動推算泛型的具體實現。順便說一下,泛型型別使用%v作為佔位符,泛型型別無法進行斷言,這一點跟interface{}不同。

當然了,我麼也可以不用any,自定義一個約束

package main

import "fmt"

func main() {
	Age[int](12)
}
type st interface{
  int | string
}
func Age[T st](t T) {
	fmt.Println((t))
}

看完了在函式內的泛型,我們在看一下在方法中如何使用泛型

package main

import "fmt"

func main() {
	new(Age[int]).Post(12)

	var dd DD[int]
	dd.TT(12)
}

type Age[T any] struct {
	I T
}

func (a *Age[T]) Post(t T) {
	fmt.Println(a.I, t)
}

type DD[T any] []T

func(dd *DD[T])TT(t T){
	fmt.Println(t,len(*dd))
}

在 age 結構體宣告的時候,宣告瞭一個泛型 T ,在struct體內就可以使用這個T,值得注意的是,這個結構體方法內部僅可以使用定義在這個結構體物件上的泛型

下面是一個錯誤案例

func (a *Age[T])Post[B any](t T,b B) {
	fmt.Println(a.I, t)
} 

syntax error: method must have no type parameters

接下來我們看一下,如何使用 type a[T any] interface{} 有型別也有方法的泛型結構

package main

import "fmt"

func main() {
	var d DDD
	var i DDD
	d = 1
	i = 2
	io := AbsDifference[DDD](d, i)
	fmt.Println(io)
}

type DDD int

func (ddd DDD) Abs() DDD {
	return ddd + ddd
}

type NumericAbs[T any] interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64 |
		~complex64 | ~complex128
	Abs() T
}

// AbsDifference computes the absolute value of the difference of
// a and b, where the absolute value is determined by the Abs method.
func AbsDifference[T NumericAbs[T]](a, b T) T {
	d := a - b
	return d.Abs()
}

實現原理

泛型的第一種方法是在編譯這個泛型時,使用一個字典,裡面包含了這個泛型函式的全部型別資訊,然後當執行時,使用函式例項化的時候從這個字典中取出資訊進行例項化即可,這種方法會導致執行效能下降,一個例項化型別int, x=y可能通過暫存器複製就可以了,但是泛型必須通過記憶體了(因為需要map進行賦值),不過好處就是不浪費空間

還有一種方法就是把這個泛型的所有型別全部提前生成,這種方法也有一個巨大的缺點就是程式碼量直線上升,如果是一個包的情況下還能根據具體的函式呼叫去實現該實現的型別,如果是包輸出的的情況下,那麼就得不得不生成所有的型別。

所以將兩者結合在一起或許是最好的選擇。

這種方法是這樣的,如果型別的記憶體分配器/垃圾回收器呈現的方式一致的情況下,只給它生成一份程式碼,然後給它一個字典來區分不同的具體行為,可以最大限度的平衡速度和體積

跟其它語言的泛型進行對比

  • c語言:本身不具有泛型,需要程式設計師去實現一個泛型,實現複雜,但是不增加語言的複雜度(換言之只增加了程式設計師的)
  • c++和rust:跟go基本保持一種方式,就是增加編譯器的工作量
  • Java:將泛型裝箱為object,在裝箱和拆箱擦除型別的過程中,程式執行效率會變低

用例子學泛型

理論學習完了,不使用例子進行復習的話會忘的很快的。跟著我看幾個例子吧

例子一: 函式泛型 map-filter-reduce

package main

import (
	"fmt"
)

func main() {
	vM := Map[int]([]int{1, 2, 3, 4, 5}, func(i int) int {

		return i + i
	})
	fmt.Printf("map的結果是:%v", vM)
	vF := Filter[int]([]int{1, 2, 3, 4, 5}, func(t int) bool {
		if t > 2 {
			return true
		}
		return false
	})
	fmt.Printf("filter的結果是:%v", vF)
	vR := Reduce[Value, *Result]([]Value{
		{name: "tt", year: 1},
		{name: "bb", year: 2},
		{name: "7i", year: 3},
		{name: "8i", year: 4},
		{name: "u4i", year: 5},
		{name: "uei", year: 6},
		{name: "uwi", year: 7},
		{name: "uti", year: 8},
	}, &Result{}, func(r *Result, v Value) *Result {
		r.value = r.value + v.year
		return r
	})
	fmt.Println("reduce的結果是:", vR.value)

}

// Map:類似於洗菜,進去的菜和出來的菜不一樣了所以需要兩種種類
func Map[T1, T2 any](arr []T1, f func(T1) T2) []T2 {
	result := make([]T2, len(arr))
	for k, v := range arr {
		result[k] = f(v)
	}
	return result
}

// Filter:類似於摘菜,進去的菜和出來的菜是一種,不過量減少了
func Filter[T any](arr []T, f func(T) bool) []T {
	var result []T
	for _, v := range arr {
		if f(v) {
			result = append(result, v)
		}
	}
	return result
}

// Reduce:類似於做菜,將菜做成一道料理,所以需要兩種型別
func Reduce[T1, T2 any](arr []T1, zero T2, f func(T2, T1) T2) T2 {
	result := zero
	for _, v := range arr {
		result = f(result, v)
	}
	return result
}

type Value struct {
	name string
	year int
}
type Result struct {
	value int
}

map的結果是:[2 4 6 8 10] filter的結果是:[3 4 5] reduce的結果是: 36

例子二: 方法上的泛型 sets

package main

import (
	"fmt"
)

func main() {

	// 這裡 Sets的具體型別和Make的具體型別都是int,所以可以正常賦值
	var s Sets[int] = Make[int]()
	//
	s.Add(1)
	s.Add(2)
	fmt.Println(s)
	fmt.Println(s.Contains(3))
	fmt.Println(s.Len())
	s.Iterate(func(i int) {
		fmt.Println(i)
	})
	fmt.Println(s)
	s.Delete(2)
	fmt.Println(s)
}

// Sets 一個key  儲存物件
type Sets[T comparable] map[T]struct{}

// Make 例項化一個map
func Make[D comparable]() Sets[D] {
	// 泛型就像一個管道一樣,只要例項化的時候管子裡的東西一致,那麼就是一根管子
	return make(Sets[D])
}

// Add 向這個sets新增內容
func (s Sets[T]) Add(t T) {
	s[t] = struct{}{}
}

// delete ,從這個sets中刪除內容
func (s Sets[T]) Delete(t T) {
	delete(s, t)
}

//  Contains 播報t是否屬於這個sets
func (s Sets[T]) Contains(t T) bool {
	_, ok := s[t]
	return ok
}

//Len sets擁有的長度

func (s Sets[T]) Len() int {
	return len(s)
}

// Iterate 迭代器,並且給予每個元素功能

func (s Sets[T]) Iterate(f func(T)) {
	for k := range s {
		f(k)
	}
}

map[1:{} 2:{}] false 2 1 2 map[1:{} 2:{}] map[1:{}]

例子三: 外部定義的約束 實現一個sort介面型別

package main

import "fmt"

func main() {
	fmt.Println("Hello, 世界")
}
// ~ 代表只要底層滿足這些型別也可以算滿足約束
type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uintptr |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
		~float32 | ~float64 | ~string
}
type orderedSlice[T Ordered] []T

func (s orderedSlice[T]) Len() int           { return len(s) }
func (s orderedSlice[T]) Less(i, j int) bool { return s[i] < s[j] }
func (s orderedSlice[T]) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func OrderedSlice[T Ordered](s []T) {
	sort.Sort(orderedSlice[T](s))
}

issues

問題一: 關於泛型中的零值

在go裡面對泛型的零值並沒有一個所謂的泛型零值可以使用,需要根據不同的實踐去實現,比如

package main

import "fmt"

func main() {
	
}

type Aget[T any] struct {
	t *T
}
// 根據實際判斷,如果a的t不等於nil再返回,如果是nil就返回一個T型別的nil(意思就是隻宣告)
func (a *Aget[T]) Approach() T {
	if a.t != nil { 
		return *a.t
	}
	var r T
	return r
}

實際上目前(1.18還沒釋出),還沒一個確切的泛型的零值,那麼我們要做的只能是按照實際來具體分析,按照提案,以後有可能使用return ... return _ return return nil return T{} 這些都是可能的結果,我個人比較喜歡 return T{} 來表示泛型的零值,或許在go1.19或者go2的時候能實現,拭目以待吧。

問題二: 無法識別使用了底層資料的其它型別

type Float interface {
	~float32 | ~float64
}

func NewtonSqrt[T Float](v T) T {
	var iterations int
	switch (interface{})(v).(type) {
	case float32:
		iterations = 4
	case float64:
		iterations = 5
	default:
		panic(fmt.Sprintf("unexpected type %T", v))
	}
	// Code omitted.
}

type MyFloat float32

var G = NewtonSqrt(MyFloat(64))

這裡約束 Float擁有的約束型別是 ~float32float64當在switch中定義了float32和flaot64時,無法識別下面的新型別 MyFloat即使它的底層時 float32 ,go的提議是以後在switch中使用 case ~float32: 來解決這個問題,目前尚未解決這個問題

問題三: 即便約束一致,型別也是不同的

func Copy[T1, T2 any](dst []T1, src []T2) int {
	for i, x := range src {
		if i > len(dst) {
			return i
		}
		dst[i] = T1(x) // x 是 T2型別 不能直接轉化為 T1型別
	}
	return len(src)
}

T1,和T2 雖然都是any的約束,但是啊,它不是一個型別啊!

Copy[int,string]() // 這種情況下,你能說可以直接轉化嗎???

這種程式碼可以更改一下

dst[i]= (interface{})(x).(T1)

確認是一種型別以後才能轉化

參考資料

相關文章