Go新泛型設計方案詳解

Go官博今晨發表了Go核心團隊兩位大神Ian Lance Taylor和Go語言之父之一的Robert Griesemer撰寫的文章“The Next Step for Generics”,該文介紹了Go泛型(Go Generics)的最新進展和未來計劃。

2019年中旬,在Go 1.13版本發佈前夕的GopherCon 2019大會上,Ian Lance Taylor代表Go核心團隊做了有關Go泛型進展的介紹。自那以後,Go團隊對原先的Go Generics技術草案做了進一步精化,並編寫了相關工具讓社區gopher體驗滿足這份設計的Go generics語法,返回建議和意見。經過一年多的思考、討論、反饋與實踐,Go核心團隊決定在這份舊設計的基礎上另起爐竈,撰寫了一份Go Generics的新技術提案:“Type Parameters”。與上一份提案最大的不同在於使用擴展的interface類型替代“Contract”用於對類型參數的約束。

parametric polymorphism((形式)參數多態)是Go此版泛型設計的基本思想。和Go設計思想一致,這種參數多態並不是通過像面嚮對象語言那種子類型的層次體系實現的,而是通過顯式定義結構化的約束實現的。基於這種設計思想,該設計不支持模板元編程(template metaprogramming)和編譯期運算。

注意:雖然都稱爲泛型(generics),但是Go中的泛型(generics)僅是用於狹義地表達帶有類型參數(type parameter)的函數或類型,這與其他編程語言中的泛型(generics)在含義上有相似性,但不完全相同。

從目前的情況來看,該版設計十分接近於最終接受的方案,因此作爲Go語言鼓吹者這裏就和大家一起看看最早將於Go 1.17版本(2021年8月)中加入的Go泛型支持究竟是什麼樣子的。由於目前關於Go泛型的資料僅限於這份設計文檔以及一些關於這份設計的討論貼,本文內容均來自這些資料。另外最終加入Go的泛型很可能與目前設計文檔中提到的有所差異,請各位小夥伴們瞭解。

1. 通過爲type和function增加類型參數(type parameters)的方式實現泛型

Go的泛型主要體現在類型和函數的定義上。

泛型函數(generic function)

Go提案中將帶有類型參數(type parameters)的函數稱爲泛型函數,比如:

func PrintSlice(type T)(s []T) {
    for _, v := range s {
        fmt.Printf("%v ", v)
    }
    fmt.Print("\n")
}

其中,函數名PrintSlice與函數參數列表之間的type T即爲類型參數列表。顧名思義,該函數用於打印元素類型爲T的切片中的所有元素。使用該函數的時候,除了要傳入要打印的切片實參外,還需要爲類型參數傳入實參(一個類型名),這個過程稱爲泛型函數的實例化。見下面例子:

// https://go2goplay.golang.org/p/rDbio9c4AQI
package main


import "fmt"


func PrintSlice(type T)(s []T) {
    for _, v := range s {
        fmt.Printf("%v ", v)
    }
    fmt.Print("\n")
}


