Go 泛型之類型參數

来源:https://www.cnblogs.com/taoxiaoxin/archive/2023/12/23/17923032.html
-Advertisement-
Play Games

Go 泛型之瞭解類型參數 目錄Go 泛型之瞭解類型參數一、Go 的泛型與其他主流編程語言的泛型差異二、返回切片中值最大的元素三、類型參數(type parameters)四、泛型函數3.1 泛型函數的結構3.2 調用泛型函數3.3 泛型函數實例化(instantiation)五、泛型類型5.1 聲明 ...


Go 泛型之瞭解類型參數

目錄

一、Go 的泛型與其他主流編程語言的泛型差異

Go泛型和其他支持泛型的主流編程語言之間的泛型設計與實現存在差異一樣,Go 的泛型與其他主流編程語言的泛型也是不同的。我們先看一下 Go 泛型設計方案已經明確不支持的若幹特性,比如:

  • 不支持泛型特化(specialization),即不支持編寫一個泛型函數針對某個具體類型的特殊版本;
  • 不支持元編程(metaprogramming),即不支持編寫在編譯時執行的代碼來生成在運行時執行的代碼;
  • 不支持操作符方法(operator method),即只能用普通的方法(method)操作類型實例(比如:getIndex(k)),而不能將操作符視為方法並自定義其實現,比如一個容器類型的下標訪問 c[k];
  • 不支持變長的類型參數(type parameters);
  • ......

這些特性如今不支持,後續大概率也不會支持。在進入 Go 泛型語法學習之前,一定要先瞭解 Go 團隊的這些設計決策。

二、返回切片中值最大的元素

我們先來看一個例子,實現一個函數,該函數接受一個切片作為輸入參數,然後返回該切片中值最大的那個元素。題目並沒有明確使用什麼元素類型的切片,我們就先以最常見的整型切片為例,實現一個 maxInt 函數:

// max_int.go
func maxInt(sl []int) int { 
    if len(sl) == 0 { 
        panic("slice is empty")
    } 

    max := sl[0]
    for _, v := range sl[1:] { 
        if v > max { 
            max = v 
        } 
    } 
    return max
}

func main() {
    fmt.Println(maxInt([]int{1, 2, -4, -6, 7, 0})) // 輸出:7
}

maxInt 的邏輯十分簡單。我們使用第一個元素值 (max := sl[0]) 作為 max 變數初值,然後與切片後面的元素 (sl[1:]) 進行逐一比較,如果後面的元素大於 max,則將其值賦給 max,這樣到切片遍歷結束,我們就得到了這個切片中值最大的那個元素(即變數 max)。

我們現在給它加一個新需求:能否針對元素為 string 類型的切片返回其最大(按字典序)的元素值呢?

答案肯定是能!我們來實現這個 maxString 函數:

// max_string.go
func maxString(sl []string) string {
    if len(sl) == 0 {
        panic("slice is empty")
    }

    max := sl[0]
    for _, v := range sl[1:] {
        if v > max {
            max = v
        }
    }
    return max
}

func main() {
    fmt.Println(maxString([]string{"11", "22", "44", "66", "77", "10"})) // 輸出:77
}

maxString 實現了返回 string 切片中值最大元素的需求。不過從實現上來看,maxStringmaxInt 異曲同工,只是切片元素類型不同罷了。這時如果讓你參考上述 maxIntmaxString 實現一個返回浮點類型切片中最大值的函數 maxFloat,你肯定“秒秒鐘”就可以給出一個正確的實現:

// max_float.go
func maxFloat(sl []float64) float64 {
    if len(sl) == 0 {
        panic("slice is empty")
    }

    max := sl[0]
    for _, v := range sl[1:] {
        if v > max {
            max = v
        }
    }
    return max
}

