泛型编程模式
泛型让一份代码跑在多种类型上。这里记录四种模式:代码复用、类型安全、接口结合,以及泛型的局限。
4.1 泛型与代码复用
泛型最直接的收益是复用:一份逻辑写一次,多种类型都能用,不用复制粘贴同一段代码。
泛型容器: 列表、栈、队列这类结构最值得做成泛型,装什么类型由使用者定。
示例:
type List[T any] struct {
items []T
}
func (l *List[T]) Add(item T) {
l.items = append(l.items, item)
}
func (l *List[T]) Get(index int) T {
return l.items[index]
}在这个例子中,List结构体是一个通用的列表,可以存储任意类型的数据。
泛型算法: 排序、搜索这类算法同样可以泛型化,类型变了算法不变。
示例:
func Find[T comparable](slice []T, value T) int {
for i, v := range slice {
if v == value {
return i
}
}
return -1
}在这个例子中,Find函数可以在任意可比较类型的切片中查找指定值。
4.2 泛型与类型安全
泛型在提供复用的同时保住类型安全。类型约束限定类型参数的范围,类型检查提前到编译期,运行时就不会冒出类型错误。
类型约束确保类型安全: 约束写明类型参数必须具备的方法,编译器照着检查,不满足就编译不过。
示例:
type Stringer interface {
String() string
}
func PrintStrings[T Stringer](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}在这个例子中,PrintStrings函数的类型参数T被约束为Stringer接口,确保所有传入的类型都实现了String方法。
4.3 泛型与接口的结合
泛型接口把"操作哪些类型"也参数化:接口定义一组通用操作,各类型各自实现。
定义泛型接口: 接口里的方法签名直接使用类型参数。
示例:
type Container[T any] interface {
Add(item T)
Remove() T
}在这个例子中,Container接口定义了Add和Remove方法,任何实现该接口的类型都必须提供这些方法。
实现泛型接口: 实现泛型接口时,需要为特定类型提供接口定义的方法。
示例:
type IntContainer struct {
items []int
}
func (c *IntContainer) Add(item int) {
c.items = append(c.items, item)
}
func (c *IntContainer) Remove() int {
item := c.items[len(c.items)-1]
c.items = c.items[:len(c.items)-1]
return item
}
var _ Container[int] = &IntContainer{}在这个例子中,IntContainer实现了Container[int]接口,提供了Add和Remove方法。
4.4 泛型的局限与注意事项
泛型不是万能的,有几处局限要注意。
类型参数过多: 过多的类型参数会使代码难以理解,应尽量简化。
性能开销: 泛型代码在某些情况下可能引入性能开销,应注意性能分析和优化。
接口约束: 类型参数的约束应尽量明确,避免不必要的约束和复杂性。
4.5 小结
用泛型记住三条:类型参数别堆多,性能敏感处实测,约束写到刚好够用。
参考资料
- Go 官方文档 · Getting started with generics(外部链接)
- 官方博客 · An Introduction To Generics
- 官方博客 · When To Use Generics
- 语言规范 · 类型参数声明