泛型:Go 的类型参数革命

学习 Go 1.18 引入的泛型特性,掌握类型参数、约束和泛型数据结构

泛型:Go 的类型参数革命

Go 1.18 引入了一个期待已久的特性——泛型(Generics)。这是 Go 语言诞生以来最大的语法变化之一。

泛型让你能写出更通用、更灵活的代码,而不需要牺牲类型安全。今天我们就来深入学习 Go 的泛型。

为什么需要泛型?

假设你要写一个求最大值的函数:

// 只能处理 int
func MaxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// 只能处理 float64
func MaxFloat64(a, b float64) float64 {
	if a > b {
		return a
	}
	return b
}

// 只能处理 string
func MaxString(a, b string) string {
	if a > b {
		return a
	}
	return b
}

逻辑完全一样,只是因为类型不同就要写多个版本。这就是泛型要解决的问题。

泛型函数

基本语法

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

// T 是类型参数,constraints.Ordered 是约束
func Max[T constraints.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() {
	fmt.Println(Max(3, 5))           // int: 5
	fmt.Println(Max(3.14, 2.71))     // float64: 3.14
	fmt.Println(Max("apple", "banana")) // string: banana
}

语法说明:

  • [T constraints.Ordered]T 是类型参数,constraints.Ordered 是约束
  • 函数签名中使用 T 代替具体类型
  • 调用时 Go 会自动推断类型,也可以显式指定:Max[int](3, 5)

类型约束

内置约束

Go 提供了几个内置约束:

// any = interface{},任意类型
func Print[T any](v T) {
	fmt.Println(v)
}

// comparable,可比较的类型(支持 == 和 !=)
func Contains[T comparable](slice []T, target T) bool {
	for _, v := range slice {
		if v == target {
			return true
		}
	}
	return false
}

constraints 包

golang.org/x/exp/constraints 提供了常用的约束:

import "golang.org/x/exp/constraints"

// constraints.Integer:所有整数类型
// constraints.Float:所有浮点数类型
// constraints.Complex:所有复数类型
// constraints.Ordered:可排序的类型(整数、浮点数、字符串)
// constraints.Signed:有符号数值
// constraints.Unsigned:无符号数值

自定义约束

package main

import "fmt"

// 自定义约束:数值类型
type Number interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
	~float32 | ~float64
}

// 使用自定义约束
func Sum[T Number](numbers []T) T {
	var sum T
	for _, n := range numbers {
		sum += n
	}
	return sum
}

func main() {
	ints := []int{1, 2, 3, 4, 5}
	floats := []float64{1.1, 2.2, 3.3}
	
	fmt.Println("整数和:", Sum(ints))     // 15
	fmt.Println("浮点和:", Sum(floats))   // 6.6
}

注意 ~int 中的 ~ 符号,它表示"底层类型是 int",包括所有基于 int 的自定义类型。

带方法的约束

package main

import "fmt"

// 约束:必须有 String() 方法
type Stringer interface {
	String() string
}

// 自定义类型
type UserID int

func (u UserID) String() string {
	return fmt.Sprintf("User#%d", u)
}

type ProductID string

func (p ProductID) String() string {
	return fmt.Sprintf("Product(%s)", p)
}

// 泛型函数
func PrintIDs[T Stringer](ids []T) {
	for _, id := range ids {
		fmt.Println(id.String())
	}
}

func main() {
	users := []UserID{1, 2, 3}
	products := []ProductID{"A001", "B002"}
	
	PrintIDs(users)
	PrintIDs(products)
}

泛型结构体

package main

import "fmt"

// 泛型栈
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) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	
	lastIndex := len(s.items) - 1
	item := s.items[lastIndex]
	s.items = s.items[:lastIndex]
	return item, true
}

func (s *Stack[T]) Peek() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	return s.items[len(s.items)-1], true
}

func (s *Stack[T]) Size() int {
	return len(s.items)
}

func main() {
	// int 栈
	intStack := &Stack[int]{}
	intStack.Push(1)
	intStack.Push(2)
	intStack.Push(3)
	
	for intStack.Size() > 0 {
		item, _ := intStack.Pop()
		fmt.Println("弹出:", item)
	}
	
	// string 栈
	stringStack := &Stack[string]{}
	stringStack.Push("apple")
	stringStack.Push("banana")
	
	for stringStack.Size() > 0 {
		item, _ := stringStack.Pop()
		fmt.Println("弹出:", item)
	}
}

实战:泛型工具集

Map、Filter、Reduce

package main

import "fmt"

// Map:转换每个元素
func Map[T any, 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
}

// Filter:过滤元素
func Filter[T any](slice []T, f func(T) bool) []T {
	result := make([]T, 0)
	for _, v := range slice {
		if f(v) {
			result = append(result, v)
		}
	}
	return result
}