func main() {
    PrintSlice(int)([]int{1, 2, 3, 4, 5})
    PrintSlice(float64)([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
    PrintSlice(string)([]string{"one", "two", "three", "four", "five"})
}

運行該示例:

1 2 3 4 5 
1.01 2.02 3.03 4.04 5.05 
one two three four five

但是這種每次都顯式指定類型參數實參的使用方式顯然有些複雜繁瑣,給開發人員帶來心智負擔和不好的體驗。Go編譯器是聰明的,大多數使用泛型函數的場景下,編譯器都會根據函數參數列表傳入的實參類型自動推導出類型參數的實參類型(type inference)。比如將上面例子改爲下面這樣,程序依然可以輸出正確的結果。

// https://go2goplay.golang.org/p/UgHqZ7g4rbo
package main


import "fmt"


func PrintSlice(type T)(s []T) {
    for _, v := range s {
        fmt.Printf("%v ", v)
    }
    fmt.Print("\n")
}


func main() {
    PrintSlice([]int{1, 2, 3, 4, 5})
    PrintSlice([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
    PrintSlice([]string{"one", "two", "three", "four", "five"})
}

泛型類型(generic type)

Go提案中將帶有類型參數(type parameters)的類型定義稱爲泛型類型,比如我們定義一個底層類型爲切片類型的新類型:Vector:

type Vector(type T) []T

該Vector(切片)類型中的元素類型爲T。和泛型函數一樣,使用泛型類型時,我們首先要對其進行實例化,即顯式爲類型參數賦一個實參值(一個類型名):

//https://go2goplay.golang.org/p/tIZN2if1Wxo


package main


import "fmt"


func PrintSlice(type T)(s []T) {
    for _, v := range s {
        fmt.Printf("%v ", v)
    }
    fmt.Print("\n")
}


type Vector(type T) []T


func main() {
    var vs = Vector(int){1, 2, 3, 4, 5}
    PrintSlice(vs)
}

泛型類型的實例化是必須顯式爲類型參數傳參的,編譯器無法自行做類型推導。如果將上面例子中main函數改爲如下實現方式:

func main() {
    var vs = Vector{1, 2, 3, 4, 5}
    PrintSlice(vs)
}

則Go編譯器會報如下錯誤:

type checking failed for main
prog.go2:15:11: cannot use generic type Vector(type T) without instantiation

這個錯誤的意思就是:未實例化(instantiation)的泛型類型Vector(type T)無法使用。

2. 通過擴展了的interface類型對類型參數進行約束和限制

1) 對泛型函數中類型參數的約束與限制

有了泛型函數,我們來實現一個“萬能”加法函數:

// https://go2goplay.golang.org/p/t0vXI6heUrT
package main


import "fmt"


func Add(type T)(a, b T) T {
    return a + b
}


func main() {
    c := Add(5, 6)
    fmt.Println(c)
}

運行上述示例:

type checking failed for main
prog.go2:6:9: invalid operation: operator + not defined for a (variable of type T)

什麼情況!這麼簡單的一個函數,Go編譯器居然報了這個錯誤:類型參數T未定義“+”這個操作符運算

在此版Go泛型設計中,泛型函數只能使用類型參數所能實例化出的任意類型都能支持的操作。比如上述Add函數的類型參數type T沒有任何約束,它可以被實例化爲任何類型。那麼這些實例化後的類型是否都支持“+”操作符運算呢?顯然不是。因此,編譯器針對示例代碼中的第六行報了錯!

對於像上面Add函數那樣的沒有任何約束的類型參數實例,Go允許對其進行的操作包括:

聲明這些類型的變量;使用相同類型的值爲這些變量賦值;將這些類型的變量以實參形式傳給函數或從作爲函數返回值;取這些變量的地址;將這些類型的值轉換或賦值給interface{}類型變量;通過類型斷言將一個接口值賦值給這類類型的變量;在type switch塊中作爲一個case分支;定義和使用由該類型組成的複合類型,比如:元素類型爲該類型的切片;將該類型傳遞給一些內置函數,比如new。

那麼,我們要讓上面的Add函數通過編譯器的檢查,我們就需要限制其類型參數所能實例化出的類型的範圍。比如:僅允許實例化爲底層類型(underlying type)爲整型類型的類型。上一版Go泛型設計中使用Contract來定義對類型參數的約束,不過由於Contract與interface在概念範疇上有交集,讓Gopher們十分困惑,於是在新版泛型設計中,Contract這個關鍵字被移除了,取而代之的是語法擴展了的interface,即我們使用interface類型來修飾類型參數以實現對其可實例化出的類型集合的約束。我們來看下面例子:

// https://go2goplay.golang.org/p/kMxZI2vIsk-
package main


import "fmt"


type PlusableInteger interface {
    type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
}


func Add(type T PlusableInteger)(a, b T) T {
    return a + b
}


func main() {
    c := Add(5, 6)
    fmt.Println(c)
}

運行該示例:

11

如果我們在main函數中寫下如下代碼:

    f := Add(3.65, 7.23)
    fmt.Println(f)

我們將得到如下編譯錯誤:

type checking failed for main
prog.go2:20:7: float64 does not satisfy PlusableInteger (float64 not found in int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64)

我們看到:該提案擴展了interface語法,新增了類型列表(type list)表達方式,專用於對類型參數進行約束。以該示例爲例,如果編譯器通過類型推導得到的類型在PlusableInteger這個接口定義的類型列表(type list)中,那麼編譯器將允許這個類型參數實例化;否則就像Add(3.65, 7.23)那樣,推導出的類型爲float64,該類型不在PlusableInteger這個接口定義的類型列表(type list)中,那麼類型參數實例化將報錯!

注意:定義中帶有類型列表的接口將無法用作接口變量類型,比如下面這個示例:

// https://go2goplay.golang.org/p/RchnTw73VMo
package main


type PlusableInteger interface {
    type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
}


func main() {
    var n int = 6
    var i PlusableInteger
    i = n
    _ = i
}

編譯器會報如下錯誤:

type checking failed for main
prog.go2:9:8: interface type for variable cannot contain type constraints (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64)


我們還可以用interface的原生語義對類型參數進行約束,看下面例子:

// https://go2goplay.golang.org/p/hyTbglTLoIn
package main


import (
    "fmt"
    "strconv"
)


type StringInt int


func (i StringInt) String() string {
    return strconv.Itoa(int(i))
}


type Stringer interface {
    String() string
}


func Stringify(type T Stringer)(s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}


func main() {
    fmt.Println(Stringify([]StringInt{1, 2, 3, 4, 5}))
}

運行該示例:

[1 2 3 4 5]

如果我們在main函數中寫下如下代碼:

func main() {
    fmt.Println(Stringify([]int{1, 2, 3, 4, 5}))
}

那麼我們將得到下面的編譯器錯誤輸出:

type checking failed for main
prog.go2:27:2: int does not satisfy Stringer (missing method String)

我們看到:只有實現了Stringer接口的類型纔會被允許作爲實參傳遞給Stringify泛型函數的類型參數併成功實例化。

我們還可以結合interface的類型列表(type list)和方法列表一起對類型參數進行約束,看下面示例:

// https://go2goplay.golang.org/p/tchwW6mPL7_d
package main


import (
    "fmt"
    "strconv"
)


type StringInt int


func (i StringInt) String() string {
    return strconv.Itoa(int(i))
}


type SignedIntStringer interface {
    type int, int8, int16, int32, int64
    String() string
}


func Stringify(type T SignedIntStringer)(s []T) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}


func main() {
    fmt.Println(Stringify([]StringInt{1, 2, 3, 4, 5}))
}

在該示例中,用於對泛型函數的類型參數進行約束的SignedIntStringer接口既包含了類型列表,也包含方法列表,這樣類型參數的實參類型既要在SignedIntStringer的類型列表中,也要實現了SignedIntStringer的String方法。

如果我們將上面的StringInt的底層類型改爲uint:

type StringInt uint


那麼我們將得到下面的編譯器錯誤輸出:

type checking failed for main
prog.go2:27:14: StringInt does not satisfy SignedIntStringer (uint not found in int, int8, int16, int32, int64)

2) 引入comparable預定義類型約束

由於Go泛型設計選擇了不支持運算操作符重載,因此,我們即便對interface做了語法擴展,依然無法表達類型是否支持==!=。爲了解決這個表達問題,這份新設計提案中引入了一個新的預定義類型約束:comparable。我們看下面例子:

// https://go2goplay.golang.org/p/tea39NqwZGC
package main


import (
    "fmt"
)


// Index returns the index of x in s, or -1 if not found.
func Index(type T comparable)(s []T, x T) int {
    for i, v := range s {
        // v and x are type T, which has the comparable
        // constraint, so we can use == here.
        if v == x {
            return i
        }
    }
    return -1
}


type Foo struct {
    a string
    b int
}


func main() {
    fmt.Println(Index([]int{1, 2, 3, 4, 5}, 3))
    fmt.Println(Index([]string{"a", "b", "c", "d", "e"}, "d"))
    pos := Index(
        []Foo{
            Foo{"a", 1},
            Foo{"b", 2},
            Foo{"c", 3},
            Foo{"d", 4},
            Foo{"e", 5},
        }, Foo{"b", 2})
    fmt.Println(pos)
}

運行該示例:

2
3
1

我們看到Go的原生支持比較的類型,諸如整型、字符串以及由這些類型組成的複合類型(如結構體)均可以直接作爲實參傳給由comparable約束的類型參數。comparable可以看成一個由Go編譯器特殊處理的、包含由所有內置可比較類型組成的type list的interface類型。我們可以將其嵌入到其他作爲約束的接口類型定義中:

type ComparableStringer interface {
    comparable
    String() string
}

只有支持比較的類型且實現了String方法,才能滿足ComparableStringer的約束。

3) 對泛型類型中類型參數的約束

