Go Generics 完全指南

Go 泛型完整指南 — type parameters、constraints、comparable/any、何時用何時不用、實戰範例


目錄


什麼是泛型?

泛型(Generics)= 同一份程式碼支援多種型別,編譯時根據實際使用產生具體版本。

沒有泛型的痛苦

// 想寫 Max 函式
func MaxInt(a, b int) int { ... }
func MaxFloat64(a, b float64) float64 { ... }
func MaxString(a, b string) string { ... }
// 重複 n 次

// 想存 generic 集合
type IntStack struct { items []int }
type StringStack struct { items []string }
// 重複又重複

有泛型的解法(Go 1.18+)

// 一份程式碼適用多型別
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Max[int](3, 5)           // 5
Max[float64](3.14, 2.71) // 3.14
Max[string]("a", "b")    // "b"

// 編譯器可推斷
Max(3, 5)

核心特點

  • 🎯 型別參數:用 [T any] 宣告
  • 編譯時生成:不是 runtime reflection,無 cast 成本
  • 🔧 約束系統:用 interface 限制 T 必須有哪些方法 / 屬於哪組型別
  • 📦 保留型別安全:編譯時就抓型別錯誤
  • 🚀 支援 1.18+:較舊版本不支援

Go 1.18 為什麼才加入

Go 從 1.0(2012)到 1.18(2022),十年沒加泛型,原因:

  1. 語言哲學:簡單優於完整,泛型容易被濫用
  2. 介面已夠用:多數 polymorphism 場景能用 interface 解決
  3. codegen 工具:泛型需求大的場合可用 go generate
  4. 設計謹慎:花十年才找到適合 Go 的泛型語法(不像 C++ 那麼複雜)

Go 泛型的設計取捨

特性
Type parameter on functions
Type parameter on types (struct)
Type parameter on methods 不支援(受限)
Generic specialization 不能對特定 T 寫不同邏輯
Variance(co/contravariant) 簡單但限制多

基本語法:Type Parameters

函式泛型

// 語法:func Name[TypeParam constraint](args) returnType
func Identity[T any](v T) T {
    return v
}

// 呼叫(編譯器推斷)
Identity(42)         // T = int
Identity("hello")    // T = string
Identity[float64](3) // 明確指定(int 3 會被 cast 成 float64)

多個型別參數

func Pair[K, V any](k K, v V) (K, V) {
    return k, v
}

func Convert[From, To any](src From, fn func(From) To) To {
    return fn(src)
}

Convert(42, strconv.Itoa) // T 推斷為 (int, string)

用於 slice / map

// 取 slice 第一個元素(如果有)
func First[T any](s []T) (T, bool) {
    var zero T
    if len(s) == 0 {
        return zero, false
    }
    return s[0], true
}

// 過濾 slice
func Filter[T any](s []T, pred func(T) bool) []T {
    var result []T
    for _, v := range s {
        if pred(v) {
            result = append(result, v)
        }
    }
    return result
}

evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })
// [2, 4]

Type Constraints 型別約束

T 不能裸 any 用所有運算 — 你只能用 T 介面有定義的操作

// ❌ T 是 any,不能用 +
func Sum[T any](a, b T) T {
    return a + b // 編譯錯:operator + not defined on T
}

Constraint 是 interface

Constraint 用 interface 表示:

// 自訂約束:T 必須支援 +
type Addable interface {
    int | int64 | float64 | string
}

func Sum[T Addable](a, b T) T {
    return a + b // OK,因為 T 限定為這幾個型別
}

|type union(型別聯集),表示 T 可以是其中任一。

Approximation 約束(~T)

type Number interface {
    ~int | ~int64 | ~float64
}

~int 表示「底層是 int 的任何型別」,包含:

type Celsius float64 // ~float64 包含 Celsius
type UserID int      // ~int 包含 UserID

不加 ~ 則必須是精確 int / float64,不含衍生型別。


內建約束:any 與 comparable

Go 內建兩個常用約束:

any

// any 是 interface{} 的別名
func Identity[T any](v T) T { return v }

T 可以是任何型別,但只能用 interface 的零方法。

comparable

// comparable: 支援 == 和 != 的型別
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item {
            return true
        }
    }
    return false
}

