go 泛型教程

泛型

本文原文
在線閱讀

導(dǎo)讀:

  • 約束
  • 使用方法
  • 實(shí)現(xiàn)原理
  • 跟其它語言的泛型進(jìn)行對比
  • 用例子學(xué)泛型
  • issues

泛型需滿足 go1.18+

約束

go使用interface作為約束,約束的意思是約束了這個泛型都具有哪些實(shí)際類型。所以可以理解為,go將interface的職責(zé)給擴(kuò)展了,讓接口不僅僅作為接口 --- 解耦的,抽象化的結(jié)構(gòu)體,還具有了約束,對于類型的約束作用。

type st interface{
  int | string
}

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

go內(nèi)置了很多約束,比如說 any 和 comparable ,意思是任何類型和可以比較的類型。以后應(yīng)該會有一個新的內(nèi)置約束的包叫做package constraints 例如any comparable ,Ordered 等等約束都會內(nèi)置到標(biāo)準(zhǔn)庫中

約束不僅僅可以單獨(dú)寫出來,還可以內(nèi)置于函數(shù)內(nèi)部。

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

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

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

type st interface{
    ~string | ~int
}

于此同時,約束也不僅僅是基礎(chǔ)類型,約束的內(nèi)容是方法也是可以的


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的功能擴(kuò)充了。

約束跟接口是一樣的也是可以嵌套的

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
}

上面的類型意思是滿足數(shù)字類型,下面的意思是滿足這個方法,所以最終實(shí)現(xiàn)這個約束,就是一個對象是數(shù)字類型,并且實(shí)現(xiàn)了這個接口

那么這里有一個疑問,給約束嵌入泛型,應(yīng)該如何操作

type EmbeddedParameter[T any] interface {
    T 
}

cannot embed a type parameter 這種方法,go不允許這么做。因?yàn)門已經(jīng)是泛型了,而約束里的類型應(yīng)該是實(shí)際類型,所以T不能這么用,

不過如果約束里面是方法就可以這么做,這是因?yàn)門 這里只是方法的一個參數(shù),比如說

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

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

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

解釋一下,中括號里面泛型的兩個T表達(dá)的意思是不一樣的,后面的T表達(dá)的是 約束里的泛型,表示 any,前面的T表示的是滿足后面的這個約束的類型T,但是這里注意,后面T雖然之前定義的時候是any但是這里被改變了,改變?yōu)榱吮仨殱M足約束 EmbeddedParameter的類型,如果說的通俗點(diǎn),從any變成了,滿足 int | uint and 實(shí)現(xiàn) me()T方法后文會有代碼進(jìn)行解釋。

當(dāng)然了,后面的T沒有也行,如果沒有后面的T就是相當(dāng)于不改變后面的T的約束類型了

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

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

當(dāng)結(jié)構(gòu)體中使用泛型的時候,泛型可以直接作為嵌入使用

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

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

當(dāng)使用了泛型之后,是無法使用斷言的,這是非法的,那么如果一定要在運(yùn)行時的時候去判斷類型怎么辦呢?答案就是轉(zhuǎn)變成interface{}即可,因?yàn)槲覀冎廊魏螌ο蠖家呀?jīng)實(shí)現(xiàn)了空接口,那么就可以被空接口去轉(zhuǎn)化

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) 
    }
}

下面看一下別名的真實(shí)類型是泛型的情況

type A[T any] []T

type AliasA = A // 錯誤 ?

type AliasA = A[int] // 正確

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

使用方法

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

package main

import "fmt"

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

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

這是函數(shù)使用泛型的寫法,當(dāng)函數(shù)使用泛型的時候,需要在變量前使用中括號標(biāo)注泛型的具體約束,然后后面才能使用這個泛型類型,使用泛型函數(shù)的時候,中括號是可以省略的Age(12) 系統(tǒng)會自動推算泛型的具體實(shí)現(xiàn)。順便說一下,泛型類型使用%v作為占位符,泛型類型無法進(jìn)行斷言,這一點(diǎn)跟interface{}不同。

當(dāng)然了,我么也可以不用any,自定義一個約束

package main

import "fmt"

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

看完了在函數(shù)內(nèi)的泛型,我們在看一下在方法中如何使用泛型

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 結(jié)構(gòu)體聲明的時候,聲明了一個泛型 T ,在struct體內(nèi)就可以使用這個T,值得注意的是,這個結(jié)構(gòu)體方法內(nèi)部僅可以使用定義在這個結(jié)構(gòu)體對象上的泛型

