從 Go 1.18 開始,Go 支援泛型(Generics,也就是 type parameters)。這讓你可以用同一份程式碼,處理多種型態,而不用到處寫重複函式或依賴 interface{} 加型態斷言。

泛型函式

函式可以在名稱後面宣告型別參數,例如:

package main

import "fmt"

func First[T any](elems []T) (T, bool) {
    if len(elems) == 0 {
        var zero T
        return zero, false
    }
    return elems[0], true
}

func main() {
    if v, ok := First([]int{10, 20, 30}); ok {
        fmt.Println(v) // 10
    }
    if v, ok := First([]string{"Go", "Rust"}); ok {
        fmt.Println(v) // Go
    }
}

T any 表示 T 可以是任何型態;anyinterface{} 的別名(Go 1.18 新增)。

通常編譯器可以從引數推斷型別參數,因此呼叫時常不必寫成 First[int](...)

型別條件(constraints)

如果泛型函式或泛型型別需要使用某些運算(例如 ==<),就要限制型別參數可接受的型態範圍,這就是型別條件。

package main

import "fmt"

func IndexOf[T comparable](elems []T, target T) int {
    for i, elem := range elems {
        if elem == target {
            return i
        }
    }
    return -1
}

func main() {
    fmt.Println(IndexOf([]int{1, 2, 3}, 2))           // 1
    fmt.Println(IndexOf([]string{"Go", "C"}, "Rust")) // -1
}

comparable 也是 Go 1.18 新增的預定義識別名稱,只能用在型別條件中。從 Go 1.20 起,可比較(但可能在執行時比較時 panic)的型別,也能滿足 comparable 條件。

介面作為型別集合(type set)

Go 1.18 之後,介面不只描述方法集合,也可以描述型別集合(用於 constraint)。例如:

type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

~int 表示「底層型態是 int 的型態」,這讓自訂型別(例如 type MyInt int)也能符合條件。

這類帶有 union(|)或具體型別元素的介面,通常只能用在型別條件,不會拿來當一般執行期介面值使用。

泛型型別

不只函式可以用泛型,型別宣告也可以:

package main

import "fmt"

type Stack[T any] struct {
    elems []T
}

func (s *Stack[T]) Push(v T) {
    s.elems = append(s.elems, v)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.elems) == 0 {
        var zero T
        return zero, false
    }
    i := len(s.elems) - 1
    v := s.elems[i]
    s.elems = s.elems[:i]
    return v, true
}

func main() {
    var s Stack[string]
    s.Push("Go")
    s.Push("1.26")
    v, _ := s.Pop()
    fmt.Println(v) // 1.26
}

Go 1.24:generic type aliases

Go 1.24 起,generic type aliases 完整支援,因此可像下面這樣寫:

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

這在整理常用資料結構別名時很方便,例如 Set[string]Set[int]

Go 1.26:型別參數列表中的自我參照

Go 1.26 放寬了限制,泛型型別可在自己的型別參數列表中參照自己。這對某些泛型介面(特別是要求「輸入輸出都是自身型態」)會比較自然:

package main

import "fmt"

type Adder[A Adder[A]] interface {
    Add(A) A
}

type MyInt int

func (x MyInt) Add(y MyInt) MyInt {
    return x + y
}

func Algo[A Adder[A]](x, y A) A {
    return x.Add(y)
}

func main() {
    fmt.Println(Algo(MyInt(10), MyInt(20))) // 30
}

何時該用泛型?

泛型適合用在:

  • 演算法或容器邏輯完全相同,只差元素型態(例如 stack、set、搜尋、走訪)
  • 想避免 interface{} 與型態斷言造成的樣板程式碼與執行期錯誤
  • 需要在編譯期保留型態資訊,提高可讀性與型別安全

但如果某個邏輯本質上就是依賴行為(方法)而不是型態,傳統介面仍然常常是更直接的做法。