Contains([]int{1, 2, 3}, 2)              // true
Contains([]string{"a", "b"}, "c")        // false
Contains([]float64{1.0}, 1.0)            // true

// ❌ slice 不可比較,不滿足 comparable
type T struct { items []int }
Contains([]T{}, T{}) // 編譯錯

cmp.Ordered(Go 1.21+)

import "cmp"

// 支援 <, <=, >, >= 的型別
func Max[T cmp.Ordered](a, b T) T {
    if a > b { return a }
    return b
}

Max(1, 2)            // int
Max(1.5, 2.5)        // float64
Max("apple", "banana") // string

cmp.Ordered 涵蓋:整數族、浮點族、string


自訂 Constraint Interface

當需求複雜時,自己定義 constraint。

純型別 union

type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

含方法的 constraint

type Stringer interface {
    String() string
}

func PrintAll[T Stringer](items []T) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

但這跟 interface 沒差:

// 不用泛型,直接收 []Stringer 也行
func PrintAll(items []Stringer) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

差別:泛型版本可以保留具體型別

func Map[T, U any](s []T, fn func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

// 輸入 []int,輸出 []string — 型別都保留
strs := Map([]int{1, 2, 3}, strconv.Itoa) // []string

Union + 方法(嚴格)

// T 必須是 int/int64/float64 且實作 Stringer
type NumberStringer interface {
    int | int64 | float64
    String() string // 必須實作
}

但這常常太嚴格,多數人不這樣寫。


Type Inference 型別推斷

呼叫端通常不用顯式寫型別參數,編譯器會推斷:

func First[T any](s []T) T { ... }

First([]int{1, 2, 3})    // T = int 推斷
First[int]([]int{1,2,3}) // 明確(多餘)

推斷失敗時要明確

func Empty[T any]() []T {
    return nil
}

Empty()       // ❌ 沒任何線索能推斷 T
Empty[int]()  // ✅ 明確

推斷規則

  1. 從參數推:函式參數的型別資訊可以推 T
  2. 不能從 return 推:return type 單獨不能推 T
  3. 複雜時可能失敗:例如 nested type、type alias

部分指定

func Convert[From, To any](src From, fn func(From) To) To { ... }

// 完全推斷
Convert(42, strconv.Itoa)

// 部分指定也行(Go 1.21+)
Convert[int](42, strconv.Itoa)

泛型 struct 與 method

泛型 struct

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

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

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

// 使用
s := &Stack[int]{}
s.Push(1)
s.Push(2)
v, _ := s.Pop() // 2

泛型 map

type SafeMap[K comparable, V any] struct {
    mu sync.RWMutex
    m  map[K]V
}

func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
    return &SafeMap[K, V]{m: make(map[K]V)}
}

func (s *SafeMap[K, V]) Set(k K, v V) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[k] = v
}

func (s *SafeMap[K, V]) Get(k K) (V, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.m[k]
    return v, ok
}

// 使用
users := NewSafeMap[string, *User]()
users.Set("alice", &User{Name: "Alice"})

Method 不能新增 type parameter

type Container[T any] struct{}

// ❌ method 不能加自己的型別參數
func (c Container[T]) Map[U any](fn func(T) U) Container[U] { ... }

這是 Go 泛型刻意的限制。需要這種轉換時用 function:

func MapContainer[T, U any](c Container[T], fn func(T) U) Container[U] {
    // ...
}

實戰範例

範例 1:常用 slice utility

package slices

