Golang Generics
Go 1.18 引入的泛型(Generics)是 Go 历史上最具革命性的特性。它让代码复用和类型安全达到了新高度。
基本语法
泛型函数
// T 是类型参数,any 是约束(表示任意类型)
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
// 使用
ints := []int{1, 2, 3}
strings := Map(ints, func(i int) string {
return fmt.Sprintf("#%d", i)
})
// strings: []string{"#1", "#2", "#3"}泛型类型
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
// 使用
intStack := &Stack[int]{}
intStack.Push(42)
v, ok := intStack.Pop() // v 是 int 类型
stringStack := &Stack[string]{}
stringStack.Push("hello")类型参数推断
Go 1.18+ 支持从函数参数推断类型参数:
// 可以省略类型参数
strings := Map(ints, func(i int) string {
return fmt.Sprintf("#%d", i)
})
// 等价于
strings := Map[int, string](ints, func(i int) string {
return fmt.Sprintf("#%d", i)
})类型约束(Constraints)
约束定义了类型参数必须满足的要求。
内置约束
func Identity[T any](v T) T { return v }
// comparable: 可比较的类型(可用 == 和 !=)
func Index[T comparable](s []T, v T) int {
for i, item := range s {
if item == v {
return i
}
}
return -1
}自定义约束
约束本质上是接口:
// 定义约束
type Number interface {
~int | ~int64 | ~float64 | ~float32
}
// ~ 表示底层类型匹配即可(包括基于这些类型的别名)
type MyInt int
func Add[T Number](a, b T) T {
return a + b
}
// 使用
var a MyInt = 10
var b MyInt = 20
c := Add(a, b) // c 是 MyInt 类型约束组合
type Stringer interface {
String() string
}
// 同时要求 comparable 和 String() 方法
type Printable interface {
comparable
Stringer
}
func PrintEqual[T Printable](a, b T) {
if a == b {
fmt.Println(a.String())
}
}cmp 包
标准库 cmp 包(Go 1.21+)提供了有序类型的约束:
import "cmp"
// cmp.Ordered: 支持 < > <= >= 比较的类型
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}注意:cmp 包只包含 Ordered 约束和 Compare 函数。如需更多约束(如 Signed、Unsigned、Integer 等),可使用 golang.org/x/exp/constraints。
实用示例
1. 泛型链表
type Node[T any] struct {
Value T
Next *Node[T]
}
type LinkedList[T any] struct {
Head *Node[T]
}
func (l *LinkedList[T]) Prepend(v T) {
l.Head = &Node[T]{Value: v, Next: l.Head}
}
func (l *LinkedList[T]) Find(f func(T) bool) *Node[T] {
for n := l.Head; n != nil; n = n.Next {
if f(n.Value) {
return n
}
}
return nil
}2. 泛型 Map 工具
// 过滤
func Filter[T any](s []T, f func(T) bool) []T {
result := make([]T, 0)
for _, v := range s {
if f(v) {
result = append(result, v)
}
}
return result
}
// 归约
func Reduce[T any, U any](s []T, init U, f func(U, T) U) U {
result := init
for _, v := range s {
result = f(result, v)
}
return result
}
// 使用
nums := []int{1, 2, 3, 4, 5, 6}
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
sum := Reduce(evens, 0, func(a, b int) int { return a + b })3. 泛型单例
type Singleton[T any] struct {
instance *T
once sync.Once
}
func (s *Singleton[T]) Get(f func() *T) *T {
s.once.Do(func() {
s.instance = f()
})
return s.instance
}4. 泛型 Repository 模式
type Repository[T any] interface {
Get(id string) (*T, error)
List() ([]*T, error)
Create(t *T) error
Update(t *T) error
Delete(id string) error
}
type User struct {
ID string
Name string
}
// 可以直接使用泛型实现
type InMemoryRepo[T any] struct {
data map[string]*T
}
func NewInMemoryRepo[T any]() *InMemoryRepo[T] {
return &InMemoryRepo[T]{data: make(map[string]*T)}
}高级技巧
1. 类型约束的巧妙用法
// 限制只能是特定结构体的指针
type Entity interface {
*User | *Product | *Order
GetID() string
}
func Save[T Entity](e T) error {
id := e.GetID()
// ...
}2. 泛型方法
Go 不支持泛型方法,只能在泛型类型上定义方法:
// 错误:方法不能有类型参数
type Box struct{}
func (b Box) Map[T any](f func(T) T) T { } // 编译错误
// 正确:类型必须是泛型的
type Box[T any] struct {
value T
}
func (b Box[T]) Map(f func(T) T) Box[T] {
return Box[T]{value: f(b.value)}
}3. 泛型与接口
泛型类型可以实现接口:
type Container interface {
Len() int
}
type Box[T any] struct {
items []T
}
func (b *Box[T]) Len() int { return len(b.items) }
// Box[T] 实现了 Container 接口
var _ Container = (*Box[int])(nil)常见陷阱
1. 零值问题
func First[T any](s []T) T {
if len(s) == 0 {
var zero T // 返回类型的零值
return zero
}
return s[0]
}
// 更好的做法是返回可选值
func SafeFirst[T any](s []T) (T, bool) {
if len(s) == 0 {
var zero T
return zero, false
}
return s[0], true
}2. 约束过松
// 太松:任何类型都可以,但函数内部用了 + 操作
func Add[T any](a, b T) T {
return a + b // 编译错误!
}
// 正确:使用约束限制
func Add[T Number](a, b T) T {
return a + b
}3. 约束过紧
// 过紧:只接受 int
func Double(s []int) []int {
result := make([]int, len(s))
for i, v := range s {
result[i] = v * 2
}
return result
}
// 更好:使用泛型
func Double[T Number](s []T) []T {
result := make([]T, len(s))
for i, v := range s {
result[i] = v * 2
}
return result
}4. 滥用泛型
不是所有地方都需要泛型:
// 不需要泛型:简单场景
func PrintInts(s []int) {
for _, v := range s {
fmt.Println(v)
}
}
// 过度设计
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
// 只有当你需要处理多种类型且保持类型安全时,才用泛型性能考虑
1. 编译时特化
Go 编译器会为每个具体类型生成特化代码:
// 编译器会生成两份代码:
ints := Map([]int{1, 2, 3}, func(i int) int { return i * 2 })
strings := Map([]string{"a", "b"}, func(s string) string { return s + "!" })
// 一份用于 int,一份用于 string2. 避免不必要的泛型
对于性能敏感且类型固定的场景,直接使用具体类型:
// 泛型版本(有轻微开销)
func Sum[T Number](s []T) T {
var sum T
for _, v := range s {
sum += v
}
return sum
}
// 具体类型版本(更快)
func SumInt64(s []int64) int64 {
var sum int64
for _, v := range s {
sum += v
}
return sum
}3. 内存布局
泛型类型的内存布局与具体类型相同,没有额外开销。
最佳实践
1. 优先使用标准库
Go 1.21+ 标准库提供了很多泛型工具:
import (
"slices"
"maps"
)
// 排序
slices.Sort([]int{3, 1, 2})
// 查找
idx := slices.Index([]string{"a", "b"}, "b")
// Map 克隆
m2 := maps.Clone(m1)2. 约束要恰到好处
// 太松
func Process[T any](v T) { }
// 太紧
func Process[T int | int64](v T) { }
// 恰到好处
func Process[T Number](v T) { }3. 文档要清晰
// Sort 对切片进行排序
// T 必须是有序类型(支持 < 比较)
func Sort[T cmp.Ordered](s []T) {
// ...
}4. 测试要覆盖多种类型
func TestMin(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"ints", 1, 2, 1},
{"negative", -1, -2, -2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Min(tt.a, tt.b); got != tt.want {
t.Errorf("Min(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
}
})
}
}总结
Go 泛型的核心价值:
| 特性 | 收益 |
|---|---|
| 类型参数 | 代码复用,减少重复 |
| 类型约束 | 编译期类型安全 |
| 类型推断 | 使用简洁 |
| 编译时特化 | 运行时零开销 |
使用原则:
- 需要处理多种类型且保持类型安全 → 用泛型
- 类型固定或只需要一种类型 → 不用泛型
- 需要
interface{}+ 类型断言 → 优先考虑泛型 - 性能敏感且类型固定 → 用具体类型


