> 文章列表 > Golang 1.18 泛型:零值判断

Golang 1.18 泛型:零值判断

Golang 1.18 泛型:零值判断

文章目录

  • 1.背景
  • 2.可能的实现
  • 3.通用的实现
  • 4.go-huge-util
  • 参考文献

1.背景

如果我想实现一个函数,其功能是清除一个切片中所有零值元素,该如何实现呢?

从 Golang 1.18 开始支持泛型,我们可以考虑使用泛型来实现支持任意类型的切片,那么需要判断泛型切片的元素是否为零值。

下面是我实现的一个清除切片零值元素的函数。

// ClearZero creates a slice with all zero values removed.
func ClearZero[S ~[]E, E any](s S) S {r := make([]E, 0, len(s))for i := range s {if !IsZero(s[i]) {r = append(r, s[i])}}return r
}

这里的问题是如何判断泛型切片元素是否为零值,也就是实现上面代码中的函数 IsZero()

2.可能的实现

Go 原生支持类型零值,我们使用var v T申明一个变量 v,那么变量 v 便是类型 T 的零值。所以你可能会这么实现 IsZero()

func IsZero[T any](v T) bool {var zero Treturn v == zero // 此处有语法错误:invalid operation: cannot compare v == zero (incomparable types in type set)
}

从语法错误提示可以看出,我们没有对类型参数做可比较的限制,即没有将类型参数 T 限制为comparable。所以改为下面这样就可以了。

func IsZero[T comparable](v T) bool {var zero Treturn v == zero
}

对应的,ClearZero 的元素类型 E 也要限定为comparable

// ClearZero creates a slice with all zero values removed.
func ClearZero[S ~[]E, E comparable](s S) S {r := make([]E, 0, len(s))for i := range s {if !IsZero(s[i]) {r = append(r, s[i])}}return r
}

上面的实现可以满足大部业务场景下的需要,因为日常使用的切片元素均是可比较大小的(comparable),比如 booleans, numbers, strings, pointers, channels 等。但是一旦切片元素类型不可比较时,便无法使用上面的ClearZero()。比如切片元素是个 map 时。

var ms []map[string]string
ClearZero(ms) // 此处有语法错误:map[string]string does not implement comparable

3.通用的实现

要想实现一个满足所有元素类型的 ClearZero(),那么将切片元素和类型参数的零值比较便不能满足要求,有没有其他更好的办法完成零值判断呢?

虽然 Go 支持了泛型,但是我们也不能忘记了反射。标准库包 reflect 有一个函数用于判断一个值是否是其对应类型的零值。

// IsZero reports whether v is the zero value for its type.
// It panics if the argument is invalid.
func (v Value) IsZero() bool

有了 reflect Value.IsZero 我们便可以改写我们的 IsZero()

func IsZeroRef[T any](v T) bool {return reflect.ValueOf(v).IsZero()
}// 或者
func IsZeroRef[T any](v T) bool {return reflect.ValueOf(&v).Elem().IsZero()
}

推荐使用后者,因为ValueOf接受一个interface{}参数,如果 v 恰好是一个接口,你就会丢失这个信息。也就是说,使用ValueOf(v)时,当 v 是一个 interface 时会有问题。

然后再改写一下ClearZero()

// ClearZeroRef creates a slice with all zero values removed.
func ClearZeroRef[S ~[]E, E any](s S) S {r := make([]E, 0, len(s))for i := range s {if !IsZeroRef(s[i]) {r = append(r, s[i])}}return r
}

测试如下:

package mainimport ("fmt""reflect"
)func main() {bs := []bool{true, false, true}fmt.Println(ClearZeroRef(bs))is := []int{1, 2, 0, 3}fmt.Println(ClearZeroRef(is))strs := []string{"foo", "bar", "", "baz"}fmt.Println(ClearZeroRef(strs))ms := []map[string]string{{"foo": "foo"},nil,{"bar": "bar"},}fmt.Println(ClearZeroRef(ms))
}

运行如下:

[true true]
[1 2 3]
[foo bar baz]
[map[foo:foo] map[bar:bar]]

4.go-huge-util

本文实现的两个函数对应的两个版本已放置开源仓库 dablelv/go-huge-util,欢迎大家使用。

// IsZero reports whether v is the zero value for its type.
func IsZero[T comparable](v T) bool {var zero Treturn v == zero
}// IsZeroRef reports whether v is the zero value for its type.
// IsZeroRef is implemented base on reflection. 
func IsZeroRef[T any](v T) bool {return reflect.ValueOf(v).IsZero()
}// ClearZero creates a slice with all zero values removed.
func ClearZero[S ~[]E, E comparable](s S) S {r := make([]E, 0, len(s))for i := range s {if !IsZero(s[i]) {r = append(r, s[i])}}return r
}// ClearZeroRef creates a slice with all zero values removed.
// ClearZeroRef is implemented base on reflection. 
func ClearZeroRef[S ~[]E, E any](s S) S {r := make([]E, 0, len(s))for i := range s {if !reflect.ValueOf(s[i]).IsZero() {r = append(r, s[i])}}return r
}

使用示例:

package mainimport ("fmt""github.com/dablelv/go-huge-util/cond""github.com/dablelv/go-huge-util/slice"
)type ILvlv interface {Name() string
}type Lvlv struct{}func (l Lvlv) Name() string {return "lvlv"
}func main() {fmt.Println(cond.IsZero(false)) // truefmt.Println(cond.IsZero(true))  // falsefmt.Println(cond.IsZero(0))     // truefmt.Println(cond.IsZero(1))     // falsefmt.Println(cond.IsZero(""))    // truefmt.Println(cond.IsZero("foo")) // falsefmt.Println(cond.IsZeroRef(map[string]string(nil)))          // truefmt.Println(cond.IsZeroRef(map[string]string{}))             // falsefmt.Println(cond.IsZeroRef(map[string]string{"foo": "foo"})) // falseifcSlice := []ILvlv{Lvlv{}, nil, Lvlv{}}fmt.Println(cond.IsZeroRef(ifcSlice[0])) // falsefmt.Println(cond.IsZeroRef(ifcSlice[1])) // truebs := []bool{true, false, true}fmt.Println(slice.ClearZero(bs))is := []int{1, 2, 0, 0, 3}fmt.Println(slice.ClearZero(is))strs := []string{"", "foo", "bar", "baz"}fmt.Println(slice.ClearZero(strs))ms := []map[string]string{{"foo": "foo"},nil,{"bar": "bar"},}fmt.Println(slice.ClearZeroRef(ms))
}

运行输出:

true
false
true
false
true
false
true
false
false
false
true
[true true]
[1 2 3]
[foo bar baz]
[map[foo:foo] map[bar:bar]]

注意,在删除切片零值元素时,如果切片元素是可比较的(comparable),建议使用ClearZero,因为其性能略好于ClearZeroRef

参考文献

dablelv/go-huge-util - GitHub
How to check if the value of a generic type is the zero value? - stackoverflow.com