Go 泛型实战:写一个类型安全的结果包装器

Go 1.18 引入泛型后,最实用的场景之一就是把"返回值 + 错误码"这种固定结构的接口统一起来。本文用一个小例子讲清楚约束怎么写、什么时候该用。 问题:重复的返回结构 每个业务接口都要返回"数据 + 状态码 + 消息",手写三遍很烦:

Go 1.18 引入泛型后,最实用的场景之一就是把”返回值 + 错误码”这种固定结构的接口统一起来。本文用一个小例子讲清楚约束怎么写、什么时候该用。

问题:重复的返回结构

每个业务接口都要返回”数据 + 状态码 + 消息”,手写三遍很烦:

type UserResp struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data *User       `json:"data"`
}

type OrderResp struct {
    Code int         `json:"code"`
    Msg  string      `json:"msg"`
    Data *Order      `json:"data"`
}

字段一模一样,只是 Data 类型不同——这正是泛型的用武之地。

定义通用的 Result

type Result[T any] struct {
    Code int    `json:"code"`
    Msg  string `json:"msg"`
    Data T      `json:"data"`
}

T any 表示任意类型。用法:

func OK[T any](data T) Result[T] {
    return Result[T]{Code: 0, Msg: "success", Data: data}
}

func Fail(code int, msg string) Result[any] {
    return Result[any]{Code: code, Msg: msg}
}

调用时类型由实参推断,不需要显式写类型参数:

r := OK(&User{Name: "tom"})     // Result[*User]
r2 := OK([]Order{})            // Result[[]Order]

加约束:只允许可比较类型

如果要做缓存,key 必须可比较。用约束限定类型:

type Cache[K comparable, V any] struct {
    m map[K]V
    mu sync.RWMutex
}

func (c *Cache[K, V]) Get(k K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.m[k]
    return v, ok
}

comparable 是预声明约束,只支持 == 和 !=。注意切片和 map 不能做 key,编译期就会报错。

自定义约束

想限制”只能是实现了 String 方法的类型”:

type Stringer interface {
    String() string
}

func Join[T Stringer](items []T, sep string) string {
    parts := make([]string, len(items))
    for i, it := range items {
        parts[i] = it.String()
    }
    return strings.Join(parts, sep)
}

多个约束用 | 联合:

type Number interface {
    ~int | ~int64 | ~float64
}

~ 表示”包含以该类型为底层类型的所有类型”,比如 type UserID int 也能通过。

什么时候别用泛型

  • 只有一种类型会用 → 直接写具体类型,可读性更好
  • 类型差异只在某个字段 → 考虑用接口而不是泛型
  • 为了”显得高级” → 泛型会增加编译时间和代码复杂度

泛型的价值在于消除重复的类型声明,不是为了炫技。上面 Result[T] 省掉了十几个几乎一样的结构体,这才是它该出现的地方。