一、Go泛型的核心概念
泛型(Generics)是Go 1.18版本引入的核心特性,它解决了Go长期以来"代码复用"的痛点------让函数、结构体、接口能够处理多种类型的数据,而无需为每种类型重复编写几乎相同的代码。
可以把泛型理解为:给代码定义一个"类型占位符",在使用时再指定具体的类型,就像函数的参数是"值的占位符"一样,泛型的类型参数是"类型的占位符"。
二、泛型的基础语法
泛型的核心是类型参数(Type Parameters) 和类型约束(Type Constraints):
- 类型参数 :用方括号
[]声明,格式为[T 类型约束],T是类型占位符(可自定义名称)。 - 类型约束 :限定
T可以接收哪些类型,常用约束:any:等价于interface{},表示任意类型。- 自定义约束:通过
interface定义,限定类型需满足的条件(如拥有某个方法、属于某个类型集合)。
三、实例Go泛型用法
实例1:最基础的泛型函数(解决重复代码问题)
假设你需要实现两个函数:一个求int切片的最大值,一个求float64切片的最大值。在没有泛型时,你需要写两份几乎一样的代码:
go
// 无泛型:求int切片最大值
func MaxInt(s []int) int {
if len(s) == 0 {
panic("切片不能为空")
}
max := s[0]
for _, v := range s[1:] {
if v > max {
max = v
}
}
return max
}
// 无泛型:求float64切片最大值
func MaxFloat64(s []float64) float64 {
if len(s) == 0 {
panic("切片不能为空")
}
max := s[0]
for _, v := range s[1:] {
if v > max {
max = v
}
}
return max
}
用泛型重构后,只需写一个函数即可处理所有可比较大小的数值类型:
go
package main
import "fmt"
// 定义类型约束:限定T必须是可比较大小的数值类型
type Number interface {
int | int64 | float64 | float32 // 类型集合:T只能是这些类型之一
}
// 泛型函数:[T Number] 声明类型参数T,约束为Number
func Max[T Number](s []T) T {
if len(s) == 0 {
panic("切片不能为空")
}
max := s[0]
for _, v := range s[1:] {
if v > max {
max = v
}
}
return max
}
func main() {
// 使用时指定具体类型(也可省略,Go会自动推导)
intSlice := []int{1, 3, 2, 5, 4}
fmt.Println("int最大值:", Max[int](intSlice)) // 输出:5
floatSlice := []float64{1.5, 3.2, 2.8, 5.1}
fmt.Println("float64最大值:", Max(floatSlice)) // 自动推导类型,输出:5.1
}
代码解释:
type Number interface { int | int64 | float64 | float32 }:自定义类型约束,限定T只能是这几种数值类型(支持>比较)。func Max[T Number](s []T) T:[T Number]是类型参数列表,声明了类型参数T并指定约束;函数参数s []T和返回值T都使用了类型参数T。- 调用时可显式指定类型(
Max[int]),也可让Go自动推导(Max(floatSlice))。
实例2:泛型结构体(支持多种类型的容器)
泛型不仅能用于函数,还能用于结构体。比如实现一个通用的"栈"结构,支持存储任意类型的数据:
go
package main
import "fmt"
// 泛型结构体:Stack[T any] 表示栈可以存储任意类型T的数据
type Stack[T any] struct {
elements []T // 底层用切片存储,元素类型为T
}
// 泛型方法:Push 向栈中添加元素
func (s *Stack[T]) Push(v T) {
s.elements = append(s.elements, v)
}
// 泛型方法:Pop 从栈顶弹出元素,返回元素和是否成功
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T // 声明T类型的零值(兼容任意类型的返回)
return zero, false
}
// 取最后一个元素
lastIdx := len(s.elements) - 1
v := s.elements[lastIdx]
s.elements = s.elements[:lastIdx] // 截断切片
return v, true
}
func main() {
// 1. 创建存储int类型的栈
intStack := &Stack[int]{}
intStack.Push(10)
intStack.Push(20)
intStack.Push(30)
v, ok := intStack.Pop()
fmt.Println("int栈弹出:", v, ok) // 输出:30 true
// 2. 创建存储string类型的栈
strStack := &Stack[string]{}
strStack.Push("hello")
strStack.Push("golang")
strStack.Push("generics")
v2, ok2 := strStack.Pop()
fmt.Println("string栈弹出:", v2, ok2) // 输出:generics true
}
代码解释:
type Stack[T any] struct:声明泛型结构体,T any表示T可以是任意类型(any是interface{}的别名)。- 结构体的方法
Push和Pop都使用了类型参数T,因此能处理对应类型的元素。 - 实例化时需指定具体类型(
&Stack[int]{}、&Stack[string]{}),不同类型的栈是完全独立的,不会互相影响。
实例3:带方法约束的泛型(限定类型必须拥有某个方法)
泛型约束不仅能限定类型集合,还能限定类型必须拥有特定方法。比如实现一个通用的"打印"函数,要求传入的类型必须有 String() string 方法:
go
package main
import "fmt"
// 类型约束:限定T必须拥有 String() string 方法
type Stringer interface {
String() string
}
// 自定义类型1:Person
type Person struct {
Name string
Age int
}
// 实现String()方法,满足Stringer约束
func (p Person) String() string {
return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)
}
// 自定义类型2:Book
type Book struct {
Title string
Author string
}
// 实现String()方法,满足Stringer约束
func (b Book) String() string {
return fmt.Sprintf("Book{Title: %s, Author: %s}", b.Title, b.Author)
}
// 泛型函数:只接收满足Stringer约束的类型
func Print[T Stringer](t T) {
fmt.Println("打印内容:", t.String())
}
func main() {
p := Person{Name: "张三", Age: 20}
Print(p) // 输出:打印内容: Person{Name: 张三, Age: 20}
b := Book{Title: "Go泛型入门", Author: "小明"}
Print(b) // 输出:打印内容: Book{Title: Go泛型入门, Author: 小明}
}
代码解释:
type Stringer interface { String() string }:约束T必须实现String() string方法(和Go标准库的fmt.Stringer接口一致)。func Print[T Stringer](t T):只有实现了String()方法的类型才能传入这个函数,保证函数内可以安全调用t.String()。
四、泛型的使用场景总结
- 通用工具函数:如排序、求最值、过滤等,避免为不同类型写重复代码。
- 通用数据结构:如栈、队列、链表、映射等,实现一次定义、多类型复用。
- 类型安全的容器 :相比
interface{},泛型能在编译期检查类型,避免运行时类型断言错误。
总结
- 核心价值 :Go泛型的核心是通过类型参数 和类型约束,实现代码的通用化复用,同时保证类型安全(编译期检查)。
- 基础语法 :泛型函数/结构体通过
[T 约束]声明类型参数,约束可以是any、自定义类型集合(int|float64)或方法约束(String() string)。 - 使用原则:泛型不是"越通用越好",仅在需要处理多种类型且逻辑完全一致时使用;如果逻辑因类型不同而变化,泛型反而会增加复杂度。