Go1.18新特性--泛型
1. 介绍
泛型可能是1.18版本最大的更新了,毕竟官方文档都写在了第一条
泛型的基本介绍就不写了,c#中有最优雅的泛型实现,可以去简单看看
全面的泛型概述可见泛型提案 https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md
更多细节可见官方文档 https://go.dev/ref/spec
下面只搬运一下对泛型的简单介绍
- 函数和类型声明的语法接受类型参数
- 可以通过方括号中的类型参数列表来实例化参数化函数和类型
- 接口类型的语法现在允许嵌入任意类型以及Union和〜T类型元素。这些接口只能用作类型约束。接口现在可以定义一组类型和一组方法
- 新的预声明标识符any是空接口的别名,可以使用any代替interface{}
- 新的预声明标识符comparable表示可以使用==或!=做比较的所有类型的一个接口,它可以被用作类型约束
有三个实验性的package在使用泛型
2. any关键字
any其实就是interface{}的别名
type any = interface {}
以下代码虽然不是泛型,但用 Go 1.18 可以正常运行,证明 any 和 interface{} 是一样的:
// 这里的 any 并非泛型的约束,而是类型
func test(x any) any {
return x
}
func main() {
fmt.Println(test( "a" ))
}
泛型中,any 换为 interface{} 也可以:
// 注意其中的 T interface{},正常应该使用 T any
func Print[T interface {}](s ...T) {
for _, v := range s {
fmt.Print(v)
}
}
func main() {
Print( "Hello, " , "playground\n" )
}
可见,之所以引入 any 关键字,主要是让泛型修饰时短一点,少一些括号。any 比 interface{} 会更清爽
3. 泛型包slices
目前,slices 包有 14 个函数,可以分成几组:
- slice比较
- 元素查找
- 修改slice
- 克隆slice
其中,修改slice分为插入元素、删除元素、连续元素去重、slice扩容和缩容
3.1 slice比较
比较两个 slice 中的元素,细分为是否相等和普通比较:
func Equal[E comparable](s1, s2 []E) bool
func EqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func (E1, E2) bool) bool
func Compare[E constraints.Ordered](s1, s2 []E) int
func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func (E1, E2) int) int
其中 comparable 约束是语言实现的(因为很常用),表示可比较约束(相等与否的比较)。主要,其中的 E、E1、E2 等,只是泛型类型表示,你定义时,可以用你喜欢的,比如 T、T1、T2 等。
看一个具体的实现:
func Equal[E comparable](s1, s2 []E) bool {
if len(s1) != len(s2) {
return false
}
for i, v1 := range s1 {
v2 := s2[i]
if v1 != v2 {
return false
}
}
return true
}
3.2 元素查找
在 slice 中查找某个元素,分为普通的所有查找和包含判断:
func Index[E comparable](s []E, v E) int
func IndexFunc[E any](s []E, f func (E) bool) int
func Contains[E comparable](s []E, v E) bool
其中,IndexFunc 的类型参数没有使用任何约束(即用的 any),说明查找是通过 f 参数进行的,它的实现如下:
func IndexFunc[E any](s []E, f func (E) bool) int {
for i, v := range s {
if f(v) {
return i
}
}
return -1
}
参数 f 是一个函数,它接收一个参数,类型是 E,是一个泛型,和 IndexFunc 的第一个参数类型 []E 的元素类型保持一致即可,因此可以直接将遍历 s 的元素传递给 f
3.3 修改slice
一般不建议做相关操作,因为性能较差。如果有较多这样的需求,可能需要考虑更换数据结构
// 往 slice 的位置 i 处插入元素(可以多个)
func Insert[S ~[]E, E any](s S, i int, v ...E) S
// 删除 slice 中 i 到 j 的元素,即删除 s[i:j] 元素
func Delete[S ~[]E, E any](s S, i, j int) S
// 将连续相等的元素替换为一个,类似于 Unix 的 uniq 命令。Compact 修改切片的内容,它不会创建新切片
func Compact[S ~[]E, E comparable](s S)
func CompactFunc[S ~[]E, E any](s S, eq func (E, E) bool) S
// 增加 slice 的容量,至少增加 n 个
func Grow[S ~[]E, E any](s S, n int) S
// 移除没有使用的容量,相当于缩容
func Clip[S ~[]E, E any](s S) S
以上类型约束都包含了两个:
- S ~[]E:表明这是一个泛型版 slice,这是对 slice 的约束。注意 [] 前面的
~
,表明支持自定义 slice 类型,如 type myslice []int - E any 或 E comparable:对上面 slice 元素类型的约束。
3.4 克隆slice
获得 slice 的副本,会进行元素拷贝,注意,slice 中元素的拷贝是浅拷贝,非值类型不会深拷贝。
func Clone[S ~[]E, E any](s S) S {
// Preserve nil in case it matters.
if s == nil {
return nil
}
return append(S([]E{}), s...)
}
3.5 总结
因为泛型的存在,相同的功能对于不同类型的slice可以少写一份代码,如果想使用slice泛型的相关操作,建议复制golang.org/x/exp中的函数进行使用或修改
4. 泛型包maps
目前maps包只有8个函数,实现的功能也比较基础,大概包含了以下几种操作类型:
- 清空map
- 拷贝、克隆
- 相等判断
- kv操作
4.1 清空map
清空map就一个函数,实现起来也非常简单
func Clear[M ~ map [K]V, K comparable, V any](m M)
4.2 拷贝克隆
其中包含了拷贝和克隆,作用稍有不同
func Clone[M ~ map [K]V, K comparable, V any](m M) M
func Copy[M ~ map [K]V, K comparable, V any](dst, src M)
很容易理解克隆和拷贝的区别,克隆就是返回M的一份浅拷贝,两份副本指向同一个map,已经预感到类似于slice的坑了...,拷贝就是将src map中所有的kv复制到dst map中,如果dst已经存在key,将会被覆盖。
4.3 相等判断
主要是判断两个两个map是否相等,从函数签名可以看出两个函数的区别
func Equal[M1, M2 ~ map [K]V, K, V comparable](m1 M1, m2 M2) bool
func EqualFunc[M1 ~ map [K]V1, M2 ~ map [K]V2, K comparable, V1, V2 any](m1 M1, m2 M2, eq func (V1, V2) bool) bool
Equal函数限定两个map的key和value必须是可比较的,也就是说kv必须都可以使用==和!=做判断,而EqualFunc限定两个map的key必须是可比较的,而value的比较按照eq函数定义的比较规则
4.4 kv操作
kv操作包括了删除指定kv、返回所有的key、返回所有的value
func DeleteFunc[M ~ map [K]V, K comparable, V any](m M, del func (K, V) bool)
func Keys[M ~ map [K]V, K comparable, V any](m M) []K
func Values[M ~ map [K]V, K comparable, V any](m M) []V
值得一提的是,keys和values函数返回的元素都是无序的,这三个方法让我想到了kv数据库...
4.5 总结
maps提供的函数比slices更简单一些,关于kv操作个人觉得会有一些应用场景,map的相等判断在泛型里可能不是很有必要了...
5. 泛型的简单使用案例
来看一个CRUD接口的定义
type Model interface {
ID() string
}
type DataProvider[MODEL Model] interface {
FindByID(id string) (MODEL, error)
List() ([]MODEL, error)
Update(id string, model MODEL) error
Insert(model MODEL) error
Delete(id string) error
}
现在我们可以定义一个使用DataProvider的HTTP处理程序:
type HTTPHandler[MODEL Model] struct {
dataProvider DataProvider[MODEL]
}
func (h HTTPHandler[MODEL]) FindByID(rw http.ResponseWriter, req *http.Request) {
// validate request here
id = // extract id here
model, err := h.dataProvider.FindByID(id)
if err != nil {
// error handling here
return
}
err = json.NewEncoder(rw).Encode(model)
if err != nil {
// error handling here
return
}
}
我们可以为每个方法实现一次,然后就完成了。我们甚至可以在事务的另一端创建一个客户端,只需要为基本方法实现一次。
为什么在此使用泛型而不是简单的我们已经定义的Model接口》
与在此使用Model类型本身相比,泛型有一些优点:
- 使用泛型方法,DataProvider根本不需要知道Model,也不需要实现它。它可以简单地提供非常强大的具体类型
- 我们可以扩展这个解决方法并使用具体类型进行操作。让我们看看插入或者更新的验证器是什么样子
type HTTPHandler[MODEL any] struct {
dataProvider DataProvider[MODEL]
InsertValidator func (new MODEL) error
UpdateValidator func (old MODEL, new MODEL) error
}
在这个验证器中是泛型方法的真正优势所在。我们将解析 HTTP 请求,如果定义了自定义的 InsertValidator,那么我们可以使用它来验证模型是否检出,我们可以以类型安全的方式进行并使用具体模型:
type User struct {
FirstName string
LastName string
}
func InsertValidator(u User) error {
if u.FirstName == "" { ... }
if u.LastName == "" { ... }
}
所以我们有一个泛型的处理器,我们可以用自定义回调来调整它,它直接为我们获取有效负载。没有类型转换。没有 map。只有结构体本身。
参考:
https://go.dev/doc/go1.18
https://pkg.go.dev/golang.org/x/exp
https://mp.weixin.qq.com/s/1Tm_E86cgTrhzZ2Rnm7UjA
https://mp.weixin.qq.com/s/tjHOd6jvGj7tpmf1K4wlYg
https://mp.weixin.qq.com/s/wg5fNsB--5nIgJ6EBBc0PA