沒有泛型的日子
假設要寫一個加總的函式,int 一份、float64 一份:
func SumInts(nums []int) int { var total int for _, n := range nums { total += n } return total}
func SumFloats(nums []float64) float64 { var total float64 for _, n := range nums { total += n } return total}兩個函式除了型別外一樣,想避開重複通常只有 any:
// 用 any(也就是 interface{})接,代價是每次都要型別斷言,且錯誤要等到執行期才會炸func SumAny(nums []any) any { var total int for _, n := range nums { total += n.(int) // 傳進 float64 就 panic } return total}第一個泛型函式
泛型就像是型別的參數,寫在函式名稱後面的方括號裡:
func Sum[T int | float64](nums []T) T { var total T for _, n := range nums { total += n } return total}[T int | float64]:宣告一個型別參數T,後面那串是約束(Constraint),意思是T只能是int或float64。nums []T、回傳值T:函式簽章裡就可以拿T當成一般型別用。var total T:T的零值。int是0、float64是0.0。
呼叫時通常什麼都不用多寫,編譯器會從參數推斷出 T:
Sum([]int{1, 2, 3}) // T 推斷成 int,得到 6Sum([]float64{1.5, 2.5}) // T 推斷成 float64,得到 4Sum[int]([]int{1, 2, 3}) // 也可以手動指定
Sum([]string{"a", "b"}) // 編譯錯誤:string 不在約束裡錯誤發生在編譯期,不是執行期。這是泛型跟 any 搭配型別斷言最大的差別。
約束怎麼寫
約束本質上是一個 interface,只是它描述的不只是方法,還可以是「哪些型別可以代入」。
any:什麼型別都行
any 當約束時代表不限型別,但也因此不能對值做任何假設,只能搬移、比較指標、放進容器。經典的例子是 Map:
func Map[T, U any](s []T, f func(T) U) []U { result := make([]U, 0, len(s)) for _, v := range s { result = append(result, f(v)) } return result}
names := Map([]int{1, 2, 3}, func(n int) string { return fmt.Sprintf("no.%d", n)})// [no.1 no.2 no.3]一個函式可以有多個型別參數,這裡 T 是輸入元素型別、U 是輸出元素型別,兩者由 f 的簽章串起來。
comparable:可以用 == 比較
要在函式裡寫 == 或 !=,約束就得是 comparable:
func Contains[T comparable](s []T, target T) bool { for _, v := range s { if v == target { return true } } return false}
Contains([]string{"a", "b"}, "b") // truecomparable 涵蓋數字、字串、布林、指標、channel,以及欄位全都可比較的 struct 和 array;slice、map、func 不在其中,因為它們本來就不能用 == 比較。
cmp.Ordered:可以用 < > 比較
comparable 只保證 ==,要比大小得用標準函式庫 cmp 提供的 cmp.Ordered:
import "cmp"
func Max[T cmp.Ordered](s []T) (T, bool) { var zero T if len(s) == 0 { return zero, false } m := s[0] for _, v := range s[1:] { if v > m { m = v } } return m, true}
Max([]int{3, 1, 4}) // 4, trueMax([]string{"b", "a", "c"}) // "c", true自訂約束與 ~ 符號
約束多了就把它抽成具名的 interface:
type Number interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64}
func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total}~int 的波浪號讀作「底層型別是 int 的所有型別」。差別在這裡:
type Celsius float64
Sum([]Celsius{36.5, 37.2}) // 約束寫 ~float64 才過,寫 float64 會被擋下沒有 ~ 的話,只有 float64 本人算數,Celsius 這種自訂型別會被排除。實務上寫約束預設加 ~ 就對了。
約束裡也可以放方法,這時它就跟一般 interface 沒兩樣:
type Stringer interface { String() string}
func JoinAll[T Stringer](items []T) string { parts := make([]string, 0, len(items)) for _, item := range items { parts = append(parts, item.String()) } return strings.Join(parts, ", ")}泛型型別
除了函式,struct 也可以有型別參數,例如一個型別安全的 Stack:
type Stack[T any] struct { items []T}
func NewStack[T any]() *Stack[T] { return &Stack[T]{}}
func (s *Stack[T]) Push(item T) { s.items = append(s.items, item)}
func (s *Stack[T]) Pop() (T, bool) { var zero T if len(s.items) == 0 { return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}
func (s *Stack[T]) Len() int { return len(s.items)}
// Go 1.27 起,方法也可以宣告自己的型別參數。func (s *Stack[T]) MapTo[U any](f func(T) U) *Stack[U] { result := NewStack[U]() for _, item := range s.items { result.Push(f(item)) } return result}用起來:
s := NewStack[string]()s.Push("a")s.Push("b")
v, ok := s.Pop() // "b", trues.Push(42) // 編譯錯誤:型別已經被鎖成 string
lengths := s.MapTo(func(v string) int { return len(v) })lengths.Push(42) // OK,lengths 是 *Stack[int]- 建立型別實例時不能省略型別參數,
&Stack{}不合法,要寫&Stack[string]{}(所以才常搭一個NewStack[T]()建構函式讓推斷生效)。 - 方法的 receiver 要帶上
[T]。Go 1.27 起,方法本身也能宣告新的型別參數,如上例的MapTo[U any];在舊版 Go,這種需求只能寫成獨立的函式。 - interface 的方法仍然不能宣告型別參數,而且 generic method 不能用來實作 interface method。
標準函式庫已經幫你寫好了
上面示範的 Contains、Max 其實不用自己實作,slices 與 maps 這兩個泛型套件已經涵蓋大部分日常需求:
import ( "maps" "slices")
nums := []int{3, 1, 4, 1, 5}
slices.Contains(nums, 4) // trueslices.Index(nums, 4) // 2slices.Max(nums) // 5slices.Sort(nums) // 就地排序成 [1 1 3 4 5]slices.Reverse(nums)
people := []Person{{Name: "b"}, {Name: "a"}}slices.SortFunc(people, func(x, y Person) int { return cmp.Compare(x.Name, y.Name)})
m := map[string]int{"a": 1, "b": 2}keys := slices.Collect(maps.Keys(m)) // maps.Keys 回傳的是 iteratorslices.Sort(keys) // map 走訪順序隨機,要排序自己來什麼時候不要用泛型
泛型解決的是「同一段邏輯要套在多種型別上」,不是「製造多餘的抽象」:
- 只有一種型別會用到就別寫泛型,等真的出現第二種再改,Go 的慣例一向是先重複再抽象。
- 只需要呼叫方法就用普通 interface。像
func Print(s fmt.Stringer)這種,寫成func Print[T fmt.Stringer](s T)沒有得到任何好處,只是多打字。差別在於泛型保留了具體型別(可以回傳T、放進[]T),一般 interface 則會抹掉。 - 邏輯依型別而異就別硬塞。如果函式裡開始出現對型別分支的判斷,那代表這其實是兩個函式。
總結
| 概念 | 寫法 | 用途 |
|---|---|---|
| 型別參數 | func F[T any](...) | 把型別變成參數 |
| 型別推斷 | F(v) 而非 F[int](v) | 多數情況免手動指定 |
any | [T any] | 不限型別,但不能對值做假設 |
comparable | [T comparable] | 允許 ==、!= |
cmp.Ordered | [T cmp.Ordered] | 允許 <、> |
~ | ~int | ~float64 | 涵蓋底層型別相同的自訂型別 |
| 泛型型別 | type Stack[T any] struct | 型別安全的容器 |
型別參數寫在方括號裡、約束決定你能對值做什麼
延伸閱讀
- Tutorial: Getting started with generics - go.dev
- An Introduction To Generics - The Go Blog
- When To Use Generics - The Go Blog
- slices package - pkg.go.dev