GO [ 泛型 ]

发布时间:2026/9/30 12:13:03

GO [ 泛型 ] 泛型前面我们已经学习了 Go 的变量、常量、数据类型、输入输出、条件控制、切片、字符串、映射表、指针、结构体、函数、方法、接口、类型、错误、文件和反射。接下来学习 Go 语言中一个很重要、也最容易被“写复杂”的能力泛型Generics。泛型解决的问题并不是“让 Go 变成 C 模板”而是让我们把算法和数据结构中真正与具体类型无关的部分复用起来。例如下面两个函数的业务逻辑完全一样只是参数类型不同func SumInts(values []int) int { var total int for _, value : range values { total value } return total } func SumFloats(values []float64) float64 { var total float64 for _, value : range values { total value } return total }如果继续支持int32、int64、float32就需要继续复制函数。复制几次并不可怕可怕的是后面修改算法时必须同步修改多份代码。泛型允许我们把“元素类型暂时留空”把这个类型作为参数传给函数或数据结构func Sum[T int | float64](values []T) T { var total T for _, value : range values { total value } return total }T不是一个运行时的值而是一个类型参数。调用Sum([]int{1, 2, 3})时编译器会把T推断为int调用Sum([]float64{1.5, 2.5})时T就是float64。按照 Go 官方博客《An Introduction To Generics》 的概括泛型给 Go 增加了三项核心能力函数和类型可以声明类型参数接口可以表示类型集合而不仅仅是方法集合编译器可以在很多调用场景中自动推断类型参数。本文按照“问题引入 → 官方定义 → 类型参数 → 类型约束 → 类型集合 → 类型推断 → 泛型类型 → 接口对比 → 实现数据结构 → 使用边界”的顺序展开重点学习泛型函数和泛型类型的声明方式类型参数、类型实参和实例化any、comparable、联合约束和~为什么约束决定了泛型代码可以执行哪些操作显式传入类型参数与类型推断使用泛型实现栈、队列、集合和单向链表泛型与接口的分工为什么“能用泛型”不等于“应该使用泛型”。本文代码在 go1.27.0 darwin/arm64 环境中实际编译运行。不同 Go 版本对标准库 API、错误文本和编译器优化的输出可能略有不同。泛型基础语法从 Go 1.18 开始支持。为什么需要泛型没有泛型时的重复代码先实现一个查找切片最大值的函数package main import fmt func MaxInt(values []int) int { if len(values) 0 { return 0 } maximum : values[0] for _, value : range values[1:] { if value maximum { maximum value } } return maximum } func MaxString(values []string) string { if len(values) 0 { return } maximum : values[0] for _, value : range values[1:] { if value maximum { maximum value } } return maximum } func main() { fmt.Println(MaxInt([]int{3, 8, 2})) fmt.Println(MaxString([]string{go, rust, c})) }运行结果8 rust这两段函数除了类型名和元素类型其他代码没有任何区别。我们可以使用any把参数改成[]any但那样会失去比较能力func MaxAny(values []any) any { // if values[0] values[1] { ... } // 编译错误any 中的值不保证支持小于号比较 return values[0] }any只表示“可以接收任意类型”不表示“这些类型拥有相同的操作”。泛型的关键就在于允许任意类型但必须明确这些类型共同支持哪些操作。接口能不能替代泛型在 Go 1.18 之前常见的通用写法是使用空接口func PrintAll(values []interface{}) { for _, value : range values { fmt.Println(value) } }这种写法可以接收任何元素但调用者取出元素后需要进行类型断言func FirstInt(values []interface{}) (int, bool) { if len(values) 0 { return 0, false } value, ok : values[0].(int) return value, ok }它有三个问题元素类型信息在装入接口时被隐藏错误可能推迟到运行时才发现每次取值都要做类型断言或类型 switch。泛型把类型检查放回编译期func First[T any](values []T) (T, bool) { if len(values) 0 { var zero T return zero, false } return values[0], true }First([]int{10, 20})返回intFirst([]string{Go})返回string。调用者不需要断言编译器也会检查类型是否正确。这并不意味着泛型会取代接口。接口描述的是运行时行为泛型描述的是编译期类型关系。后文会专门对比两者。泛型的三个基本概念泛型代码里经常出现三个词类型参数、类型约束、类型实参。类型参数类型参数写在函数名或类型名后面的方括号中func Identity[T any](value T) T { return value }这里的T就是类型参数。它在函数声明时还没有具体类型调用函数时才会被替换成某个实际类型。类型参数和普通参数的区别如下对比项普通参数类型参数写法func f(value int)func f[T any](value T)表示一个运行时值一个编译期类型占位符传入位置小括号方括号作用决定本次调用的输入数据决定本次实例化的具体类型类型约束any是interface{}的别名表示允许所有类型func Identity[T any](value T) T { return value }但any没有告诉编译器T支持哪些运算。下面的代码不能编译func Add[T any](left, right T) T { return left right }编译器会拒绝left right因为any的类型集合中包含结构体、切片、函数等不能使用的类型。要执行加法约束必须缩小到支持加法的类型集合type Number interface { int | int64 | float32 | float64 } func Add[T Number](left, right T) T { return left right }约束既决定“调用者可以传什么类型”也决定“函数体里可以对类型参数做什么操作”。类型实参调用泛型函数时方括号中的具体类型叫类型实参result : Add[int64](10, 20)这里T是类型参数int64是类型实参Add[int64]表示把T实例化为int64的泛型函数。如果编译器能够从普通参数推断出类型实参可以省略方括号result : Add(10, 20)这两种调用的含义相同。泛型函数一个最小的泛型函数先实现一个返回切片长度的泛型函数package main import fmt func Length[T any](values []T) int { return len(values) } func main() { fmt.Println(Length([]int{10, 20, 30})) fmt.Println(Length([]string{Go, 泛型})) }运行结果3 2这里T只出现在[]T中函数不需要对元素做任何操作所以约束使用any就足够了。显式指定类型实参如果希望让代码更直观可以显式写出类型numbers : []int{1, 2, 3} length : Length[int](numbers) fmt.Println(length)显式类型实参常见于以下场景函数参数中没有直接出现类型参数编译器无法从参数推断类型代码需要强调某个类型转换或实例化结果教学代码需要展示类型参数和类型实参的对应关系。类型推断官方入门教程使用下面的求和函数说明了类型推断func SumIntsOrFloats[K comparable, V int64 | float64](values map[K]V) V { var total V for _, value : range values { total value } return total }K必须是comparable因为 map 的 key 必须能够使用和!V只能是int64或float64因为函数体使用了加法。调用时可以显式指定类型ints : map[string]int64{a: 10, b: 20} floats : map[string]float64{a: 1.5, b: 2.5} fmt.Println(SumIntsOrFloats[string, int64](ints)) fmt.Println(SumIntsOrFloats[string, float64](floats))也可以让编译器从map[K]V参数中推断fmt.Println(SumIntsOrFloats(ints)) fmt.Println(SumIntsOrFloats(floats))运行结果30 4 30 4官方教程在 Getting started with generics 中明确指出编译器主要根据普通函数参数和形参之间的类型关系推断类型实参如果函数没有参数可以提供类型信息就通常需要显式写出类型参数。没有参数时不能推断下面这个函数没有普通参数编译器没有任何输入可以用来推断Tfunc Zero[T any]() T { var zero T return zero } func main() { value : Zero[int]() fmt.Println(value) }不能写成// value : Zero() // 编译错误无法推断 T因为“返回值接收变量的类型”不会反向参与普通的类型参数推断。需要显式写成Zero[int]()。类型约束约束是接口Go 语言规范把类型约束定义为一种接口它规定类型参数允许使用的类型集合并控制类型参数值支持的操作。最简单的约束是单个类型func Double[T int](value T) T { return value * 2 }这个函数只能接收int不能接收int64fmt.Println(Double(10)) // fmt.Println(Double(int64(10))) // 编译错误如果希望接收多种类型可以使用联合元素type Integer interface { int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr }然后func Double[T Integer](value T) T { return value * 2 }此时T可以是这些整数类型中的任意一种但一次具体调用中仍然只有一个确定类型。T不是“同时拥有所有类型”而是“在编译时被替换成其中一个类型”。~表示底层类型直接写int只允许类型实参是int写~int则允许所有底层类型为int的定义类型type UserID int type Integer interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr } func Double[T Integer](value T) T { return value * 2 } func main() { var id UserID 7 fmt.Println(Double(id)) }运行结果14如果约束写成inttype OnlyInt interface { int }那么UserID不满足约束因为UserID是一个新的定义类型。~int的意思可以理解为“底层类型是 int 的类型集合”。这也是泛型约束和普通接口最容易混淆的地方普通接口常用方法描述行为泛型约束可以用类型元素描述一组底层类型。comparablecomparable是 Go 预声明的约束表示类型可以使用和!进行比较。它常用于 map 的 key、去重集合和查找func Contains[T comparable](values []T, target T) bool { for _, value : range values { if value target { return true } } return false }可以传入整数、字符串、数组和字段都可比较的结构体fmt.Println(Contains([]int{1, 2, 3}, 2)) fmt.Println(Contains([]string{Go, Rust}, Python)) fmt.Println(Contains([][2]int{{1, 2}}, [2]int{1, 2}))不能传入切片// Contains([][]int{{1, 2}}, []int{1, 2}) // 编译错误[]int 不满足 comparable切片虽然可以和nil比较但不能和另一个切片使用比较所以不满足comparable。方法约束约束也可以要求类型实现某个方法type Stringer interface { String() string } func JoinWithComma[T Stringer](values []T) string { result : for i, value : range values { if i 0 { result , } result value.String() } return result }定义一个实现String()的类型type User struct { Name string } func (user User) String() string { return user.Name }调用users : []User{{Name: Alice}, {Name: Bob}} fmt.Println(JoinWithComma(users))运行结果Alice,Bob这里的Stringer既可以被普通接口使用也可以作为泛型约束使用。区别在于作为约束时它同时限制类型参数的可用类型和函数体可以调用的方法。组合方法和类型集合约束可以同时包含方法和类型元素但带有类型元素的接口只能作为类型约束不能声明普通变量type SignedNumber interface { ~int | ~int64 | ~float32 | ~float64 }下面这样是合法的func Abs[T SignedNumber](value T) T { if value 0 { return -value } return value }下面这样不合法// var value SignedNumber // 编译错误包含类型元素的接口只能作为约束使用这套设计来自 Go 1.18 的泛型方案。类型集合是给编译器检查类型参数用的不是一个可以在运行时保存任意成员的接口值。泛型类型泛型不仅可以用于函数还可以用于定义结构体、切片类型、链表节点和容器。泛型结构体定义一个可以保存任意类型的盒子type Box[T any] struct { value T } func (box Box[T]) Value() T { return box.value }使用时必须实例化泛型类型intBox : Box[int]{value: 100} stringBox : Box[string]{value: Go} fmt.Println(intBox.Value()) fmt.Println(stringBox.Value())运行结果100 GoBox[int]和Box[string]是两个不同的具体类型。它们共享同一个泛型定义但不能互相赋值。泛型类型的方法泛型类型的方法接收者需要声明对应的类型参数type Pair[A, B any] struct { First A Second B } func (pair Pair[A, B]) Swap() Pair[B, A] { return Pair[B, A]{ First: pair.Second, Second: pair.First, } }调用pair : Pair[string, int]{First: age, Second: 18} swapped : pair.Swap() fmt.Printf(%#v\n, swapped)运行结果main.Pair[int,string]{First:18, Second:age}方法接收者中的A、B对应Pair定义里的类型参数。方法不能凭空使用没有声明的类型参数。Go 1.27 的泛型方法如果你使用的是 Go 1.27具体类型的方法本身也可以声明新的类型参数。例如把一个List[E]的元素转换成另一种类型type List[E any] []E func (list List[E]) Map[R any](transform func(E) R) List[R] { result : make(List[R], len(list)) for i, value : range list { result[i] transform(value) } return result } func main() { words : List[int]{1, 2, 3}.Map(func(value int) string { return fmt.Sprintf(item-%d, value) }) fmt.Println(words) }运行结果[item-1 item-2 item-3]这里E是List自己的类型参数R是Map方法新增的类型参数所以List[int]可以转换成List[string]。Go 官方在 Generic Methods 中说明了这项能力它让类型相关的转换操作可以留在类型的方法上调用链也更自然。但泛型方法仍然不能凭空让接口拥有泛型方法。下面的接口声明仍然是不允许的// type Transformer interface { // Map[R any](func(int) R) []R // }接口方法不能声明自己的类型参数。需要跨类型复用算法时可以使用泛型函数需要把操作组织在具体类型上时可以使用 Go 1.27 的泛型方法。泛型类型不能被直接使用下面的写法缺少类型实参// var box Box // 编译错误泛型类型必须实例化正确写法是var box Box[int]泛型类型的实例化和泛型函数调用有一个重要区别函数调用经常可以依靠参数推断而声明变量时通常必须明确写出类型参数。使用泛型实现数据结构接下来不只停留在语法层面我们实现一个小型的泛型容器包。这个练习包含四种数据结构Stack[T]后进先出栈Queue[T]先进先出队列Set[T comparable]去重集合LinkedList[T]泛型单向链表。这些数据结构的共同特点是算法不依赖具体元素类型但容器必须保证类型安全。泛型栈 Stack栈只需要保存一个[]T。使用切片作为底层存储Push追加元素Pop删除最后一个元素。type Stack[T any] struct { items []T } func (stack *Stack[T]) Push(value T) { stack.items append(stack.items, value) } func (stack *Stack[T]) Pop() (T, bool) { if len(stack.items) 0 { var zero T return zero, false } last : len(stack.items) - 1 value : stack.items[last] // 释放被弹出元素的引用避免 T 是指针时长期占用对象。 var zero T stack.items[last] zero stack.items stack.items[:last] return value, true } func (stack *Stack[T]) Len() int { return len(stack.items) }完整测试package main import fmt type Stack[T any] struct { items []T } func (stack *Stack[T]) Push(value T) { stack.items append(stack.items, value) } func (stack *Stack[T]) Pop() (T, bool) { if len(stack.items) 0 { var zero T return zero, false } last : len(stack.items) - 1 value : stack.items[last] var zero T stack.items[last] zero stack.items stack.items[:last] return value, true } func (stack *Stack[T]) Len() int { return len(stack.items) } func main() { var numbers Stack[int] numbers.Push(10) numbers.Push(20) value, ok : numbers.Pop() fmt.Println(value, ok, numbers.Len()) var names Stack[string] names.Push(Go) name, ok : names.Pop() fmt.Println(name, ok) }运行结果20 true 1 Go true这里的Stack[int]和Stack[string]使用相同的栈算法但每个实例的元素类型在编译期已经确定。向Stack[int]压入字符串会直接编译失败而不是等到运行时才发现。泛型队列 Queue队列可以用切片实现。为了避免每次出队都复制剩余元素我们维护一个head下标type Queue[T any] struct { items []T head int } func (queue *Queue[T]) Enqueue(value T) { queue.items append(queue.items, value) } func (queue *Queue[T]) Dequeue() (T, bool) { if queue.head len(queue.items) { var zero T return zero, false } value : queue.items[queue.head] var zero T queue.items[queue.head] zero queue.head // 所有元素都出队后恢复零值状态。 if queue.head len(queue.items) { queue.items nil queue.head 0 } return value, true } func (queue *Queue[T]) Len() int { return len(queue.items) - queue.head }测试func main() { var queue Queue[string] queue.Enqueue(first) queue.Enqueue(second) first, _ : queue.Dequeue() second, _ : queue.Dequeue() _, ok : queue.Dequeue() fmt.Println(first) fmt.Println(second) fmt.Println(empty:, !ok, length:, queue.Len()) }运行结果first second empty: true length: 0Queue[T]的关键不是泛型语法而是让数据结构的存储类型、入队参数和出队返回值始终保持同一个T。如果不用泛型通常只能为每种元素类型复制一个队列或者把元素装进interface{}后不断断言。泛型集合 Set集合需要判断元素是否相等因此类型参数必须满足comparabletype Set[T comparable] struct { items map[T]struct{} } func NewSet[T comparable]() *Set[T] { return Set[T]{items: make(map[T]struct{})} } func (set *Set[T]) Add(value T) { set.items[value] struct{}{} } func (set *Set[T]) Remove(value T) { delete(set.items, value) } func (set *Set[T]) Contains(value T) bool { _, ok : set.items[value] return ok } func (set *Set[T]) Len() int { return len(set.items) }测试func main() { set : NewSet[string]() set.Add(Go) set.Add(泛型) set.Add(Go) fmt.Println(set.Contains(Go)) fmt.Println(set.Contains(Rust)) fmt.Println(set.Len()) set.Remove(Go) fmt.Println(set.Contains(Go)) }运行结果true false 2 falseNewSet没有普通参数因此无法根据参数推断T调用时需要显式写成NewSet[string]()。这正好说明了泛型函数类型推断的边界。如果尝试创建Set[[]int]// invalid : NewSet[[]int]()编译器会报错因为切片不能作为 map key也不满足comparable。泛型单向链表链表节点保存一个元素和下一个节点指针type Node[T any] struct { Value T Next *Node[T] } type LinkedList[T any] struct { head *Node[T] len int } func (list *LinkedList[T]) PushFront(value T) { list.head Node[T]{ Value: value, Next: list.head, } list.len } func (list *LinkedList[T]) PopFront() (T, bool) { if list.head nil { var zero T return zero, false } value : list.head.Value list.head list.head.Next list.len-- return value, true } func (list *LinkedList[T]) Len() int { return list.len }遍历链表func (list *LinkedList[T]) Values() []T { values : make([]T, 0, list.len) for node : list.head; node ! nil; node node.Next { values append(values, node.Value) } return values }测试func main() { var list LinkedList[int] list.PushFront(1) list.PushFront(2) list.PushFront(3) fmt.Println(list.Values()) value, ok : list.PopFront() fmt.Println(value, ok, list.Values()) }运行结果[3 2 1] 3 true [2 1]这里Node[T]的Next必须写成*Node[T]不能写成不带类型参数的*Node。泛型类型被使用时必须实例化这条规则同样适用于递归结构。一个完整的泛型工具示例前面的代码分别演示了语法。接下来把常见的泛型函数组合起来实现一个小型工具文件package main import fmt type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } func Min[T Ordered](left, right T) T { if left right { return left } return right } func Max[T Ordered](left, right T) T { if left right { return left } return right } func Map[T any, R any](values []T, transform func(T) R) []R { result : make([]R, len(values)) for i, value : range values { result[i] transform(value) } return result } func Filter[T any](values []T, keep func(T) bool) []T { result : make([]T, 0, len(values)) for _, value : range values { if keep(value) { result append(result, value) } } return result } func Reduce[T any](values []T, initial T, combine func(T, T) T) T { result : initial for _, value : range values { result combine(result, value) } return result } func main() { numbers : []int{1, 2, 3, 4, 5} squares : Map(numbers, func(value int) int { return value * value }) even : Filter(numbers, func(value int) bool { return value%2 0 }) total : Reduce(numbers, 0, func(left, right int) int { return left right }) fmt.Println(min:, Min(10, 3)) fmt.Println(max:, Max(10, 3)) fmt.Println(squares:, squares) fmt.Println(even:, even) fmt.Println(total:, total) }运行结果min: 3 max: 10 squares: [1 4 9 16 25] even: [2 4] total: 15Map使用两个类型参数输入元素类型T和输出元素类型R。这说明泛型并不要求输入和输出是同一种类型。Map可以把[]int转换成[]stringlabels : Map([]int{1, 2, 3}, func(value int) string { return fmt.Sprintf(item-%d, value) }) fmt.Println(labels)运行结果[item-1 item-2 item-3]Filter的输入输出类型相同所以只需要一个类型参数。Reduce使用一个初始值和二元合并函数把一组值折叠成一个结果。泛型与接口的区别泛型和接口都能帮助我们写“通用代码”但抽象的方向不同。泛型关注类型关系下面的函数接收一个切片并返回同类型的第一个元素func First[T any](values []T) (T, bool) { if len(values) 0 { var zero T return zero, false } return values[0], true }输入是[]User输出就是User输入是[]int输出就是int。类型之间的关系在编译期保留了下来。接口关注行为替换下面的函数只关心参数能不能写入func Save(writer interface{ Write([]byte) (int, error) }, data []byte) error { _, err : writer.Write(data) return err }调用者可以传入文件、网络连接、内存缓冲区或测试替身只要它们实现Write方法即可。这里并不关心具体类型之间的关系而是关心运行时对象提供的行为。什么时候使用泛型参考 Go 官方博客 When To Use Generics可以先遵循下面几条经验函数操作的是切片、map、channel 等语言内置容器并且算法不依赖元素的具体类型多个类型实现完全相同的算法复制代码会造成维护负担需要构建类型安全的容器例如栈、队列、集合和树输入和输出之间存在明确的类型关系例如Map[T, R]。什么时候优先使用接口下面这些场景通常优先考虑接口调用者需要替换不同实现例如数据库、文件系统和消息队列类型之间的共同点是方法行为而不是底层类型需要把不同具体类型放进同一个运行时集合代码本身就是依赖注入、插件系统或测试替身。不要为了泛型而泛型下面这个函数没有必要泛型化func PrintUser(user User) { fmt.Println(user.Name) }如果写成func PrintValue[T any](value T) { fmt.Println(value) }反而丢失了User的业务语义也没有获得有价值的复用。官方建议是先写清楚普通代码等重复模式和类型关系真的出现后再引入类型参数。泛型不是设计工作的起点通常是从重复代码中提炼出的结果。泛型中的零值泛型代码无法直接写出一个适用于所有T的字面量零值func Zero[T any]() T { var zero T return zero }var zero T是泛型代码获取零值的标准写法。也可以使用命名返回值func Find[T any](values []T, predicate func(T) bool) (result T, ok bool) { for _, value : range values { if predicate(value) { return value, true } } return result, false }不能把nil返回给任意Tfunc Invalid[T any]() T { // return nil // 编译错误T 不一定是指针、切片、map 或接口 var zero T return zero }如果函数的语义确实需要空值通常返回(T, bool)或(T, error)不要试图把所有类型都强行当成指针处理。泛型代码中的常见错误把any当成可比较类型func Equal[T any](left, right T) bool { // return left right // 编译错误 return false }正确写法func Equal[T comparable](left, right T) bool { return left right }约束没有覆盖函数体中的操作type Number interface { ~int | ~float64 } func Multiply[T Number](left, right T) T { return left * right }如果把约束换成any*就不能使用。编写约束时应先看函数体真正使用了哪些操作再决定类型集合。把类型集合当成运行时接口type TextOrNumber interface { ~string | ~int } // var value TextOrNumber // 编译错误只能作为约束类型集合用于编译期约束不是用于运行时保存值。需要运行时保存不同类型时应使用普通接口或any并明确处理类型断言。误以为~int等于inttype UserID int type OnlyInt interface { int } type AnyInt interface { ~int }UserID满足AnyInt但不满足OnlyInt。如果库希望支持调用者自定义的业务类型通常应考虑使用~。在不需要时声明复杂约束下面这种约束看起来很专业但如果函数只是遍历切片就没有必要type HugeConstraint interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string | ~bool }正确的约束应该尽可能小。只读元素就使用any需要比较就使用comparable需要排序才定义有序类型集合。约束越复杂调用者越难理解编译错误也越难读。忽略并发安全泛型只解决类型复用不会自动给容器增加并发安全。下面的Stack[T]仍然不能被多个 goroutine 同时写入// stack.Push(1) 和 stack.Pop() 并发执行仍然可能产生数据竞争如果需要并发容器应当在结构体中加入sync.Mutex或使用专门的并发设计并通过go test -race验证。官方实现与设计理解Go 官方在 Go 1.18 Release Notes 中把泛型描述为一次较大的语言变化同时提醒开发者只在合适的场景使用泛型。这个提醒的重点不是“泛型不可靠”而是泛型会增加语言和 API 的复杂度应该让复用收益覆盖复杂度成本。从语言机制看一次泛型调用大致经历下面的过程泛型声明 | | 类型参数 类型约束 v 调用泛型函数或实例化泛型类型 | | 显式类型实参或由编译器推断 v 检查类型实参是否满足约束 | | 检查函数体中的运算是否合法 v 生成/使用具体类型实例这里有三个容易混淆的层次类型约束描述一组可能的类型一次具体调用中的类型参数代表其中一个确定类型泛型代码可以使用的操作是所有候选类型共同允许的操作。例如type Number interface { ~int | ~float64 } func Add[T Number](left, right T) T { return left right }Number的类型集合包含int和float64的定义类型但Add的一次调用不允许让left是int、right是float64。两个参数都必须是同一个推断出来的T。fmt.Println(Add(1, 2)) fmt.Println(Add(1.5, 2.5)) // Add(1, 2.5) // 编译器无法让两个参数同时满足同一个 T泛型的安全性来自编译期约束而不是运行时检查。类型不满足约束时程序不会进入运行阶段。一个可运行的完整示例下面把栈、队列、集合和链表放在同一个文件中作为本章最终练习。保存为generics_demo.go后执行go run generics_demo.gopackage main import fmt type Stack[T any] struct { items []T } func (stack *Stack[T]) Push(value T) { stack.items append(stack.items, value) } func (stack *Stack[T]) Pop() (T, bool) { if len(stack.items) 0 { var zero T return zero, false } last : len(stack.items) - 1 value : stack.items[last] var zero T stack.items[last] zero stack.items stack.items[:last] return value, true } func (stack *Stack[T]) Len() int { return len(stack.items) } type Queue[T any] struct { items []T head int } func (queue *Queue[T]) Enqueue(value T) { queue.items append(queue.items, value) } func (queue *Queue[T]) Dequeue() (T, bool) { if queue.head len(queue.items) { var zero T return zero, false } value : queue.items[queue.head] var zero T queue.items[queue.head] zero queue.head if queue.head len(queue.items) { queue.items nil queue.head 0 } return value, true } func (queue *Queue[T]) Len() int { return len(queue.items) - queue.head } type Set[T comparable] struct { items map[T]struct{} } func NewSet[T comparable]() *Set[T] { return Set[T]{items: make(map[T]struct{})} } func (set *Set[T]) Add(value T) { set.items[value] struct{}{} } func (set *Set[T]) Contains(value T) bool { _, ok : set.items[value] return ok } func (set *Set[T]) Len() int { return len(set.items) } type Node[T any] struct { Value T Next *Node[T] } type LinkedList[T any] struct { head *Node[T] len int } func (list *LinkedList[T]) PushFront(value T) { list.head Node[T]{Value: value, Next: list.head} list.len } func (list *LinkedList[T]) Values() []T { values : make([]T, 0, list.len) for node : list.head; node ! nil; node node.Next { values append(values, node.Value) } return values } func main() { var stack Stack[int] stack.Push(1) stack.Push(2) value, ok : stack.Pop() fmt.Println(stack:, value, ok, stack.Len()) var queue Queue[string] queue.Enqueue(A) queue.Enqueue(B) first, _ : queue.Dequeue() fmt.Println(queue:, first, queue.Len()) set : NewSet[string]() set.Add(Go) set.Add(Go) set.Add(泛型) fmt.Println(set:, set.Contains(Go), set.Len()) var list LinkedList[int] list.PushFront(10) list.PushFront(20) fmt.Println(list:, list.Values()) }运行结果stack: 2 true 1 queue: A 1 set: true 2 list: [20 10]这个示例里没有使用反射也没有使用any保存所有元素。每个容器都在实例化时确定元素类型Stack[int]只保存intQueue[string]只保存stringSet[string]只保存可比较的stringLinkedList[int]的每个节点都保存int。这就是泛型最实际的价值复用数据结构实现同时保留编译期类型安全。泛型速查表写法含义func F[T any](value T)泛型函数T可以是任意类型type Box[T any] struct{ value T }泛型结构体F[int](10)显式指定类型实参F(10)尝试让编译器推断类型实参T comparableT必须支持和!T ~intT的底层类型必须是intT int | stringT只能是int或stringvar zero T获取泛型类型T的零值map[K]VK的约束通常必须是comparableStack[T]实例化泛型类型总结泛型的核心不是方括号而是把“算法”和“具体类型”分开类型参数让函数和类型可以延迟确定具体元素类型类型约束规定允许哪些类型以及泛型代码可以使用哪些操作any适合不需要操作元素的通用代码comparable适合比较、去重和 map key联合约束和~适合表达一组底层类型类型推断可以简化调用但没有普通参数提供线索时仍需显式指定类型泛型类型适合实现类型安全的栈、队列、集合、链表等数据结构接口负责运行时行为抽象泛型负责编译期类型关系抽象泛型不是所有重复代码的答案约束过度会让 API 变复杂。如果把本文内容压缩成一个实践判断可以记住下面这句话当多个类型需要执行同一套算法而且类型之间的关系可以在编译期表达时优先考虑泛型当调用方只需要替换不同实现时优先考虑接口。官方资料Go 官方语言规范Type parametersGo 官方语言规范Type constraintsGo 官方博客An Introduction To GenericsGo 官方教程Getting started with genericsGo 官方博客When To Use GenericsGo 官方博客Generic MethodsGo 1.18 Release NotesGenerics
延伸阅读

更多相关文章

2026/9/30 12:13:03

STM32上电启动全流程解析:从复位向量到RTOS第一个任务

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 12:13:03

国产车规MCU首次量产主动悬架:从工程视角拆解核心技术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 13:03:13

Windows Server 2022部署全流程:初始化、远程管理与安全加固

Windows Server 2022 装完的那一刻,很多人第一反应是直接挂业务、装环境、跑服务,但我自己的习惯恰恰相反——先花点时间把这台机器"打理干净"。一套新系统从裸装到能安心托付业务,中间的配置环节远比安装本身更考验人。选什么版本…

2026/9/30 13:03:13

2026年AI配音做PPT讲解怎么选?

做PPT讲解、汇报材料、课程课件时,很多人卡在最后一步:PPT做完了,配音却不知道怎么弄。如果是几十页的PPT,逐页真人录音不仅耗时间,后面修改一页内容,还可能需要重新录整段。AI配音更适合这种需要反复修改、…

2026/9/30 13:03:13

JDK切换实战指南:多版本共存的环境变量配置与排坑

经常有同事问我:你电脑上装了几个 JDK?我一般回答三个,8、17、21,按需切。JDK切换看着是个基础操作,但真动起手来,环境变量配置失败、java -version和javac -version各说各话、IDEA 里编译还是旧版本&#…

2026/9/30 13:03:13

特价股投资:如何动态评估地缘科技与数字经济主权风险

做特价股票投资这些年,我最有价值的一次认知转变,是发现自己盯着的那些“便宜科技股”,绝大部分的便宜都带着某种隐藏标签。这个标签可能是地缘科技层面的规则变动,可能是数字经济主权问题带来的合规成本,也可能是数据…

2026/9/29 11:07:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 0:01:22

MATLAB+Yalmip+CPLEX实战:综合能源系统优化调度全流程解析

做综合能源系统优化调度这活儿,最痛苦的不是建模本身,而是模型写完之后不知道该怎么求解。看论文里轻飘飘一句“采用Yalmip调用CPLEX求解”,自己上手时却往往卡在环境配置、变量声明、约束写法和求解状态判读上,一耗就是两三天。这…

2026/9/30 0:01:22

I3C比I2C快10倍?RK3576实战:速率、DTS配置与混合总线避坑指南

I3C 比 I2C 快 10 倍?这句话在嵌入式群里传了很久,每次都能吵出一堆截图。前段时间我正好在 RK3576 上调板级 I3C 接口,从控制器寄存器一路摸到 Linux DTS 配置,踩了不少坑,也把这笔速度账彻底算明白了。本文就用 RK35…

2026/9/30 0:01:22

字符串转对象:JSON.parse、new Function与URLSearchParams

“字符串转对象”这几个字,我在技术群里见过的问法至少有十几种:有人拿着一串{a:1,b:2}说 JSON.parse 直接报错,有人要从 URL 里抠出参数,还有人只是想把abc变成能挂属性的东西。js 这门语言里,字符串和对象之间的转换…

2026/9/29 3:53:39

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/29 9:46:12

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/30 10:28:53

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