Go 1.18 泛型:从入门到实践
目录
前言
2022 年 3 月,Go 1.18 正式发布,其中最引人瞩目的特性就是泛型(Generics)。这是 Go 语言自 2009 年开源以来最大的一次语法变更,解决了长期被开发者诟病的"没有泛型"的痛点。
为什么需要泛型?
在没有泛型之前,如果我们要写一个反转切片的函数,需要为每种类型写一份:
func ReverseInts(s []int) []int {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}
func ReverseStrings(s []string) []string {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}使用泛型后,只需一个函数就能搞定:
func Reverse[T any](s []T) []T {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}泛型语法速览
1. 泛型函数
// 任意类型,T 必须是可比较的
func Find[T comparable](items []T, target T) int {
for i, v := range items {
if v == target {
return i
}
}
return -1
}2. 泛型类型
// 泛型栈
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}3. 类型约束(constraints)
// 数值类型约束
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}Go 1.18 内置了
constraints包(实验性),包含constraints.Ordered、constraints.Integer等常用约束。Go 1.21 起移到了cmp和math等标准包中。
实际应用场景
场景一:通用的 JSON 响应封装
type ApiResponse[T any] struct {
Code int `json:"code"`
Message string `json:"message"`
Data T `json:"data"`
}
// 使用
func GetUser(w http.ResponseWriter, r *http.Request) {
user := User{Name: "张三", Age: 28}
resp := ApiResponse[User]{
Code: 200,
Message: "success",
Data: user,
}
json.NewEncoder(w).Encode(resp)
}场景二:类型安全的集合操作
// Map 转换
func Map[T, U any](items []T, fn func(T) U) []U {
result := make([]U, len(items))
for i, item := range items {
result[i] = fn(item)
}
return result
}
// Filter 过滤
func Filter[T any](items []T, fn func(T) bool) []T {
var result []T
for _, item := range items {
if fn(item) {
result = append(result, item)
}
}
return result
}
// 使用示例
names := []string{"alice", "bob", "charlie"}
upperNames := Map(names, strings.ToUpper)
// ["ALICE", "BOB", "CHARLIE"]
longNames := Filter(names, func(s string) bool {
return len(s) > 3
})
// ["alice", "charlie"]注意事项
- 性能:泛型不会引入运行时开销,编译器会在编译期对每个具体类型生成特化代码
- 限制:泛型类型不能用于方法(receiver 不能是泛型),但结构体方法可以用
- 可读性:不要过度使用泛型,简单的
interface{}+ 类型断言有时更清晰
总结
Go 泛型的引入让代码复用变得更加优雅,尤其是在编写通用数据结构和工具函数时。推荐的使用原则是:“需要的时候再用,不要为了泛型而泛型”。对于日常业务开发,大部分场景其实用不到泛型,但当遇到重复的类型书写的痛点时,泛型就是最好的解药。