一切始于 Go 1.18:为什么泛型不仅仅是一个时髦的特性?
\n\n长期以来,Go 社区一直以语言的简洁性为傲。缺乏泛型编程被认为是有意为之的选择:“我们不需要复杂的模板,我们编写清晰的代码”。但现实终究占了上风。开发者们厌倦了为 int、string 和 float64 复制粘贴函数——或者使用 interface{} 并随后进行类型转换,从而在编译阶段失去安全性。
Go 1.18(2022 年)的发布成为了一场革命。泛型为语言添加了类型参数和约束。现在,Go 程序员可以编写泛型数据结构和算法,而无需牺牲性能和严格的类型安全。
\n\n在本文中,我们将了解 Go 中泛型的工作原理,查看真实的代码示例,并理解何时应该使用它们,何时不应该。开始吧!
\n\n用简单的话说,Go 中的泛型是什么?
\n\n想象一下,你正在编写一个在切片中查找最小元素的函数。没有泛型,你必须创建三个版本:分别用于 int、float64 和 string。或者使用 interface{} 进行类型转换——既慢又不安全。
泛型允许你编写一个函数,该函数适用于满足特定条件的任何类型。语法很简单:在函数名后的方括号中指定类型参数。
\n\n语法:方括号和 any 关键字
\n\npackage main\n\nimport "fmt"\n\n// Min — 用于查找最小值的泛型函数\nfunc Min[T any](a, b T) T {\n // 错误:运算符 < 未为 any 定义\n // if a < b {\n // return a\n // }\n return b\n}\n\n上面的代码无法编译,因为 any 是一个空接口(interface{} 的同义词)。编译器不知道该类型是否支持 < 运算符。需要添加一个约束。
类型约束:constraints
\n\n在 Go 中,约束通过接口来定义。你可以使用 golang.org/x/exp/constraints 包中的内置约束,或者编写自己的约束。
import (\n "fmt"\n "golang.org/x/exp/constraints"\n)\n\n// Min 适用于任何有序类型\nfunc Min[T constraints.Ordered](a, b T) T {\n if a < b {\n return a\n }\n return b\n}\n\nfunc main() {\n fmt.Println(Min(10, 20)) // 10\n fmt.Println(Min(3.14, 2.71)) // 2.71\n fmt.Println(Min("apple", "banana")) // "apple"\n}\n\n请注意:constraints.Ordered 包含所有定义了运算符 <、<=、>、>= 的类型——即 int、float、string。
实战示例:从简单到复杂
\n\n让我们看看 Go 中的泛型如何简化真实代码的编写。
\n\n1. 泛型栈(Stack)
\n\n没有泛型,你将不得不存储 []interface{} 并进行类型断言。有了泛型,代码优雅且安全。
package main\n\nimport "fmt"\n\n// Stack — 泛型数据结构\ntype Stack[T any] struct {\n items []T\n}\n\nfunc (s *Stack[T]) Push(item T) {\n s.items = append(s.items, item)\n}\n\nfunc (s *Stack[T]) Pop() (T, bool) {\n if len(s.items) == 0 {\n var zero T\n return zero, false\n }\n item := s.items[len(s.items)-1]\n s.items = s.items[:len(s.items)-1]\n return item, true\n}\n\nfunc main() {\n intStack := Stack[int]{}\n intStack.Push(1)\n intStack.Push(2)\n val, ok := intStack.Pop()\n fmt.Println(val, ok) // 2 true\n\n strStack := Stack[string]{}\n strStack.Push("hello")\n