// Reduce:聚合
func Reduce[T any, U any](slice []T, initial U, f func(U, T) U) U {
	result := initial
	for _, v := range slice {
		result = f(result, v)
	}
	return result
}

func main() {
	numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	
	// Map:每个数乘以 2
	doubled := Map(numbers, func(n int) int {
		return n * 2
	})
	fmt.Println("乘以2:", doubled)
	
	// Filter:保留偶数
	evens := Filter(numbers, func(n int) bool {
		return n%2 == 0
	})
	fmt.Println("偶数:", evens)
	
	// Reduce:求和
	sum := Reduce(numbers, 0, func(acc, n int) int {
		return acc + n
	})
	fmt.Println("和:", sum)
	
	// 组合使用:偶数的平方和
	result := Reduce(
		Map(
			Filter(numbers, func(n int) bool { return n%2 == 0 }),
			func(n int) int { return n * n },
		),
		0,
		func(acc, n int) int { return acc + n },
	)
	fmt.Println("偶数的平方和:", result)
}

泛型 Set

package main

import "fmt"

type Set[T comparable] struct {
	items map[T]struct{}
}

func NewSet[T comparable]() *Set[T] {
	return &Set[T]{
		items: make(map[T]struct{}),
	}
}

func (s *Set[T]) Add(item T) {
	s.items[item] = struct{}{}
}

func (s *Set[T]) Remove(item T) {
	delete(s.items, item)
}

func (s *Set[T]) Contains(item T) bool {
	_, ok := s.items[item]
	return ok
}

func (s *Set[T]) Size() int {
	return len(s.items)
}

func (s *Set[T]) ToSlice() []T {
	result := make([]T, 0, len(s.items))
	for item := range s.items {
		result = append(result, item)
	}
	return result
}

// 集合运算
func (s *Set[T]) Union(other *Set[T]) *Set[T] {
	result := NewSet[T]()
	for item := range s.items {
		result.Add(item)
	}
	for item := range other.items {
		result.Add(item)
	}
	return result
}

func (s *Set[T]) Intersect(other *Set[T]) *Set[T] {
	result := NewSet[T]()
	for item := range s.items {
		if other.Contains(item) {
			result.Add(item)
		}
	}
	return result
}

func (s *Set[T]) Difference(other *Set[T]) *Set[T] {
	result := NewSet[T]()
	for item := range s.items {
		if !other.Contains(item) {
			result.Add(item)
		}
	}
	return result
}

func main() {
	set1 := NewSet[int]()
	set1.Add(1)
	set1.Add(2)
	set1.Add(3)
	
	set2 := NewSet[int]()
	set2.Add(2)
	set2.Add(3)
	set2.Add(4)
	
	fmt.Println("set1:", set1.ToSlice())
	fmt.Println("set2:", set2.ToSlice())
	fmt.Println("并集:", set1.Union(set2).ToSlice())
	fmt.Println("交集:", set1.Intersect(set2).ToSlice())
	fmt.Println("差集:", set1.Difference(set2).ToSlice())
}

泛型的限制

不能用于方法接收者的类型参数

// ❌ 不允许
type Container[T any] struct {
	value T
}

func (c *Container[T]) Method[U any](u U) {
	// 方法不能有自己的类型参数
}

// ✅ 允许
func (c *Container[T]) Method() T {
	return c.value
}

不能嵌入类型参数

// ❌ 不允许
type Wrapper[T any] struct {
	T  // 不能嵌入类型参数
}

何时使用泛型?

适合使用泛型的场景:

  1. 数据结构(栈、队列、集合等)
  2. 算法实现(排序、搜索等)
  3. 工具函数(Map、Filter、Reduce 等)
  4. 处理切片、map、channel 的通用函数

不适合使用泛型的场景:

  1. 业务逻辑(通常类型是确定的)
  2. 可以用接口解决的问题
  3. 过度抽象

小结

今天我们学习了 Go 的泛型:

  1. 泛型函数:类型参数和类型推断
  2. 类型约束:内置约束和自定义约束
  3. 泛型结构体:泛型栈、集合等
  4. 实战应用:Map、Filter、Reduce
  5. 限制:方法类型参数、嵌入限制

泛型是 Go 的一个重要里程碑,它让代码更通用而不失类型安全。但不要为了用泛型而用泛型,简单直接仍然是 Go 的核心哲学。

练习时间

  1. 实现一个泛型的二叉搜索树
  2. 写一个泛型缓存,支持过期时间
  3. 实现泛型的 Priority Queue
  4. 创建一个泛型的 Result 类型(类似 Rust 的 Result<T, E>)

我们下篇见!

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页