func Map[T, U any](s []T, fn func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

func Filter[T any](s []T, pred func(T) bool) []T {
    var result []T
    for _, v := range s {
        if pred(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](s []T, init U, fn func(U, T) U) U {
    acc := init
    for _, v := range s {
        acc = fn(acc, v)
    }
    return acc
}

func Contains[T comparable](s []T, v T) bool {
    for _, x := range s {
        if x == v {
            return true
        }
    }
    return false
}

func Unique[T comparable](s []T) []T {
    seen := make(map[T]struct{}, len(s))
    var result []T
    for _, v := range s {
        if _, ok := seen[v]; !ok {
            seen[v] = struct{}{}
            result = append(result, v)
        }
    }
    return result
}

// 使用
nums := []int{1, 2, 3, 4, 5}

doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6, 8, 10]

evens := Filter(nums, func(n int) bool { return n%2 == 0 })
// [2, 4]

sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
// 15

unique := Unique([]string{"a", "b", "a", "c"})
// ["a", "b", "c"]

Go 1.21+ 已有官方 slices 套件,提供 Map/Filter 等大部分 utility。優先用官方版。

範例 2:Result 型別(替代 (T, error) tuple)

type Result[T any] struct {
    Value T
    Err   error
}

func Ok[T any](v T) Result[T] {
    return Result[T]{Value: v}
}

func Err[T any](err error) Result[T] {
    var zero T
    return Result[T]{Value: zero, Err: err}
}

func (r Result[T]) Unwrap() (T, error) {
    return r.Value, r.Err
}

// 使用(通常 Go 不這樣寫,這只是示例語法)
func divide(a, b int) Result[int] {
    if b == 0 {
        return Err[int](errors.New("divide by zero"))
    }
    return Ok(a / b)
}

r := divide(10, 2)
if r.Err != nil { ... }
fmt.Println(r.Value)

這不是 Go 風格。Go 慣例是 (T, error) tuple return。Result type 是其他語言(Rust)風格,硬塞到 Go 反而怪。

範例 3:LRU Cache

package lru

import "container/list"

type entry[K comparable, V any] struct {
    key   K
    value V
}

type Cache[K comparable, V any] struct {
    cap   int
    items map[K]*list.Element
    order *list.List
}

func New[K comparable, V any](capacity int) *Cache[K, V] {
    return &Cache[K, V]{
        cap:   capacity,
        items: make(map[K]*list.Element),
        order: list.New(),
    }
}

func (c *Cache[K, V]) Get(k K) (V, bool) {
    var zero V
    if el, ok := c.items[k]; ok {
        c.order.MoveToFront(el)
        return el.Value.(*entry[K, V]).value, true
    }
    return zero, false
}

func (c *Cache[K, V]) Set(k K, v V) {
    if el, ok := c.items[k]; ok {
        el.Value.(*entry[K, V]).value = v
        c.order.MoveToFront(el)
        return
    }

    if c.order.Len() >= c.cap {
        oldest := c.order.Back()
        if oldest != nil {
            c.order.Remove(oldest)
            delete(c.items, oldest.Value.(*entry[K, V]).key)
        }
    }

    el := c.order.PushFront(&entry[K, V]{k, v})
    c.items[k] = el
}

// 使用
cache := lru.New[string, *User](100)
cache.Set("alice", &User{Name: "Alice"})
u, _ := cache.Get("alice")

範例 4:通用 Min/Max(用 cmp.Ordered)

import "cmp"

func Min[T cmp.Ordered](a, b T) T {
    if a < b { return a }
    return b
}

func Max[T cmp.Ordered](a, b T) T {
    if a > b { return a }
    return b
}

func Clamp[T cmp.Ordered](v, lo, hi T) T {
    return Max(lo, Min(hi, v))
}

Clamp(15, 0, 10) // 10
Clamp(-5, 0, 10) // 0
Clamp(5, 0, 10)  // 5

Go 1.21+ 已有官方 minmax builtin。


何時用 / 何時不用

✅ 適合用泛型

情境 範例
資料容器:跨型別重用結構 Stack[T]LRU Cache[K, V]
演算法:邏輯與型別無關 SortReverseMap/Filter/Reduce
數值計算:跨數值型別重用 SumAverageMin/Max
無法 cast 的場景:保留型別資訊 Map[int → string]

❌ 不適合用泛型

情境 原因 替代
行為多型(不同型別不同行為) 用 interface 更清楚 type Sharer interface { Share() }
單一具體型別 增加複雜度沒收益 直接寫 func SumInts(...)
API 邊界(套件對外) 增加使用者學習成本 介面或具體型別
效能極致追求 編譯時生成可能增加 binary size 手寫對應版本
強約束太麻煩 constraint 寫起來很冗 介面

取捨原則

先寫具體版本,重複到第 3 次再考慮抽成泛型

// 第 1 版:先寫具體
func SumInts(nums []int) int { ... }

// 第 2 次需求:再寫一份
func SumFloats(nums []float64) float64 { ... }

// 第 3 次:開始抽
func Sum[T int | float64](nums []T) T { ... }

Generic vs Interface 決策

要做的事是「演算法、容器」?
└─ 是 → 泛型可能合適
   └─ 多個具體型別需要重用?
      └─ 是 → 用泛型
      └─ 否 → 直接寫具體版本

要做的事是「不同型別不同行為」?
└─ 是 → 用 interface
   例如:Save() 給 DB 一種寫法、給 S3 一種寫法

常見問題

問題 1:constraint 寫不出來

症狀:想限制 T 是「能 +、-、*、/ 的數值」,但全部寫一遍好累

解決:用 union 或社群 constraint

// 社群方案:golang.org/x/exp/constraints
import "golang.org/x/exp/constraints"

func Sum[T constraints.Integer](nums []T) T { ... }
func MaxF[T constraints.Float](nums []T) T { ... }

問題 2:T 內部要 new instance 怎麼辦

症狀:泛型 function 內想 new(T)T{} 不行

func Make[T any]() T {
    return T{} // ❌ T 可能是 int,T{} 沒意義
}

解決:用 zero value

func Make[T any]() T {
    var zero T
    return zero
}

或要求 T 實作工廠介面:

type Factory[T any] interface {
    New() T
}

問題 3:能對特定 T 寫不同邏輯嗎?

不能(specialization)。Go 泛型刻意限制這點。

// ❌ 不能這樣
func Sum[T Number](nums []T) T {
    if T == string {
        // 特殊處理
    }
}

替代:用 type switch(拿到具體值後 switch)或寫多個函式。

問題 4:performance vs 不用泛型

通常幾乎沒差,因為編譯器會為每個具體型別生成獨立版本(dictionary based codegen + monomorphization)。

但 binary size 會稍微大(多種 instantiation)。

問題 5:method 不能加新 type parameter 怎麼辦

type Container[T any] struct{}

// ❌ method 不能加 U
func (c Container[T]) Map[U any](fn func(T) U) Container[U]

解決:抽成 package-level function

func MapContainer[T, U any](c Container[T], fn func(T) U) Container[U] {
    // ...
}

問題 6:comparable 跟 == 對 nil 的處理

func Find[T comparable](s []T, target T) int {
    for i, v := range s {
        if v == target { return i }
    }
    return -1
}

// pointer 也是 comparable(比指標位址)
type User struct{ Name string }
users := []*User{{"a"}, {"b"}}
Find(users, &User{"a"}) // -1(不是同一個指標)

要比結構內容用 slices.IndexFunc + 自訂 equal。


總結

核心要點

泛型 = func F[T constraint](...) + 編譯期 monomorphization
constraint 用 interface 寫(union | ~T | 加方法)
內建:any、comparable
1.21+:cmp.Ordered、min/max builtin、slices package

設計原則

  • ✅ 重複 3 次以上再抽泛型
  • ✅ 容器、演算法、數值計算最適合
  • ✅ 不同型別不同行為用 interface
  • ✅ 優先用官方 slicesmaps 套件
  • ❌ Method 不能加新 type parameter(抽成 function)
  • ❌ 不支援 specialization

速查表

// 函式泛型
func F[T any](v T) T

// 多型別參數
func G[K comparable, V any](m map[K]V) []K

// 約束 union
type Addable interface { int | string }

// Approximation
type Number interface { ~int | ~float64 }

// 內建
any                 // 任何型別
comparable          // 支援 ==
cmp.Ordered         // 支援 <, >, ≤, ≥(1.21+)

// 結構泛型
type Stack[T any] struct { items []T }
func (s *Stack[T]) Push(v T) { ... }

何時用速查

場景
容器(Stack/Queue/Cache) ✅ 泛型
Map/Filter/Reduce ✅ 泛型(或 slices 套件)
Min/Max/Sum ✅ 泛型(或 builtin)
不同型別不同行為 ❌ 用 interface
純粹避免重複 cast 看情況,多數時候 any + cast 也行
對外 API 入口 ⚠️ 通常用具體型別或 interface 更清楚

相關閱讀


參考資源


建立日期:2026-05-16 最後更新:2026-05-16

🔗相關文章