func main() {
    fmt.Println(maxFloat([]float64{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 輸出:7.07
}

問題來了!你肯定在上面三個函數發現了的“糟糕味道”:代碼重覆。上面三個函數除了切片的元素類型不同,其他邏輯都一樣。

那麼能否實現一個“通用”的函數,可以處理上面三種元素類型的切片呢?提到“通用”,你一定想到了 Go 語言提供的 anyinterface{}的別名),我們來試試:

// max_any.go
func maxAny(sl []any) any {
    if len(sl) == 0 {
        panic("slice is empty")
    }

    max := sl[0]
    for _, v := range sl[1:] {
        switch v.(type) {
        case int:
            if v.(int) > max.(int) {
                max = v
            }
        case string:
            if v.(string) > max.(string) {
                max = v
            }
        case float64:
            if v.(float64) > max.(float64) {
                max = v
            }
        }
    }
    return max
}

func main() {
    i := maxAny([]any{1, 2, -4, -6, 7, 0})
    m := i.(int)
    fmt.Println(m) // 輸出:7
    fmt.Println(maxAny([]any{"11", "22", "44", "66", "77", "10"})) // 輸出:77
    fmt.Println(maxAny([]any{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 輸出:7.07
}

我們看到,maxAny 利用 anytype switch 和類型斷言(type assertion)實現了我們預期的目標。不過這個實現並不理想,它至少有如下幾個問題:

  1. 若要支持其他元素類型的切片,我們需對該函數進行修改;
  2. maxAny 的返回值類型為 anyinterface{}),要得到其實際類型的值還需要通過類型斷言轉換;
  3. 使用 anyinterface{})作為輸入參數的元素類型和返回值的類型,由於存在裝箱和拆箱操作,其性能與 maxInt 等比起來要遜色不少,實測數據如下:
// max_test.go
func BenchmarkMaxInt(b *testing.B) {
    sl := []int{1, 2, 3, 4, 7, 8, 9, 0}
    for i := 0; i < b.N; i++ {
        maxInt(sl)
    }
}

func BenchmarkMaxAny(b *testing.B) {
    sl := []any{1, 2, 3, 4, 7, 8, 9, 0}
    for i := 0; i < b.N; i++ {
        maxAny(sl)
    }
}

測試結果如下:

$go test -v -bench . ./max_test.go max_any.go max_int.go
goos: darwin
goarch: amd64
... ...
BenchmarkMaxInt
BenchmarkMaxInt-8     398996863           2.982 ns/op
BenchmarkMaxAny
BenchmarkMaxAny-8     85883875          13.91 ns/op
PASS
ok    command-line-arguments  2.710s

我們看到,基於 anyinterface{}) 實現的 maxAny 其執行性能要比像 maxInt 這樣的函數慢上數倍。

在 Go 1.18 版本之前,Go 的確沒有比較理想的解決類似上述“通用”問題的手段,直到 Go 1.18 版本泛型落地後,我們可以用泛型語法實現 maxGenerics 函數:

// max_generics.go
type ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

func maxGenerics[T ordered](sl []T) T {
    if len(sl) == 0 {
        panic("slice is empty")
    }

    max := sl[0]
    for _, v := range sl[1:] {
        if v > max {
            max = v
        }
    }
    return max
}

type myString string

func main() {
    var m int = maxGenerics([]int{1, 2, -4, -6, 7, 0})
    fmt.Println(m) // 輸出:7
    fmt.Println(maxGenerics([]string{"11", "22", "44", "66", "77", "10"})) // 輸出:77
    fmt.Println(maxGenerics([]float64{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 輸出:7.07
    fmt.Println(maxGenerics([]int8{1, 2, -4, -6, 7, 0})) // 輸出:7
    fmt.Println(maxGenerics([]myString{"11", "22", "44", "66", "77", "10"})) // 輸出:77
}

我們看到,從功能角度看,泛型版本的 maxGenerics 實現了預期的特性,對於 ordered 介面中聲明的那些原生類型以及以這些原生類型為底層類型(underlying type)的類型(比如示例中的 myString),maxGenerics 都可以無縫支持。並且,maxGenerics 返回的類型與傳入的切片的元素類型一致,調用者也無需通過類型斷言做轉換。

此外,通過下麵的性能基準測試我們也可以看出,與 maxAny 相比,泛型版本的 maxGenerics 性能要好很多,但與原生版函數如 maxInt 等還有差距。性能測試如下:

$go test -v -bench . ./max_test.go max_any.go max_int.go max_generics.go
goos: darwin
goarch: amd64
BenchmarkMaxInt
BenchmarkMaxInt-8          400910706           2.983 ns/op
BenchmarkMaxAny
BenchmarkMaxAny-8          85257433          14.04 ns/op
BenchmarkMaxGenerics
BenchmarkMaxGenerics-8     209468593           5.701 ns/op
PASS
ok    command-line-arguments  4.492s

通過這個例子,我們也可以看到 Go 泛型十分適合實現一些操作容器類型(比如切片、map 等)的演算法,這也是 Go 官方推薦的第一種泛型應用場景,此類容器演算法的泛型實現使得容器演算法與容器內元素類型徹底解耦!

三、類型參數(type parameters)

根據官方說法,由於“泛型”(generic)一詞在 Go 社區中被廣泛使用,所以官方也就接納了這一說法。但 Go 泛型方案的實質是對類型參數(type parameter)的支持,包括:

  • 泛型函數(generic function):帶有類型參數的函數;
  • 泛型類型(generic type):帶有類型參數的自定義類型;
  • 泛型方法(generic method):泛型類型的方法。

首先,以泛型函數為例來具體說明一下什麼是類型參數。

四、泛型函數

3.1 泛型函數的結構

我們回顧一下上面的示例,maxGenerics 就是一個泛型函數,我們看一下 maxGenerics 的函數原型:

func maxGenerics[T ordered](sl []T) T {
    // ... ...
}

我們看到,maxGenerics 這個函數與我們之前學過的普通 Go 函數(ordinary function)相比,至少有兩點不同:

  • maxGenerics 函數在函數名稱與函數參數列表之間多了一段由方括弧括起的代碼:[T ordered]
  • maxGenerics 參數列表中的參數類型以及返回值列表中的返回值類型都是 T,而不是某個具體的類型。

maxGenerics 函數原型中多出的這段代碼[T ordered]就是 Go 泛型的類型參數列表(type parameters list示例中這個列表中僅有一個類型參數 Tordered 為類型參數的類型約束(type constraint)。類型約束之於類型參數,就好比常規參數列表中的類型之於常規參數。

Go 語言規範規定:函數的類型參數列表位於函數名與函數參數列表之間,由方括弧括起的固定個數的、由逗號分隔的類型參數聲明組成,其一般形式如下:

func genericsFunc[T1 constraint1, T2, constraint2, ..., Tn constraintN](ordinary parameters list) (return values list)

函數一旦擁有類型參數,就可以用該參數作為常規參數列表和返回值列表中修飾參數和返回值的類型。我們繼續 maxGenerics 泛型函數為例分析,它擁有一個類型參數 T,在常規參數列表中,T 被用作切片的元素類型;在返回值列表中,T 被用作返回值的類型。

按 Go 慣例,類型參數名的首字母通常採用大寫形式,並且類型參數必須是具名的,即便你在後續的函數參數列表、返回值列表和函數體中沒有使用該類型參數,也是這樣。比如下麵例子中的類型參數 T

func print[T any]() { // 正確
}     

func print[any]() {   // 編譯錯誤:all type parameters must be named 
}

和常規參數列表中的參數名唯一一樣,在同一個類型參數列表中,類型參數名字也要唯一,下麵這樣的代碼將會導致 Go 編譯器報錯:

func print[T1 any, T1 comparable](sl []T) { //  編譯錯誤:T1 redeclared in this block
    //...
}

常規參數列表中的參數有其特定作用域,即從參數聲明處開始到函數體結束。和常規參數類似,泛型函數中類型參數也有其作用域範圍,這個範圍從類型參數列表左側的方括弧[開始,一直持續到函數體結束,如下圖所示:

類型參數的作用域也決定了類型參數的聲明順序並不重要,也不會影響泛型函數的行為,於是下麵的泛型函數聲明與上圖中的函數是等價的:

func foo[M map[E]T, T any, E comparable](m M)(E, T) {
    //... ...
}

3.2 調用泛型函數

首先,我們對“類型參數”做一下細分。和普通函數有形式參數與實際參數一樣,類型參數也有類型形參(type parameter)和類型實參(type argument)之分。其中類型形參就是泛型函數聲明中的類型參數,以前面示例中的 maxGenerics 泛型函數為例,如下麵代碼,maxGenerics 的類型形參就是 T,而類型實參則是在調用 maxGenerics 時實際傳遞的類型 int

// 泛型函數聲明:T為類型形參
func maxGenerics[T ordered](sl []T) T

// 調用泛型函數:int為類型實參
m := maxGenerics[int]([]int{1, 2, -4, -6, 7, 0})

從上面這段代碼我們也可以看出調用泛型函數與調用普通函數的區別。在調用泛型函數時,除了要傳遞普通參數列表對應的實參之外,還要顯式傳遞類型實參,比如這裡的 int。並且,顯式傳遞的類型實參要放在函數名和普通參數列表前的方括弧中。

在反覆揣摩上面代碼和說明後,你可能會提出這樣的一個問題:如果泛型函數的類型形參較多,那麼逐一顯式傳入類型實參會讓泛型函數的調用顯得十分冗長,比如:

foo[int, string, uint32, float64](1, "hello", 17, 3.14)

這樣的寫法對開發者而言顯然談不上十分友好。其實不光大家想到了這個問題,Go 團隊的泛型實現者們也考慮了這個問題,並給出瞭解決方法:函數類型實參的自動推斷(function argument type inference)。

顧名思義,這個機制就是通過判斷傳遞的函數實參的類型來推斷出類型實參的類型,從而允許開發者不必顯式提供類型實參,下麵是以 maxGenerics 函數為例的類型實參推斷過程示意圖:

我們看到,當 maxGenerics 函數傳入的實際參數為 []int{…} 時,Go 編譯器會將其類型 []int 與泛型函數參數列表中對應參數的類型([]T)作比較,並推斷出 T == int 這一結果。當然這個例子的推斷過程較為簡單,那些有難度的,甚至無法肉眼可見的就交給 Go 編譯器去處理吧,我們沒有必要過於深入。

不過,這個類型實參自動推斷有一個前提,你一定要記牢,那就是它必須是函數的參數列表中使用了的類型形參,否則就會像下麵的示例中的代碼,編譯器將報無法推斷類型實參的錯誤:

func foo[T comparable, E any](a int, s E) {
}

foo(5, "hello") // 編譯器錯誤:cannot infer T

在編譯器無法推斷出結果時,我們可以給予編譯器“部分提示”,比如既然編譯器無法推斷出 T 的實參類型,那我們就顯式告訴編譯器 T 的實參類型,即在泛型函數調用時,在類型實參列表中顯式傳入 T 的實參類型,但 E 的實參類型依然由編譯器自動推斷,示例代碼如下:

var s = "hello"
foo[int](5, s)  //ok
foo[int,](5, s) //ok

那麼,除了函數參數列表中的參數類型可以作為類型實參推斷的依據外,函數返回值的類型是否也可以呢?我們看下麵示例:

func foo[T any](a int) T {
    var zero T
    return zero
}

var a int = foo(5) // 編譯器錯誤:cannot infer T
println(a)

我們看到,這個函數僅在返回值中使用了類型參數,但編譯器沒能推斷出 T 的類型,所以我們切記:不能通過返回值類型來推斷類型實參。

有了函數類型實參推斷後,在大多數情況下,我們調用泛型函數就無須顯式傳遞類型實參了,開發者也因此獲得了與普通函數調用幾乎一致的體驗。

其實泛型函數調用是一個不同於普通函數調用的過程,為了揭開其中的“奧秘”,接下來我們看看泛型函數調用過程究竟發生了什麼。

3.3 泛型函數實例化(instantiation)

我們還以 maxGenerics 為例來演示一下這個過程:

maxGenerics([]int{1, 2, -4, -6, 7, 0})

上面代碼是對 maxGenerics 泛型函數的一次調用,Go 對這段泛型函數調用代碼的處理分為兩個階段,如下圖所示:

我們看到,Go 首先會對泛型函數進行實例化(instantiation),即根據自動推斷出的類型實參生成一個新函數(當然這一過程是在編譯階段完成的,不會對運行時性能產生影響),然後才會調用這個新函數對輸入的函數參數進行處理。

我們也可以用一種更形象的方式來描述上述泛型函數的實例化過程。實例化就好比一家生產“求最大值”機器的工廠,它會根據要比較大小的對象的類型將這樣的機器生產出來。以上面的例子來說,整個實例化過程如下:

  • 工廠接單:調用 maxGenerics([]int{…}),工廠師傅發現要比較大小的對象類型為 int
  • 模具檢查與匹配:檢查 int 類型是否滿足模具的約束要求,即 int 是否滿足 ordered 約束,如滿足,則將其作為類型實參替換 maxGenerics 函數中的類型形參 T,結果為 maxGenerics[int]
  • 生產機器:將泛型函數 maxGenerics 實例化為一個新函數,這裡將其起名為 maxGenericsInt,其函數原型為 func([]int) int。本質上 maxGenericsInt := maxGenerics[int]

我們實際的 Go 代碼也可以真實得到這台新生產出的“機器”,如下麵代碼所示:

maxGenericsInt := maxGenerics[int] // 實例化後得到的新“機器”:maxGenericsInt
fmt.Printf("%T\n", maxGenericsInt) // func([]int) int

一旦針對 int 對象的“求最大值”的機器被生產出來了,它就可以對目標對象進行處理了,這和普通的函數調用沒有區別。這裡就相當於調用如下代碼:

maxGenericsInt([]int{1, 2, -4, -6, 7, 0}) // 輸出:7

整個過程只需檢查傳入的函數實參([]int{1, 2, …})的類型與 maxGenericsInt 函數原型中的形參類型([]int)是否匹配即可。

另外要註意,當我們使用相同類型實參對泛型函數進行多次調用時,Go 僅會做一次實例化,並復用實例化後的函數,比如:

maxGenerics([]int{1, 2, -4, -6, 7, 0})
maxGenerics([]int{11, 12, 14, -36,27, 0}) // 復用第一次調用後生成的原型為func([]int) int的函數

好了,接下來我們再來看 Go 對類型參數的另一類支持:帶有類型參數的自定義類型,即泛型類型。

五、泛型類型

5.1 聲明泛型類型

所謂泛型類型,就是在類型聲明中帶有類型參數的 Go 類型,比如下麵代碼中的 maxableSlice

// maxable_slice.go

type maxableSlice[T ordered] struct {
    elems []T
}

顧名思義,maxableSlice 是一個自定義切片類型,這個類型的特點是總可以獲取其內部元素的最大值,其唯一的要求是其內部元素是可排序的,它通過帶有 ordered 約束的類型參數來明確這一要求。像這樣在定義中帶有類型參數的類型就被稱為泛型類型(generic type)。

從例子中的 maxableSlice 類型聲明中我們可以看到,在泛型類型中,類型參數列表放在類型名字後面的方括弧中。和泛型函數一樣,泛型類型可以有多個類型參數,類型參數名通常是首字母大寫的,這些類型參數也必須是具名的,且命名唯一。其一般形式如下:

type TypeName[T1 constraint1, T2 constraint2, ..., Tn constraintN] TypeLiteral

和泛型函數中類型參數有其作用域一樣,泛型類型中類型參數的作用域範圍也是從類型參數列表左側的方括弧[開始,一直持續到類型定義結束的位置,如下圖所示:

這樣的作用域將方便我們在各個欄位中靈活使用類型參數,下麵是一些自定義泛型類型的示例:

type Set[T comparable] map[T]struct{}

type sliceFn[T any] struct {
  s   []T
  cmp func(T, T) bool
}

type Map[K, V any] struct {
  root    *node[K, V]
  compare func(K, K) int
}

type element[T any] struct {
  next *element[T]
  val  T
}

type Numeric interface {
  ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
    ~float32 | ~float64 |
    ~complex64 | ~complex128
}

type NumericAbs[T Numeric] interface {
  Abs() T
}

我們看到,泛型類型中的類型參數可以用來作為類型聲明中欄位的類型(比如上面的 element 類型)、複合類型的元素類型(比如上面的 SetMap 類型)或方法的參數和返回值類型(如 NumericAbs 介面類型)等。

如果要在泛型類型聲明的內部引用該類型名,必須要帶上類型參數,如上面的 element 結構體中的 next 欄位的類型:*element[T]。按照泛型設計方案,如果泛型類型有不止一個類型參數,那麼在其聲明內部引用該類型名時,不僅要帶上所有類型參數,類型參數的順序也要與聲明中類型參數列表中的順序一致,比如:

type P[T1, T2 any] struct {
    F *P[T1, T2]  // ok
}

不過從實測結果來看,對於下麵不符合技術方案的泛型類型聲明也並未報錯:

type P[T1, T2 any] struct {
    F *P[T2, T1] // 不符合技術方案,但Go 編譯器並未報錯
}

5.2 使用泛型類型

和泛型函數一樣,使用泛型類型時也會有一個實例化(instantiation)過程,比如:

var sl = maxableSlice[int]{
    elems: []int{1, 2, -4, -6, 7, 0},
} 

Go 會根據傳入的類型實參(int)生成一個新的類型並創建該類型的變數實例,sl 的類型等價於下麵代碼:

type maxableIntSlice struct {
    elems []int
}

看到這裡你可能會問:泛型類型是否可以像泛型函數那樣實現類型實參的自動推斷呢?很遺憾,目前的 Go 1.21.4 尚不支持,下麵代碼會遭到 Go 編譯器的報錯:

var sl = maxableSlice {
    elems: []int{1, 2, -4, -6, 7, 0}, // 編譯器錯誤:cannot use generic type maxableSlice[T ordered] without instantiation
} 

不過這一特性在 Go 的未來版本中可能會得到支持。

既然涉及到了類型,你肯定會想到諸如類型別名、類型嵌入等 Go 語言機制,那麼這些語言機制對泛型類型的支持情況又是如何呢?我們逐一來看一下。

5.2.1 泛型類型與類型別名

我們知道類型別名type alias)與其綁定的原類型是完全等價的,但這僅限於原類型是一個直接類型,即可直接用於聲明變數的類型。那麼將類型別名與泛型類型綁定是否可行呢?我們來看一個示例:

type foo[T1 any, T2 comparable] struct {
    a T1
    b T2
}
  
type fooAlias = foo // 編譯器錯誤:cannot use generic type foo[T1 any, T2 comparable] without instantiation

在上述代碼中,我們為泛型類型 foo 建立了類型別名 fooAlias,但編譯這段代碼時,編譯器還是報了錯誤!

這是因為,泛型類型只是一個生產真實類型的“工廠”,它自身在未實例化之前是不能直接用於聲明變數的,因此不符合類型別名機制的要求。泛型類型只有實例化後才能得到一個真實類型,例如下麵的代碼就是合法的:

type fooAlias = foo[int, string]

也就是說,我們只能為泛型類型實例化後的類型創建類型別名,實際上上述 fooAlias 等價於實例化後的類型 fooInstantiation

type fooInstantiation struct {
    a int   
    b string
}

5.2.2 泛型類型與類型嵌入

類型嵌入是運用 Go 組合設計哲學的一個重要手段。引入泛型類型之後,我們依然可以在泛型類型定義中嵌入普通類型,比如下麵示例中 Lockable 類型中嵌入的 sync.Mutex

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

func (l *Lockable[T]) Get() T {
    l.Lock()
    defer l.Unlock()
    return l.t
}

func (l *Lockable[T]) Set(v T) {
    l.Lock()
    defer l.Unlock()
    l.t = v
}

在泛型類型定義中,我們也可以將其他泛型類型實例化後的類型作為成員。現在我們改寫一下上面的 Lockable,為其嵌入另外一個泛型類型實例化後的類型 Slice[int]

type Slice[T any] []T
  
func (s Slice[T]) String() string {
    if len(s) == 0 {
        return ""
    }
    var result = fmt.Sprintf("%v", s[0])
    for _, v := range s[1:] {
        result = fmt.Sprintf("%v, %v", result, v)
    }
    return result
}

type Lockable[T any] struct {
    t T
    Slice[int]
    sync.Mutex
}

func main() {
    n := Lockable[string]{
        t:     "hello",
        Slice: []int{1, 2, 3},
    }
    println(n.String()) // 輸出:1, 2, 3
}

我們看到,代碼使用泛型類型名(Slice)作為嵌入後的欄位名,並且 Slice[int] 的方法 String 被提升為 Lockable 實例化後的類型的方法了。同理,在普通類型定義中,我們也可以使用實例化後的泛型類型作為成員,比如讓上面的 Slice[int] 嵌入到一個普通類型 Foo 中,示例代碼如下:

type Foo struct {
    Slice[int]
}

func main() {
    f := Foo{
        Slice: []int{1, 2, 3},
    }
    println(f.String()) // 輸出:1, 2, 3
}

此外,Go 泛型設計方案支持在泛型類型定義中嵌入類型參數作為成員,比如下麵的泛型類型 Lockable 內嵌了一個類型 T,且 T 恰為其類型參數:

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

不過,Go 最新版1.21.4 編譯上述代碼時會針對嵌入 T 的那一行報如下錯誤:

編譯器報錯:embedded field type cannot be a (pointer to a) type parameter

關於這個錯誤,Go 官方在其 issue 中給出了臨時的結論:暫不支持

六、泛型方法

我們知道 Go 類型可以擁有自己的方法(method),泛型類型也不例外,為泛型類型定義的方法稱為泛型方法(generic method),接下來我們就來看看如何定義和使用泛型方法。

我們用一個示例,給 maxableSlice 泛型類型定義 max 方法,看一下泛型方法的結構:

func (sl *maxableSlice[T]) max() T {
    if len(sl.elems) == 0 {
        panic("slice is empty")
    }

    max := sl.elems[0]
    for _, v := range sl.elems[1:] {
        if v > max {
            max = v
        }
    }
    return max
}

我們看到,在定義泛型類型的方法時,方法的 receiver 部分不僅要帶上類型名稱,還需要帶上完整的類型形參列表(如 maxableSlice[T]),這些類型形參後續可以用在方法的參數列表和返回值列表中。

不過在 Go 泛型目前的設計中,泛型方法自身不可以再支持類型參數了,不能像下麵這樣定義泛型方法:

func (f *foo[T]) M1[E any](e E) T { // 編譯器錯誤:syntax error: method must have no type parameters
    //... ...
}

關於泛型方法未來是否能支持類型參數,目前 Go 團隊傾向於否,但最終結果 Go 團隊還要根據 Go 社區在使用泛型過程中的反饋而定。

在泛型方法中,receiver 中某個類型參數如果沒有在方法參數列表和返回值中使用,可以用“_”代替,但不能不寫,比如:

type foo[A comparable, B any] struct{}

func (foo[A, B]) M1() { // ok
}

或

func (foo[_, _]) M1() { // ok
}

或

func (foo[A, _]) M1() { // ok
}

但

func (foo[]) M1() { // 錯誤:receiver部分缺少類型參數

}

另外,泛型方法中的 receiver 中類型參數名字可以與泛型類型中的類型形參名字不同,位置和數量對上即可。我們還以上面的泛型類型 foo 為例,可以為它添加下麵方法:

type foo[A comparable, B any] struct{}

func (foo[First, Second]) M1(a First, b Second) { // First對應類型參數A,Second對應類型參數B

}
分享是一種快樂,開心是一種態度!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文深入探討了安卓DocumentsProvider的應用場景,分析了其優勢與不足,並提供了簡單的代碼實現。DocumentsProvider是安卓系統中用於文件存儲與訪問的關鍵組件,為應用開發者提供了強大的文件管理能力。 ...
  • Android對接微信登錄記錄 - Stars-One的雜貨小窩 Android項目要對接下微信登錄,稍微記錄下踩坑點 代碼 1.添加依賴 implementation 'com.tencent.mm.opensdk:wechat-sdk-android:6.8.0' 2.聲明Activity 在你 ...
  • login-status-iframe.html是keycloak為我們提供的一種檢測用戶登錄狀態的頁面,它要求用戶對接的系統通過iframe進行嵌入,然後通過window.addEventListener去訂閱子頁面的信息。 提示: 所有 HTML DOM 事件,可以查看我們完整的https:// ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 最近受夠了公司內部站點每次登陸都需要填寫用戶名和密碼,還有輸入驗證碼。 要是能夠直接跳過登陸頁面就好啦。 說乾就乾,決定使用油猴插件實現自動登陸功能。 其中最難解決的就是驗證碼破解,花了一天的時間完美解決,現在整理出來。 1.分析驗證碼 ...
  • 實現說明: 在 JS 中 canvas 原生沒有支持對文字間距的調整,我們可以通過將文字的每個字元單獨渲染來實現。本案例從 CanvasRenderingContext2D 對象的原型鏈上擴展了一個用於繪製帶間距的函數 fillTextWithSpacing(),使用方式與原生 fillText() ...
  • 1.關鍵字(keyword) 定義:被Java語言賦予了特殊含義,用做專門用途的字元串(或單詞),這些字元串(或單詞)已經被Java定義好了。 特點:全部關鍵字都是小寫字母。 關鍵字查閱的官方地址: https://docs.oracle.com/javase/tutorial/java/nutsa ...
  • 二三、編譯器 1、One Definition Rule 1)轉化單元 我們寫好的每個源文件(.cpp,.c)將其所包含的頭文件(#include <xxx.h>)合併後,稱為一個轉化單元。 編譯器單獨的將每一個轉化單元生成為對應的對象文件(.obj),對象文件包含了轉化單元的機器碼和轉化單元的引用 ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`QCharts`折線圖的常用方法及靈活運用。折線圖(Line Chart)是一種常用的數據可視化圖表,用於展示隨... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...