如何看待 Go 1.18 引入的泛型?对 Go 开发者来说是必须掌握的吗?

Go 语言在 1.18 版本正式引入了泛型,这事儿在国内 Go 圈子里炸过一波。说实话,我一开始觉得"不就是模板嘛,C++早就有了",但真正上手写了几次之后发现------这东西的爽点,比你想象的多得多。

以前写 Go,想要一个函数同时处理 int、float64、string 的切片,你得写出三份几乎一模一样的代码,或者硬上 interface{} 然后做类型断言。说真的,写起来浑身难受。

有了泛型之后,一行代码搞定:

bash 复制代码
func print[T any](arr []T) {
    for _, v := range arr {
        fmt.Print(v, " ")
    }
    fmt.Println("")
}

[T any] 里的 any 就相当于说"不管啥类型,你来吧"。同样的函数,int 切片能用,string 切片能用,float64 切片也能用。编译期就帮你把类型确定好了,性能和手写的版本没差。

comparable 是个好东西

写个查找算法的时候,你会发现 any 不够用了------== 操作符在泛型类型上是不一定能用的。这时候就需要 comparable 约束:

bash 复制代码
func find[T comparable](arr []T, elem T) int {
    for i, v := range arr {
        if v == elem {
            return i
        }
    }
    return -1
}

讲道理,这种设计挺 Go 的------你明确告诉我这个类型支持什么操作,我才让你做。编译期就把问题堵死,不让你运行时踩坑。

用泛型手搓数据结构

泛型最大的价值之一,就是写数据结构。以前我们要写一个泛型的栈,要么上 interface{} 然后到处断言,要么用 go generate 生成代码。前者不安全,后者麻烦。

现在直接写:

bash 复制代码
type stack[T any] []T

func (s *stack[T]) push(elem T) {
    *s = append(*s, elem)
}

func (s *stack[T]) pop() {
    if len(*s) > 0 {
        *s = (*s)[:len(*s)-1]
    }
}

func (s *stack[T]) top() *T {
    if len(*s) > 0 {
        return &(*s)[len(*s)-1]
    }
    return nil
}

注意这里 top() 返回的是指针,而不是值。为啥?因为泛型 T 的"零值"不确定------int 是 0,string 是空串,但指针的零值永远是 nil,用一个指针就能统一处理空栈的情况。这是我写的时候踩过的坑,希望对你有帮助。

双向链表的实现同理,用 node[T comparable] 来定义结点,泛型的 == 直接支持删除操作。之前写链表只能"一条路走到黑"用一个类型,现在想放 int 就放 int,想放 string 就放 string,贼灵活。

函数式编程三板斧

说完了数据结构,再聊聊函数式编程那套。这是 Go 社区之前用反射写了无数次、代码丑到没法看的东西。

Map------把一个切片映射成另一个切片,类型可以不一样:

bash 复制代码
func gMap[T1 any, T2 any](arr []T1, f func(T1) T2) []T2 {
    result := make([]T2, len(arr))
    for i, elem := range arr {
        result[i] = f(elem)
    }
    return result
}

比如把数字切片映射成中文数字字符串,或者把字符串全转大写,一行调用搞定。

Reduce------把一堆数据合成一个:

bash 复制代码
func gReduce[T1 any, T2 any](arr []T1, init T2, f func(T2, T1) T2) T2 {
    result := init
    for _, elem := range arr {
        result = f(result, elem)
    }
    return result
}

我个人觉得这个写法还不够优雅,每次都要传初始值,但总比写循环强吧?

Filter------按条件过滤:

bash 复制代码
func gFilterIn[T any](arr []T, f func(T) bool) []T {
    result := []T{}
    for _, elem := range arr {
        if f(elem) {
            result = append(result, elem)
        }
    }
    return result
}

你懂的,这三个函数一组合,数据处理就跟搭积木似的。

做个业务场景试试水

来看一个实际的例子。我们有个员工列表:

bash 复制代码
type Employee struct {
    Name     string
    Age      int
    Vacation int
    Salary   float32
}

想统计薪资超过 6000 的人数、年龄小于 30 岁的员工总薪资、全员休假天数......以前你得写好几个不同的循环。现在用泛型的一行解决:

bash 复制代码
// 薪资超过 6000 的人数
highPay := gCountIf(employees, func(e Employee) bool {
    return e.Salary >= 6000
})

// 年龄小于 30 的员工总薪资
youngPay := gSum(employees, func(e Employee) float32 {
    if e.Age < 30 { return e.Salary }
    return 0
})

干净、清晰、编译期类型安全。

这里还有一个细节------gSum 函数用了接口约束 Sumable,它限定了只能用于数值类型(int 系列和 float 系列),这样做的好处是不会出现把字符串拿来求和的荒谬操作。

小结一下

Go 的泛型不是最强大的(你说它比 C++ 模板弱我认),但它足够实用。没有奇技淫巧,没有模板元编程,就是解决"写重复代码"这个实际问题。

我个人觉得,每个 Go 开发者都应该尽快上手泛型。原因很简单------你的代码会变得更短、更安全、更容易维护。尤其在做工具库、写通用数据结构和算法的时候,泛型就是那个你用了就回不去的东西。

你现在项目里用上 Go 泛型了吗?评论区聊聊你的实战感受👇

相关推荐
Mandy的名字被占用了8 小时前
晨风AI+知识付费系统|学练考全闭环,重构教育变现新模式
人工智能·后端
why技术9 小时前
分享一套我一直在使用的 AICoding 组合拳,小而美的典范。
前端·后端·ai编程
GetcharZp9 小时前
SRS实战:一个开源流媒体服务,支持6大协议,80ms低延迟,轻松搭建自己的直播平台
后端
Csvn10 小时前
📊 SQL 入门 Day 7:子查询 — 查询中的查询
后端·sql
IT_陈寒13 小时前
为什么我的JavaScript异步代码总是不按顺序执行?
前端·人工智能·后端
星栈13 小时前
Node 框架怎么选?Express、Koa、Egg、NestJS 场景化选型指南
后端·node.js
锋行天下13 小时前
打造企业内部知识库系统RAG全栈项目
前端·后端·架构
码事漫谈14 小时前
Kimi K3 真实体验:全网评价整理,优缺点一次性说清楚
后端
Fanta丶14 小时前
16.Activiti8 SpringBoot3.X 部署与测试
后端
用户2080468045614 小时前
Python3 数据类型转换新手实战指南
后端