一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
1.18之前,我们想使用不确定的变量类型,通过interface{}空接口来保证.
func SumWithInterface(a []interface{}) (sum int) {
for _, v := range a {
//需要运行时,断言类型
switch v.(type) {
case int64:
sum += int(v.(int64))
case int:
sum += v.(int)
case string:
tmp, _ := strconv.Atoi(v.(string))
sum += tmp
default:
panic("unsupported type") //入参类型不符合预期,引发panic
}
}
return
}
func TestSumWithInterface(t *testing.T) {
SumWithInterface([]interface{}{1, 2, 3, 4, 5})
SumWithInterface([]interface{}{1, 2, 3, 4, 4.33,""}) //panic
}
复制代码
泛型
1.17中已经预埋了泛型代码,只不过没有默认开启。需要使用 -gcflags=-G=3 编译参数开启泛型,1.18中只不过是默认开启了。
做了什么?
泛型在编译阶段完成类型判定。
有什么效果?
可以编写通用的代码,对不同类型的变量进行通用的操作;意味着我们不用再运行时断言变变量类型(性能、安全)
保持GO语言灵活的基础上,对GO作为强类型、编译型语言的安全性、稳定性、和性能上进行了增强。
使用泛型重构
//[V int64 | int | string] 编译阶段确定入参类型。
func SumWithT[V int64 | int | string](a []V) V {
//代码简介 无需类型断言
var s V
for _, v := range a {
s += v
}
return s
}
func TestSumWithT(t *testing.T) {
SumWithT([]int64{1, 2, 3, 4, 65, 6, 7, 8, 9, 10})
SumWithT([]string{"1", "2", "3", "4", "65", "6", "7", "8", "9", "10"})
SumWithT([]int{1, 2, 3, 4, 65, 6, 7, 8, 9, 6.44}) //通过编译时类型推断,编译不通过
}
//类型别名怎么办? 要重复写吗? 波浪线保留字 ~int64 所有底层基于int64实现的类型,如time.Duration
func SumWithAlias[V ~int64](a []V) V {
var s V
for _, v := range a {
s += v
}
return s
}
func TestSumWithAlias(t *testing.T) {
SumWithAlias([]time.Duration{time.Second, time.Hour, time.Minute})
}
复制代码
上面写了形参类型写了太多,麻烦, 还可以定义形参接口类型
type T interface {
int64 | int | string | float
}
func SumWithTInter[V T](a []V) V {
var s V
for _, v := range a {
s += v
}
return s
}
复制代码
性能对比
基于interface
func SumWithInterface(a []interface{}) (sum int) {
for _, v := range a {
switch v.(type) {
case int64:
sum += int(v.(int64))
case int:
sum += v.(int)
case string:
tmp, _ := strconv.Atoi(v.(string))
sum += tmp
default:
panic("unsupported type")
}
}
return
}
func BenchmarkSumWithInterface(b *testing.B) {
for i := 0; i < b.N; i++ {
SumWithInterface([]interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
}
}
goos: darwin
goarch: amd64
pkg: demo/t
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSumWithInterface
BenchmarkSumWithInterface-12 47217136 23.50 ns/op
PASS
复制代码
基于泛型
func SumWithT[V int64 | int | string](a []V) V {
var s V
for _, v := range a {
s += v
}
return s
}
func BenchmarkSumWithT(b *testing.B) {
for i := 0; i < b.N; i++ {
SumWithT([]int64{1, 2, 3, 4, 65, 6, 7, 8, 9, 10})
}
}
goos: darwin
goarch: amd64
pkg: demo/t
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSumWithT
BenchmarkSumWithT-12 285750075 3.987 ns/op
PASS
复制代码
经过测试,泛型相比interface对不同类型变量的操作的性能提升大约8倍左右。
性能分析
inteface
不快:操作interface变量时,如上面例子所示,需要在运行时进行类型断言,过多的运行时断言会导致程序运行效率下降,同时会让代码显得很不简洁。
不安全:在GO里面任意类型均实现了interface{}空接口,所以当传统的形参interface{},被传入不符合预期的变量类型,会带来一定的麻烦甚至引发panic。
泛型
在编译阶段就已经确定了变量类型,解决了安全和类型断言引起的性能问题。
项目应用
切片去重 interface实现
func SliceUNIQ(data interface{}) interface{} {
tOf := reflect.TypeOf(data)
if tOf.Kind() != reflect.Slice {
return data
}
if tOf.Elem().Comparable() == false {
return data
}
vOf := reflect.ValueOf(data)
if vOf.Len() == 0 {
return data
}
if vOf.Index(0).CanInterface() == false {
return data
}
var table = make(map[interface{}]struct{})
sliceOfV := reflect.MakeSlice(tOf, 0, 0)
for i := 0; i < vOf.Len(); i++ {
tmp := vOf.Index(i).Interface()
if _, ok := table[tmp]; ok {
continue
}
table[tmp] = struct{}{}
sliceOfV = reflect.Append(sliceOfV, reflect.ValueOf(tmp))
}
return sliceOfV.Interface()
}
复制代码
泛型实现
func SliceUNIQ[V comparable](data []V) []V {
var ret []V
var table = make(map[interface{}]struct{}, 0)
for _, v := range data {
if _, ok := table[v]; ok {
continue
}
table[v] = struct{}{}
ret = append(ret, v)
}
return ret
}
func TestSlices(t *testing.T) {
t.Log(SliceUNIQ([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}))
t.Log(SliceUNIQ([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}))
}
复制代码
使用建议
GO推出泛型的目的是解决intreface类型断言的问题而生,所以我们只在需要的时候使用泛型,而不是起手式就是泛型。
GO官方对一些泛型类型的封装