和對泛型函數中類型參數的約束方法一樣,我們也可以對泛型類型的類型參數以同樣方法做同樣的約束,看下面例子:

// https://go2goplay.golang.org/p/O-YpTcW-tPu


// Package set implements sets of any comparable type.
package main


// Set is a set of values.
type Set(type T comparable) map[T]struct{}


// Make returns a set of some element type.
func Make(type T comparable)() Set(T) {
    return make(Set(T))
}


// Add adds v to the set s.
// If v is already in s this has no effect.
func (s Set(T)) Add(v T) {
    s[v] = struct{}{}
}


// Delete removes v from the set s.
// If v is not in s this has no effect.
func (s Set(T)) Delete(v T) {
    delete(s, v)
}


// Contains reports whether v is in s.
func (s Set(T)) Contains(v T) bool {
    _, ok := s[v]
    return ok
}


// Len reports the number of elements in s.
func (s Set(T)) Len() int {
    return len(s)
}


// Iterate invokes f on each element of s.
// It's OK for f to call the Delete method.
func (s Set(T)) Iterate(f func(T)) {
    for v := range s {
        f(v)
    }
}


func main() {
    s := Make(int)()


    // Add the value 1,11,111 to the set s.
    s.Add(1)
    s.Add(11)
    s.Add(111)


    // Check that s does not contain the value 11.
    if s.Contains(11) {
        println("the set contains 11")
    }
}

