删除切片指定元素,Go 标准库并未给出相应的函数,需要我们自己实现。以 []int 类型的切片为例,我们可能会直接写出下面的函数。
// DeleteSliceElms 删除切片指定元素(不改原切片)。
func DeleteSliceElms(sl []int, elms ...int) []int {
// 先将元素转为 set。
m := make(map[int]struct{})
for _, v := range elms {
m[v] = struct{}{}
}
// 过滤掉指定元素。
res := make([]int, 0, len(sl))
for _, v := range sl {
if _, ok := m[v]; !ok {
res = append(res, v)
}
}
return res
}
// 使用示例
sl := []int{1, 2, 3, 3, 2, 5}
res := DeleteSliceElms(sl, 2, 3) // [1,5]
完全没有问题,上面的函数完美了实现了我们想要的功能。
但是如果我们现在又需要对 []string 类型的切片删除指定的元素,你可能想到的是拷贝一下上面的函数,改下对应的类型即可。
// DeleteStrSliceElms 删除切片指定元素(不许改原切片)。
func DeleteStrSliceElms(sl []string, elms ...string) []string {
// 先将元素转为 set。
m := make(map[string]struct{})
for _, v := range elms {
m[v] = struct{}{}
}
// 过滤掉指定元素。
res := make([]string, 0, len(sl))
for _, v := range sl {
if _, ok := m[v]; !ok {
res = append(res, v)
}
}
return res
}
如此又解决了我们的问题。但是如果我们又需要对其他类型的切片进行删除,难道故技重施,再次拷贝重复的代码吗?
2.反射范化面对重复的代码,我们应该消灭它,而不是助长它。如何消灭呢,这本该是泛型要做的事情,可惜在 Go(截止 Go 1.17)不支持范型。但是 Go 为我们提供了反射,我们可以利用反射,间接地实现范型的效果:只写一个函数,支持所有类型的切片。
// DeleteSliceElmsE deletes the specified elements from the slice.
// Note that the original slice will not be modified.
func DeleteSliceElmsE(i interface{}, elms ...interface{})(interface{}, error) {
// check params
v := reflect.ValueOf(i)
if v.Kind() != reflect.Slice {
return nil, errors.New("the input isn't a slice")
}
if v.Len() == 0 || len(elms) == 0 {
return i, nil
}
if reflect.TypeOf(i).Elem() != reflect.TypeOf(elms[0]) {
return nil, errors.New("element type is ill")
}
// convert the elements to map set
m := make(map[interface{}]struct{})
for _, v := range elms {
m[v] = struct{}{}
}
// filter out specified elements
t := reflect.MakeSlice(reflect.TypeOf(i), 0, v.Len())
for i := 0; i < v.Len(); i++ {
if _, ok := m[v.Index(i).Interface()]; !ok {
t = reflect.Append(t, v.Index(i))
}
}
return t.Interface(), nil
}
如果不关心错误,可以再封装一个函数:
// DeleteSliceElms deletes the specified elements from the slice.
// Note that the original slice will not be modified.
func DeleteSliceElms(i interface{}, elms ...interface{}) interface{} {
res, _ := DeleteSliceElmsE(i, elms...)
return res
}
使用示例:
sl1 := []int{1, 2, 3, 3, 2, 5}
res1, _ := DeleteSliceElms(sl1, 2, 3).([]int) // [1,5]
sl2 := []string{"foo", "bar", "baz", "bar"}
res2, _ := DeleteSliceElms(sl2, "foo", "bar").([]string) // [baz]
通过反射我们成功消灭了多余重复代码。看似美好,果然如此吗?
3.反射缺点反射主要用于在运行时检测或修改程序行为,提高程序的灵活性。天下没有免费的午餐,反射带来灵活的同时,也带来了性能问题。
我们通过性能测试对比下通过反射和不通过反射着两种方式的性能差异。
func BenchmarkDeleteStrSliceElmsFast(b *testing.B) {
sl := []string{"foo", "bar", "baz", "bar"}
for n := 0; n < b.N; n++ {
DeleteStrSliceElmsFast(sl, "foo", "bar")
}
}
func BenchmarkDeleteStrSliceElmsReflect(b *testing.B) {
sl := []string{"foo", "bar", "baz", "bar"}
for n := 0; n < b.N; n++ {
DeleteStrSliceElmsReflect(sl, "foo", "bar")
}
}
执行性能测试命令:
go test -bench .
goos: darwin
goarch: amd64
pkg: test/slice
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkDeleteStrSliceElmsFast-12 9195922 123.5 ns/op
BenchmarkDeleteStrSliceElmsReflect-12 2258203 524.0 ns/op
PASS
ok test/slice 3.338s
可见性能差距接近 5 倍。如果是密集型 *** 作或对性能要求较高的场景,在 Go 支持范型前(听说 Go 1.18 开始支持范型),建议还是乖乖地写对应类型的切片删除函数。
4.快速使用以上反射版本的实现已经开源至 go-huge-util,欢迎使用。
package main
import (
"fmt"
huge "github.com/dablelv/go-huge-util"
)
func main() {
sl1 := []int{1, 2, 3, 3, 2, 5}
res1, _ := huge.DeleteSliceElms(sl1, 2, 3).([]int)
sl2 := []string{"foo", "bar", "baz", "bar"}
res2, _ := huge.DeleteSliceElms(sl2, "foo", "bar").([]string)
fmt.Printf("res1 is %v, res2 is %v\n", res1, res2)
}
运行输出:
res1 is [1 5], res2 is [baz]
关于开源工具库 go-huge-util,欢迎大家协同共建,添砖加瓦。
参考文献
reflect package
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)