在go语言中泛型及可变参数的支持和使用方式
1 泛型
- 简介
通常在go中使用interface 空接口 可以用作泛型的支持。
1.18.1 之后的版本的golang语言 已经支持泛型。
那么,泛型可以提升什么?
对任何元素类型的切片,映射,通道进行操作的函数。
对切片或map 元素 进行计算的函数,例如最大,最小,平均,模式,标准偏差.
切片或map 的转换函数,如缩放切片.
在channel 通道运行的功能,例如将两个通道组合为一个通道.
- 类型近似
类型近似,用~
(波浪号)符号
`~` `~`
通用数据结构,如集合,多map,并发散列图,图,树,链表.
对函数进行操作的函数,例如并行调用给定函数并返回一部分结果.
当公共方法的实现对于每种类型看起来都相同时。
1.1 使用反射, 避免使用泛型类型参数
当每个类型都有不同的 方法时,使用反射将 interface 中的类型转换为 []byte
json.Marshal(inter)
如果算法调用一组特定方法就足够,那么使用特定接口仍然为最好的方法
特别是当每个类型的通用方法实现不同时。
如 io.Reader 之类的通用接口将无处可用。
还应考虑使用反射对传递的数据进行拆箱,因为它可简化API的使用
例如: 将数据作为 空接口的 json.Marshal()函数对用户来说非常方便。
修改它以使用类型参数将损害这种情况,因为传递数据需要实现特定方法。
1.2 自定义泛型:变长参数支持
在某些场景下,目前依然能使用反射来实现,比如泛型。
因为现在 Go 官方尚未在1.18版本之前的语法层面提供对泛型的支持,我们只能通过空接口结合反射来实现。
空接口 interface{}
本身可以表示任何类型的泛型,不过这个泛型太泛了,我们必须结合反射在运行时对实际传入的参数做类型检查,让其变得可控,
从而确保程序的健壮性,否则很容易因为传递进来的参数类型不合法导致程序崩溃。
下面我们通过一个自定义容器类型的实现来演示如何基于空接口和反射来实现泛型:
基于 空接口和反射的 容器,实现泛型
package main
import (
"fmt"
"reflect"
)
通过传入存储元素类型 和 容量 来初始化 容器
type Container struct {
s reflect.Value
}
基于切片类型实现的容器,这里通过反射动态初始化这个底层切片
func NewContainer(t reflect.Type, size int) *Container {
return &Container{s: reflect.MakeSlice(reflect.SliceOf(t), 0, size)}
}
通过反射对 实际传递来的 元素类型进行运行时检查
如果与容器初始化设置的元素类型不同,则返回错误信息
c.s.Type() 对应的是 切片类型,c.s.Type().Elem()对应的才是切片元素类型
func (c *Container) Put(val interface{}) error {
if reflect.ValueOf(val).Type() != c.s.Type().Elem() {
c.s 切片元素类型 与 传入参数不同
return fmt.Errorf("put error:cannot put a %T into a slice of %s", c.s.Type().Elem())
}
如果类型检查通过则将其添加到容器
c.s = reflect.Append(c.s, reflect.ValueOf(val))
return nil
}
func (c *Container) Get(val interface{}) error {
还是通过反射对元素 类型进行检查,如果不通过则返回错误信息
kind 与 Type 相比范围更大,表示类别,如指针,而Type则对应具体类型,如 *int
由于 val是指针类型,所有需要通过reflect.ValueOf(val).Elem() 获取指针指向的类型
if reflect.ValueOf(val).Kind() != reflect.Ptr || reflect.ValueOf(val).Elem().Type() != c.s.Type().Elem() {
return fmt.Errorf("get error:needs *%s but got %T", c.s.Type().Elem(), val)
}
将容器第一个索引位置值赋值给 val 指针
reflect.ValueOf(val).Elem().Set(c.s.Index(0))
然后删除容器第一个索引位置值
c.s = c.s.Slice(1, c.s.Len())
return nil
}
func main() {
nums := []int{1, 2, 3, 4, 5}
初始化容器,元素类型和nums中的元素类型相同
c := NewContainer(reflect.TypeOf(nums[0]), 16)
for _, n := range nums {
if err := c.Put(n); err != nil {
panic(err)
}
从容器读取元素,将返回结果初始化为0
num := 0
if err := c.Get(&num); err != nil {
panic(err)
}
打印返回结果值
fmt.Printf("%v, (%T)\n", num, num)
}
err := c.Put("s") //put error:cannot put a *reflect.rtype into a slice of %!s(MISSING)
err2 := c.Get("s100") //get error:needs *int but got string
fmt.Println(err, err2)
}
```
具体细节都已经在代码注释中详细标注了,执行上述代码,打印结果如下:
-> chapter04 git:(main) x go run reflect/generic.go
1(int)
如果我们试图添加其他类型元素到容器:
```
if err := c.Put("s"); err != nil { panic(err)}
```
或者存储返回结果的变量类型与容器内元素类型不符:
```
if err := c.Get(num); err != nil { panic(err)}
```
都会报错:
-> generator git:(main) x go run reflect/generic.go
panic: put error: can not put staring into a slice of int
goroutine 1 [running]:
main.main()
...
exit status 2
-> generator git:(main) x go run reflect/generic.go
panic: get error: needs *int but got int
goroutine 1 [running]:
main.main()
...
exit status 2
在这里,为了提高程序的健壮性,我们引入了错误处理机制,这块内容我们即将在下个章节中详细给大家介绍。
2 小结
-
泛型的使用场景
对特殊函数进行操作的函数
使用反射时,可能更慢,它不是类型检查
通用数据结构
切片器数据结构,如链表 和 二叉树 类型参数 替换 接口 存储数据,可避免类型断言
不同的类型 需要实现一些通用方法时,有 通用方法时 可使用 类型
许多函数都有类似代码,而不同点仅仅是 类型不同,此时使用 类型参数
-
使用类型参数的场景的方式
建议 func ReadFour(r io.Reader) ([]byte, error) 不建议的方式 func ReadFour[T io.Reader](r T) ([]byte, error)
- 点赞
- 收藏
- 关注作者
评论(0)