每日一Go-33、Go深入-泛型深度解析--类型推断、约束、any vs T、性能对比、实战模型

10 阅读4分钟

图片

***文末有源码下载链接***

一、为什么需要泛型?

在 Go 1.18 之前,你要么:

  • 为每个类型重复写函数(大量重复代码)

  • 或用 interface{} + 断言(失去类型安全)

  • 或用反射(慢、不安全)

泛型的出现,解决三件事:

  1. 类型安全,不用写断言

  2. 减少重复代码

  3. 比反射快非常多

一句话:Go 泛型是“零成本抽象”的入口。

二、泛型的基本结构

    2.1 函数泛型

func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

    2.2 类型泛型

type Animal[T any] struct {
    data []T
}

    2.3 核心语法提醒:

  • 方括号 [] 放在名字后,不是参数里

  • T 是类型形参,不是值参数

  • 可以有多个类型参数 [K V any]

三、类型推断:Go十怎么自动猜T的?

    x := Min(1, 3) // T = int
    y := Min(1.0, 2.0) // T = float64
    z := Min("hello""Codee君") // T = string

在以下情况自动推断会失效

场景会不会推断
同类型参数

✔️

参数类型不同
没有参数(例如构建泛型类型)

返回值没有显示类型

xx := Min(1,"2") //× 参数类型必须一致
var Cat Animal// ×类型参数不完整 
//必须写成
var Cat Animal[string]

四、约束

这是最令人模糊的部分。泛型真正的逻辑在于:约束=类型必须满足什么能力

    4.1 any:完全不限制

func Print[T any](x T) {
    fmt.Println(x)
}
//等于旧的interface{},但在编译期就是具体类型

    4.2 comparable:允许== !=

func IndexOf[T comparable](arr []T, x T) int {
    for i, v := range arr {
        if v == x {
            return i
        }
    }
    return -1
}
arr := []int{1, 2, 3, 4, 5}
fmt.Println(IndexOf(arr, 3))
fmt.Println(IndexOf(arr, 6))
// 输出
//2
//-1

    4.3 Ordered:可用 <>的类型

// 来自包"golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

    4.4 ~Type:匹配任何以Type为底层类型的类型

type MyInt int
// ~int 表示类型约束:T 可以是 int 或任何以 int 为底层类型的自定义类型(如 MyInt)
func Add[T ~int](a, b T) T {
    return a + b
}
c := Add[MyInt](1, 2)
fmt.Println(c)
//输出
//3

    4.5 接口约束:自定义能力模型

type Number interface {
    ~int | ~float64
}
func Sum[T Number](arr []T) T {
    var sum T
    for _, v := range arr {
        sum += v
    }
    return sum
}
fmt.Println(Sum([]float64{1.1, 2.2, 3.3}))
fmt.Println(Sum([]MyInt{1, 2, 3}))
// 输出
//6.6
//6

五、any vs T:本质区别是什么?

any不等于泛型。any是一种约束,意思是:该类型参数没有任何限制。但T仍然是“泛型参数”,不是空接口

写法意义
T any使用泛型,但无约束
interface{}完全没有泛型行为,需要断言
anyinterface{}的别名
func Get(x any) // x 是 interface{}类型
func Get[T any](x T) // x 是具体类型

后者是泛型,前者不是

六、泛型类型、泛型结构体、泛型方法

    6.1 结构体泛型

type Animal[T any] struct {
    data []T
}

    6.2 方法接收者泛型

func (a *Animal[T]) Append(b T) {
    a.data = append(a.data, b)
}

七、泛型的性能:与接口和反射对比

    7.1 泛型vs接口

项目泛型接口
类型安全

✔️

需要断言
运行时成本✔️调度开销
使用体验稍复杂简单
编译结果每种类型实例化一个版本单一版本   

    泛型性能更好,尤其在高频调用的热路径。

    7.2 泛型vs反射

    泛型完全碾压反射:

  • 无需运行时类型判断

  • 无需unsafe.Pointer

  • 无需大量内存分配

  • 优化空间更大

八、泛型常见错误

    8.1 ×给泛型方法定义自己的类型参数

a
[T]
[U any]
x

    8.2 ×误用any

        any不是万能钥匙,你可能反而失去约束能力

    8.3 ×想用泛型实现“运行时类型变化”

        泛型是在编译器处理的,不能动态变类型。

九、工程实践:什么时候应该用泛型?

    9.1 应该用:

  • 数据结构:栈、队列、集合、树

  • 集合操作:Map、Filter、Reduce

  • 数字运算库

  • 数据仓库模板

  • 业务逻辑确实可复用并且类型不同

    9.2 ×不应该用:

  • 业务场景逻辑简单,不需要复杂抽象

  • 过度泛型导致代码难懂

  • 运行时类型不同(适合用接口+多态)

泛型用于抽象“跨类型但逻辑一致”的代码,不适合抽象“行为不同但名字相似”的场景。

十、实战:通用Repository实现

// 数据资源接口
type IRepo[T IModel] interface {
    // 分页查询
    PageList(c *gin.Context, query *IFilter) (res *response.PageListT[T], err error)
    // 分页查询
    PageListWithSelectOption(c *gin.Context, query *IFilter, selectOpt []string) (res *response.PageListT[T], err error)
    // 查询一个
    One(c *gin.Context, id uint) (res T, err error)
    // 查询一个
    OneWithSelectOption(c *gin.Context, id uint, selectOpt []string) (res T, err error)
    // 根据名称查询
    OneByName(c *gin.Context, name string) (res T, err error)
    // 根据名称查询
    OneByNameWithSelectOption(c *gin.Context, name string, selectOpt []string) (res T, err error)
    // 添加
    Add(c *gin.Context, model T) (newId uint, err error)
    // 更新,传什么就更新什么
    Update(c *gin.Context, updateFields map[string]any, id uint) (updated bool, err error)
    // 删除
    Delete(c *gin.Context, id uint) (deleted bool, err error)
}
// 数据资源接口实现
type Repo[T IModel] struct {
    DB *gorm.DB
}
// 新建一个数据资源
func NewRepo[T IModel](db *gorm.DB) *Repo[T] {
    return &Repo[T]{
        DB: db,
    }
}
// 分页查询数据
func (r *Repo[T]) PageList(c *gin.Context, f *IFilter) (res *response.PageListT[T], err error) {
    db := r.DB
    db = (*f).BuildPageListFilter(c, db)
    offset := ((*f).GetPage() - 1) * (*f).GetPageSize()
    db = db.Model(new(T)).Offset(int(offset)).Limit(int((*f).GetPageSize()))
    objs := make([]T, 0)
    err = db.Find(&objs).Error
    var count int64
    db.Offset(-1).Limit(-1).Select("count(id)").Count(&count)
    res = &response.PageListT[T]{
        List:  objs,
        Pages: response.MakePages(count, (*f).GetPage(), (*f).GetPageSize()),
    }
    return
}

*源码地址*

1、公众号“Codee君”回复“每日一Go”获取源码

2、pan.baidu.com/s/1B6pgLWfS…


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!