你在 Go 1.17 想寫一個 Stack(後進先出的資料結構):

// 方法一:為每個型別寫一個 Stack
type IntStack struct {
    items []int
}
 
type StringStack struct {
    items []string
}
 
// 如果你有 10 個型別,你寫 10 個幾乎一模一樣的 struct
// 方法二:用 interface{} 通殺
type Stack struct {
    items []interface{}
}
 
func (s *Stack) Push(item interface{}) {
    s.items = append(s.items, item)
}
 
func (s *Stack) Pop() interface{} {
    // ...
    return s.items[len(s.items)-1]
}
 
// 問題:你 Pop 出來的是 interface{},你要自己做型別斷言
val := stack.Pop().(int)  // 如果不是 int,runtime panic

這兩個方法都不理想:前者程式碼重複,後者失去型別安全。

Generics 是第三條路:寫一次,但型別是參數,在使用時指定。


Generics 的核心概念

// Go 1.18 的 generic Stack
type Stack[T any] struct {
    items []T
}
 
func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}
 
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}
 
// 使用:
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, _ := intStack.Pop()  // val 的型別是 int,不需要斷言

T 是型別參數,any 是 constraint(T 可以是任何型別)。在使用時你指定具體型別,編譯器幫你確保型別正確。


各語言的泛型比較

Java:最早的主流泛型(2004,Java 5)

public class Stack<T> {
    private List<T> items = new ArrayList<>();
 
    public void push(T item) { items.add(item); }
 
    public T pop() {
        return items.remove(items.size() - 1);
    }
}
 
Stack<Integer> intStack = new Stack<>();
intStack.push(1);
int val = intStack.pop();  // 型別安全

Type Erasure(型別抹除)

Java 的泛型在編譯時存在,但在執行時被抹除——Stack<Integer>Stack<String> 在 JVM 裡都是 Stack<Object>。這是為了保持 Java 5 之前的 bytecode 相容性(backward compatibility)做的取捨。

Type erasure 帶來的後果:

  • 你不能在 runtime 做 if (list instanceof List<String>)(型別資訊已經被抹除)
  • 某些泛型用法需要加 unchecked cast
  • 基本型別(intlong)不能直接用作型別參數,要用 IntegerLong wrapper

TypeScript:最靈活的泛型

function identity<T>(arg: T): T {
    return arg;
}
 
// 使用
const result = identity<string>("hello");
const result2 = identity(42);  // TypeScript 能推斷 T = number

TypeScript 的泛型非常靈活,支援 conditional types、mapped types、infer:

// Conditional type:根據型別條件選擇輸出型別
type IsArray<T> = T extends any[] ? true : false;
 
type A = IsArray<string[]>;  // true
type B = IsArray<string>;    // false

這讓 TypeScript 的型別系統幾乎是圖靈完備的,能表達複雜的型別轉換邏輯,但也讓複雜的型別定義變得難以閱讀。

Go 1.18:最晚加入的泛型,但設計更保守

// Constraint:限制型別參數必須滿足的條件
type Number interface {
    int | int64 | float64
}
 
func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}
 
fmt.Println(Sum([]int{1, 2, 3}))      // 6
fmt.Println(Sum([]float64{1.1, 2.2})) // 3.3

Go 的泛型設計刻意保守——沒有 Java 的 wildcard <? extends T>,沒有 TypeScript 的 conditional types,沒有 variance(協變/逆變)的顯式標注。Go team 的態度是「只加確定有用且不增加太多複雜度的功能」。

Go 的泛型是真正的泛型(不是 type erasure)——編譯器為每個具體型別組合生成特化的程式碼(或用 dictionary-based dispatch),runtime 有完整的型別資訊。


什麼時候用泛型

泛型適合的場景:「這個邏輯對多種型別都適用,但需要保留型別資訊」。

適合用泛型

  • 容器型別(Stack、Queue、Set、Map)
  • 工具函式(MapFilterReduce
  • Repository 模式(Repository[T] 讓你有型別安全的 CRUD)
  • 結果包裝(Result[T, E]Optional[T]

不適合用泛型

  • 當不同型別的邏輯實際上不同,只是「看起來像」可以泛化
  • 當 interface 就夠了(「我需要能 Write() 的東西」不需要泛型,用 interface 就好)
  • 過度泛化反而讓程式碼難讀(泛型是工具,不是炫技)

泛型 vs Interface 的選擇(Go 的例子)

Go 1.18 加入泛型之後,很多人問「應該用泛型還是 interface?」

簡單的判斷:

用 interface

你的函式需要接受「滿足某種行為」的值,不需要知道具體型別,也不需要在輸出時保留型別資訊:

func Write(w io.Writer, data []byte) error {
    _, err := w.Write(data)
    return err
}

用泛型

你需要在函式的輸入和輸出之間保留型別關係,或者需要處理多種型別但邏輯完全相同:

func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}
 
ints := []int{1, 2, 3}
strs := Map(ints, strconv.Itoa)  // []string{"1", "2", "3"}
// 沒有泛型,你需要 []interface{} 和 type assertion

下一篇:3 語言實作 Debounce