讲解

泛型(Go 1.18 引入)让函数和类型可以「参数化类型」:写一次逻辑,对多种类型生效,同时保住编译期类型检查。语法是在函数名后加方括号声明类型参数:func Max[T int | float64](a, b T) T,T 是类型参数,竖线列出允许的类型集合(约束),调用时编译器自动推断 T 的具体类型。

约束(constraint)决定类型参数能做什么:[T any] 完全不限制,但函数体内也只能做所有类型都支持的操作(赋值、比较存进容器);[T constraints.Ordered] 或手写的 int | float64 这类联合约束允许比较运算;还可以用接口做约束。经验是「约束写到刚好够用」——约束越宽函数体内能用的操作越少,越窄可复用的类型越少。

泛型也能定义类型:type Stack[T any] struct { items []T },方法声明里要带上类型参数 func (s *Stack[T]) Push(v T)。标准库的 slices、maps 包(前面见过 slices.Sort)就是泛型的活广告。但要提醒:泛型不是银弹,能用接口、代码生成或干脆写两遍小函数解决的,不要为泛型而泛型——Go 团队的官方建议也是「先写具体代码,重复真的痛了再抽象」。

示例

泛型函数:Max 与 Map:

package main

import "fmt"

func Max[T int | float64](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func Map[T any, R any](s []T, f func(T) R) []R {
	out := make([]R, len(s))
	for i, v := range s {
		out[i] = f(v)
	}
	return out
}

func main() {
	fmt.Println(Max(3, 7))       // T 推断为 int
	fmt.Println(Max(2.5, 1.8))   // T 推断为 float64

	squares := Map([]int{1, 2, 3}, func(n int) int { return n * n })
	fmt.Println(squares)

	lengths := Map([]string{"go", "generics"}, func(s string) int { return len(s) })
	fmt.Println(lengths)
}

泛型类型:一个通用的栈:

package main

import "fmt"

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
	}
	top := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return top, true
}

func main() {
	s := Stack[int]{}
	s.Push(1)
	s.Push(2)
	if v, ok := s.Pop(); ok {
		fmt.Println("弹出:", v)
	}
	fmt.Println("剩余:", s.items)

	names := Stack[string]{}
	names.Push("小明")
	fmt.Println(names.Pop())
}

常见坑

  • 为炫技而泛型:把只被一种类型调用的函数改成泛型,复杂度上升收益为零。官方博客的建议值得背下来:先写具体类型版本,出现真实的第三种用法再泛化。
  • 以为 any 约束里能做比较:T any 的值不能用 ==(除非额外约束 comparable),不能用 >(需要 Ordered)。编译报错时回头检查约束是不是给得太宽。
  • 泛型里返回零值卡壳:return nil 对 T 不适用,正确写法是 var zero T; return zero——切片章节的「零值思维」在泛型里依然有效。
  • 方法上想加新类型参数:Go 不允许在方法上声明额外的类型参数(只能沿用接收者类型的参数)。func (s Stack[T]) MapR any 是语法错误,这种需求要写成独立的泛型函数。
  • 泛型与接口混用无度:泛型解决「同样的逻辑多种类型」,接口解决「不同的行为统一调用」,很多场景两者都行。判断标准:逻辑依赖类型的具体结构用泛型,依赖行为用接口。

小结

类型参数写在方括号里,约束限定可用操作,泛型类型在方法签名里带参数;先具体后抽象。下一节是收官章:Go 最佳实践清单。