下面是一個錯誤案例

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{} 有類型也有方法的泛型結(jié)構(gòu)

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()
}

實(shí)現(xiàn)原理

泛型的第一種方法是在編譯這個泛型時,使用一個字典,里面包含了這個泛型函數(shù)的全部類型信息,然后當(dāng)運(yùn)行時,使用函數(shù)實(shí)例化的時候從這個字典中取出信息進(jìn)行實(shí)例化即可,這種方法會導(dǎo)致執(zhí)行性能下降,一個實(shí)例化類型int, x=y可能通過寄存器復(fù)制就可以了,但是泛型必須通過內(nèi)存了(因?yàn)樾枰猰ap進(jìn)行賦值),不過好處就是不浪費(fèi)空間

還有一種方法就是把這個泛型的所有類型全部提前生成,這種方法也有一個巨大的缺點(diǎn)就是代碼量直線上升,如果是一個包的情況下還能根據(jù)具體的函數(shù)調(diào)用去實(shí)現(xiàn)該實(shí)現(xiàn)的類型,如果是包輸出的的情況下,那么就得不得不生成所有的類型。

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

這種方法是這樣的,如果類型的內(nèi)存分配器/垃圾回收器呈現(xiàn)的方式一致的情況下,只給它生成一份代碼,然后給它一個字典來區(qū)分不同的具體行為,可以最大限度的平衡速度和體積

跟其它語言的泛型進(jìn)行對比

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

用例子學(xué)泛型

理論學(xué)習(xí)完了,不使用例子進(jìn)行復(fù)習(xí)的話會忘的很快的。跟著我看幾個例子吧

例子一: 函數(shù)泛型 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的結(jié)果是:%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的結(jié)果是:%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的結(jié)果是:", vR.value)

}

// Map:類似于洗菜,進(jìn)去的菜和出來的菜不一樣了所以需要兩種種類
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:類似于摘菜,進(jìn)去的菜和出來的菜是一種,不過量減少了
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的結(jié)果是:[2 4 6 8 10] filter的結(jié)果是:[3 4 5] reduce的結(jié)果是: 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 實(shí)例化一個map
func Make[D comparable]() Sets[D] {
    // 泛型就像一個管道一樣,只要實(shí)例化的時候管子里的東西一致,那么就是一根管子
    return make(Sets[D])
}

// Add 向這個sets添加內(nèi)容
func (s Sets[T]) Add(t T) {
    s[t] = struct{}{}
}

// delete ,從這個sets中刪除內(nèi)容
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:{}]

例子三: 外部定義的約束 實(shí)現(xiàn)一個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

問題一: 關(guān)于泛型中的零值

在go里面對泛型的零值并沒有一個所謂的泛型零值可以使用,需要根據(jù)不同的實(shí)踐去實(shí)現(xiàn),比如

package main

import "fmt"

func main() {
    
}

type Aget[T any] struct {
    t *T
}
// 根據(jù)實(shí)際判斷,如果a的t不等于nil再返回,如果是nil就返回一個T類型的nil(意思就是只聲明)
func (a *Aget[T]) Approach() T {
    if a.t != nil { 
        return *a.t
    }
    var r T
    return r
}

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

問題二: 無法識別使用了底層數(shù)據(jù)的其它類型

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當(dāng)在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類型 不能直接轉(zhuǎn)化為 T1類型
    }
    return len(src)
}

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

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

這種代碼可以更改一下

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

確認(rèn)是一種類型以后才能轉(zhuǎn)化

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 泛型,即 "參數(shù)化類型",將類型參數(shù)化,可以用在類,接口,方法上。 與 Java 一樣,Kotlin 也提供泛型,...
    安卓技術(shù)磚家閱讀 208評論 0 1
  • 變化 該草案是一份變化文件,這意味著這些提案需要隨著時間的推移而改變。本節(jié)記錄此提案何時發(fā)生更改。 2020/08...
    相思胡楊閱讀 391評論 0 1
  • 寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kot...
    胡奚冰閱讀 1,550評論 1 3
  • 在 Go1.17 中,千呼萬喚的泛型終于出來了,但又沒有完全出來。在 Go1.17 的發(fā)布文檔中,并沒有提到泛型,...
    rayjun閱讀 5,203評論 0 5
  • 建議先閱讀我的上一篇文章 -- Java 泛型 和 Java 泛型一樣,Kotlin 泛型也是 Kotlin 語言...
    JohnnyShieh閱讀 6,630評論 1 26

友情鏈接更多精彩內(nèi)容