運行該示例:

the set contains 11

這個示例定義了一個數據結構:Set。該Set中的元素是有約束的:必須支持可比較。對應到代碼中,我們用comparable作爲泛型類型Set的類型參數的約束。

4) 關於泛型類型的方法

泛型類型和普通類型一樣,也可以定義自己的方法。但泛型類型的方法目前不支持除泛型類型自身的類型參數之外的其他類型參數了。我們看下面例子:

// https://go2goplay.golang.org/p/JipsxG7jeCN


// Package set implements sets of any comparable type.
package main


// Set is a set of values.
type Set(type T comparable) map[T]struct{}


// Make returns a set of some element type.
func Make(type T comparable)() Set(T) {
    return make(Set(T))
}


// Add adds v to the set s.
// If v is already in s this has no effect.
func (s Set(T)) Add(v T) {
    s[v] = struct{}{}
}


func (s Set(T)) Method1(type P)(v T, p P) {


}




func main() {
    s := Make(int)()
    s.Add(1)
    s.Method1(10, 20)
}

在這個示例中,我們新定義的Method1除了在參數列表中使用泛型類型Set的類型參數T之外,又接受了一個類型參數P。執行該示例:

type checking failed for main
prog.go2:18:24: methods cannot have type parameters

我們看到編譯器給出錯誤:泛型類型的方法不能再有其他類型參數。目前提案僅是暫時不支持額外的類型參數(如果支持,會讓語言規範和實現都變得異常複雜),Go核心團隊也會聽取社區反饋的意見,直到大家都認爲支持額外類型參數是有必要的,那麼後續會重新添加。

5) type *T Constraint

上面我們一直採用的對類型參數的約束形式是:

type T Constraint

假設調用泛型函數時某類型A要作爲T的實參傳入,A必須實現Constraint(接口)。

如果我們將上面對類型參數的約束形式改爲:

type *T Constraint

那麼這將意味着類型A要作爲T的實參傳入,

A必須滿足Constraint(接口)。並且Constraint中的所有方法(如果有的話)都僅能通過

A實例調用。我們來看下面示例:

// https://go2goplay.golang.org/p/g3cwgguCmUo
package main


