Go泛型基礎使用

大雄45發表於2022-01-15
導讀 泛型,是Go語言多年來最令人興奮和根本性的變化之一。沒有泛型,很多人以此「鄙視」Go語言。當然,也有人覺得根本不需要泛型。有泛型,不代表你一定要用。平心而論,有些場景下,泛型還是很有必要和幫助的。

Go泛型基礎使用Go泛型基礎使用

現在已經確認,Go1.18 正式包含泛型(Go1.17 已經可以試用,只是預設不支援,見之前的文章:揚眉吐氣:剛剛,Go 已經預設支援泛型了)。

不過,不少人對泛型還是迷迷糊糊的。本文就嘗試用簡單的術語解釋泛型相關的內容。

01 什麼是泛型

Go 是一門強型別語言,意味著程式中的每個變數和值都有某種特定的型別,例如int、string 等。在函式簽名中,我們需要對引數和返回值指定型別,如下所示:

func Add(a, b int) int

引數 a 和 b 的型別是 int,返回值型別也是 int,結果是 a 和 b 的和。

如果現在需要一個對兩個 float64 求和的函式,怎麼辦?

大機率會出現類似這樣的函式:

func AddFloat(a, b float64) float64

如果有更多其他的型別(比如字串相加),可能需要寫更多的對應版本函式,很不方便,也很繁瑣,一堆複製貼上的程式碼。

02 Go 中的泛型函式

如果有了泛型,上面的問題怎麼解決呢?只需要一個函式就搞定:

func Add[T any](a, b T) T

是不是很簡單?不過看著有點暈?稍微解釋下:

  1. Add 後面的 [T any],T 表示型別的標識,any 表示 T 可以是任意型別
  1. a、b 和返回值的型別 T 和前面的 T 是同一個型別
  1. 為什麼用 [],而不是其他語言中的 <>,官方有過解釋,大概就是 <> 會有歧義。曾經計劃使用 (),因為太容易混淆,最後使用了 []。

這樣就表示,a、b 和返回值可以是任意型別,但它們的型別是同一個。那具體是什麼型別如何確定呢?根據呼叫時的實際引數決定。因此,我們現在可以這麼使用:

Add(1, 2) 
Add(2.1, 3.2)

不過,這時候程式碼會報錯。你可以本地用 Go1.17 啟用泛型的方式試驗,也可以使用 gotip 版本,亦或直接訪問這裡試驗:

package main 
 
import ( 
 "fmt" 
) 
 
func Add[T any](a, b T) T { 
 return a + b 
} 
 
func main() { 
 fmt.Println(Add(1, 2)) 
 fmt.Println(Add(2.1, 3.2)) 
}

執行會報錯:

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

為什麼?請看下文。

03 約束

很顯然,並非所有型別都支援加法操作。因此我們需要給出約束,指定可以進行加法操作的型別。

上面程式碼中,我們對型別 T 使用的是 any,相當於沒有進行任何約束。現在我們給一個約束:

type Addable interface { 
 type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128, string 
}

這是新語法,叫做型別列表(type list)。

首先,Addable 重用了介面語法,即 interface 關鍵字,表示約束,具體約束的型別透過 type 指定,多個用逗號分隔。

現在 Add 函式中 T 的約束從 any 改為 Addable:

func Add[T Addable](a, b T "T Addable") T { 
 return a + b 
}

現在再次執行:,發現正常了。而且還支援字串、複數等:

Add("polaris", "xu")

可見,約束可以是任意介面型別。(any 相當於空介面)

還有另外一種場景:可比較。比如 map 中的 key 要求是可比較的。比如下面的程式碼:

func findFunc[T any](a []T, v T "T any") int { 
 for i, e := range a { 
  if  e == v { 
      return i 
    } 
 } 
 return -1 
 }

T 的約束是任意型別,而實際上並非所有型別都是可比較的。怎麼辦?我們當然可以向上面 Addable 一樣定義一個約束,但為了方便,Go 內建提供了一個 comparable 約束,表示可比較的。參考下面程式碼:

package main 
 
func findFunc[T comparable](a []T, v T "T comparable") int { 
 for i, e := range a { 
  if e == v { 
   return i 
  } 
 } 
 return -1 
} 
 
func main() { 
 print(findFunc([]int{1, 2, 3, 4, 5, 6}, 5)) 
}
04 constraints 包

寫泛型程式碼時,約束挺常見。再看一個例子,從切片中找出最大值:

func Max[T any](input []T "T any") (max T) { 
    for _, v := range input { 
        if v > max { 
            max = v 
        } 
    } 
    return 
}

但執行會報錯:

fmt.Println(Max([]int{1, 4, 2, 10})) 
// cannot compare v > max (operator > not defined for T)

這時,我們自然想到使用上面 Add 函式類似的辦法,自定義一個約束:Ordered,把可能的型別都列上。

type Ordered interface {
type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string
}
因為這樣的需求挺常見的,為了方面,官方提供了一個新包:constraints,預定義了一些約束,具體檢視:。

有了它,不需要自定義這個 Ordered 約束,而是使用 constraints 包中的,即:

func Max[T constraints.Ordered](input []T "T constraints.Ordered") (max T)
05 泛型型別

上面,我們介紹了泛型函式:即函式可以接受任意型別。注意和 interface{} 這樣的任意型別區分開,泛型中的型別,在函式內部並不需要做任何型別斷言和反射的工作,在編譯期就可以確定具體的型別。

我們知道,Go 支援自定義型別,比如標準庫 sort 包中的 IntSlice:

type IntSlice []int

此外,還有 StringSlice、Float64Slice 等,一堆重複程式碼。如果我們能夠定義泛型型別,就不需要定義這麼多不同的型別了。比如:

type Slice[T any] []T

能看懂吧。

在使用時,針對 int 型別,就是這樣:

x := Slice[int]{1, 2, 3}

如果作為函式引數,這麼使用:

func PrintSlice[T any](b Slice[T] "T any")

如果為這個型別定義方法,則是這樣:

func (b Slice[T]) Print()

也就是說,Slice[T] 作為整體存在。

當然,泛型型別也可以做型別約束,而不是 any 型別:

type Slice[T comparable] []T
06 總結

透過本文的講解,相信你對 Go 泛型有了一個基本的掌握。

Go1.18 會包含不少泛型相關的標準庫,包括對現有標準庫的泛型支援,這是目前 Go 官方的重要工作。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2837746/,如需轉載,請註明出處,否則將追究法律責任。

相關文章