如果我想实现一个函数,其功能是清除一个切片中所有零值元素,该如何实现呢?
从 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()
。
Go 原生支持类型零值,我们使用var v T
申明一个变量 v,那么变量 v 便是类型 T 的零值。所以你可能会这么实现 IsZero()
func IsZero[T any](v T) bool {
var zero T
return v == zero // 此处有语法错误:invalid operation: cannot compare v == zero (incomparable types in type set)
}
从语法错误提示可以看出,我们没有对类型参数做可比较的限制,即没有将类型参数 T 限制为comparable
。所以改为下面这样就可以了。
func IsZero[T comparable](v T) bool {
var zero T
return 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
要想实现一个满足所有元素类型的 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 main
import (
"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]]
本文实现的两个函数对应的两个版本已放置开源仓库 dablelv/go-huge-util,欢迎大家使用。
// IsZero reports whether v is the zero value for its type.
func IsZero[T comparable](v T) bool {
var zero T
return 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 main
import (
"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)) // true
fmt.Println(cond.IsZero(true)) // false
fmt.Println(cond.IsZero(0)) // true
fmt.Println(cond.IsZero(1)) // false
fmt.Println(cond.IsZero("")) // true
fmt.Println(cond.IsZero("foo")) // false
fmt.Println(cond.IsZeroRef(map[string]string(nil))) // true
fmt.Println(cond.IsZeroRef(map[string]string{})) // false
fmt.Println(cond.IsZeroRef(map[string]string{"foo": "foo"})) // false
ifcSlice := []ILvlv{Lvlv{}, nil, Lvlv{}}
fmt.Println(cond.IsZeroRef(ifcSlice[0])) // false
fmt.Println(cond.IsZeroRef(ifcSlice[1])) // true
bs := []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