目錄
- 什麼是泛型?
- Go 1.18 為什麼才加入
- 基本語法:Type Parameters
- Type Constraints 型別約束
- 內建約束:any 與 comparable
- 自訂 Constraint Interface
- Type Inference 型別推斷
- 泛型 struct 與 method
- 實戰範例
- 何時用 / 何時不用
- 常見問題
- 總結
- 參考資源
什麼是泛型?
泛型(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),十年沒加泛型,原因:
- 語言哲學:簡單優於完整,泛型容易被濫用
- 介面已夠用:多數 polymorphism 場景能用 interface 解決
- codegen 工具:泛型需求大的場合可用
go generate - 設計謹慎:花十年才找到適合 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]() // ✅ 明確
推斷規則
- 從參數推:函式參數的型別資訊可以推 T
- 不能從 return 推:return type 單獨不能推 T
- 複雜時可能失敗:例如 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+ 已有官方
min、maxbuiltin。
何時用 / 何時不用
✅ 適合用泛型
| 情境 | 範例 |
|---|---|
| 資料容器:跨型別重用結構 | Stack[T]、LRU Cache[K, V] |
| 演算法:邏輯與型別無關 | Sort、Reverse、Map/Filter/Reduce |
| 數值計算:跨數值型別重用 | Sum、Average、Min/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
- ✅ 優先用官方
slices、maps套件 - ❌ 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 更清楚 |
相關閱讀
- Go 型別與介面 — interface 是泛型 constraint 的基礎
- Go 錯誤處理 — 為何 Go 不用
Result[T] - Go 標準庫實戰 — 1.21+ slices/maps 內建泛型 utility
參考資源
- 官方 Generics 教學:https://go.dev/doc/tutorial/generics
- Type Parameters Proposal:https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
- golang.org/x/exp/constraints:https://pkg.go.dev/golang.org/x/exp/constraints
- Go 1.21 slices package:https://pkg.go.dev/slices
- Go 1.21 maps package:https://pkg.go.dev/maps
- Go 1.21 cmp package:https://pkg.go.dev/cmp
建立日期:2026-05-16 最後更新:2026-05-16