import (
    "fmt"
    "strconv"
)


type Setter interface {
    Set(string)
}


func FromStrings(type *T Setter)(s []string) []T {
    result := make([]T, len(s))
    for i, v := range s {
        result[i].Set(v)
    }
    return result
}


// Settable is a integer type that can be set from a string.
type Settable int


// Set sets the value of *p from a string.
func (p *Settable) Set(s string) {
    i, _ := strconv.Atoi(s) // real code should not ignore the error
    *p = Settable(i)
}


func main() {
    nums := FromStrings(Settable)([]string{"1", "2"})
    fmt.Println(nums)
}

運行該示例:

[1 2]

我們看到Settable的方法集合是空的,而

Settable的方法集合(method set)包含了Set方法。因此,

Settable是滿足Setter對FromStrings函數的類型參數的約束的。

而如果我們直接使用type T Setter,那麼編譯器將給出下面錯誤:

type checking failed for main
prog.go2:30:22: Settable does not satisfy Setter (missing method Set)

如果我們使用type T Setter並結合使用FromStrings(*Settable),那麼程序運行會panic。https://go2goplay.golang.org/p/YLe2d78aSz-

3. 性能影響

根據這份技術提案中關於泛型函數和泛型類型實現的說明,Go會使用基於接口的方法來編譯泛型函數(generic function),這將優化編譯時間,因爲該函數僅會被編譯一次。但是會有一些運行時代價。

對於每個類型參數集,泛型類型(generic type)可能會進行多次編譯。這將延長編譯時間,但是不會產生任何運行時代價。編譯器還可以選擇使用類似於接口類型的方法來實現泛型類型,使用專用方法訪問依賴於類型參數的每個元素。

4. 小結

Go泛型方案的即將定型即好也不好。Go向來以簡潔著稱,增加加泛型,無論採用什麼技術方案,都會增加Go的複雜性,提升其學習門檻,代碼可讀性也會下降。但在某些場合(比如實現container數據結構及對應算法庫等),使用泛型卻又能簡化實現。

在這份提案中,Go核心團隊也給出如下期望:

We expect that most packages will not define generic types or functions, but many packages are likely to use generic types or functions defined elsewhere


我們期望大多數軟件包不會定義泛型類型或函數,但是許多軟件包可能會使用在其他地方定義的泛型類型或函數。

並且提案提到了會在Go標準庫中增加一些新包,已實現基於泛型的標準數據結構(slice、map、chan、math、list/ring等)、算法(sort、interator)等,gopher們只需調用這些包提供的API即可。

另外該提案的一大優點就是與Go1兼容,我們可能永遠不會使用Go2這個版本號了。

go核心團隊提供了可實踐該方案語法的playground:https://go2goplay.golang.org/,大家可以一邊研讀技術提案,一邊編寫代碼進行實驗驗證。


我的網課“Kubernetes實戰:高可用集羣搭建、配置、運維與應用”在慕課網上線了,感謝小夥伴們學習支持!

我愛發短信:企業級短信平臺定製開發專家 https://51smspush.com/ smspush : 可部署在企業內部的定製化短信平臺,三網覆蓋,不懼大併發接入,可定製擴展;短信內容你來定,不再受約束, 接口豐富,支持長短信,簽名可選。

2020年4月8日,中國三大電信運營商聯合發佈《5G消息白皮書》,51短信平臺也會全新升級到“51商用消息平臺”,全面支持5G RCS消息。

著名雲主機服務廠商DigitalOcean發佈最新的主機計劃,入門級Droplet配置升級爲:1 core CPU、1G內存、25G高速SSD,價格5$/月。有使用DigitalOcean需求的朋友,可以打開這個鏈接地址:https://m.do.co/c/bff6eed92687 開啓你的DO主機之路。

Gopher Daily(Gopher每日新聞)歸檔倉庫 - https://github.com/bigwhite/gopherdaily

我的聯繫方式:

微博:https://weibo.com/bigwhite20xx 微信公衆號:iamtonybai 博客:tonybai.com github: https://github.com/bigwhite

微信讚賞:


商務合作方式:撰稿、出書、培訓、在線課程、合夥創業、諮詢、廣告合作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章