你在 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
- 基本型別(
int、long)不能直接用作型別參數,要用Integer、Longwrapper
TypeScript:最靈活的泛型
function identity<T>(arg: T): T {
return arg;
}
// 使用
const result = identity<string>("hello");
const result2 = identity(42); // TypeScript 能推斷 T = numberTypeScript 的泛型非常靈活,支援 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.3Go 的泛型設計刻意保守——沒有 Java 的 wildcard <? extends T>,沒有 TypeScript 的 conditional types,沒有 variance(協變/逆變)的顯式標注。Go team 的態度是「只加確定有用且不增加太多複雜度的功能」。
Go 的泛型是真正的泛型(不是 type erasure)——編譯器為每個具體型別組合生成特化的程式碼(或用 dictionary-based dispatch),runtime 有完整的型別資訊。
什麼時候用泛型
泛型適合的場景:「這個邏輯對多種型別都適用,但需要保留型別資訊」。
適合用泛型:
- 容器型別(Stack、Queue、Set、Map)
- 工具函式(
Map、Filter、Reduce) - 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