1.删除指定类型切片

删除切片指定元素,Go 标准库并未给出相应的函数,需要我们自己实现。以 []int 类型的切片为例,我们可能会直接写出下面的函数。

// DeleteSliceElems 删除切片指定元素(不改原切片)。
func DeleteSliceElems(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 := DeleteSliceElems(sl, 2, 3) // [1,5]

完全没有问题,上面的函数完美了实现了我们想要的功能。

但是如果我们现在又需要对 []string 类型的切片删除指定的元素,你可能想到的是拷贝一下上面的函数,改下对应的类型即可。

// DeleteStrSliceElems 删除切片指定元素(不许改原切片)。
func DeleteStrSliceElems(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 为我们提供了反射,我们可以利用反射,间接地实现范型的效果:只写一个函数,支持所有类型的切片。

// DeleteSliceElemsE deletes the specified elements from the slice.
// Note that the original slice will not be modified.
func DeleteSliceElemsE(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
}

如果不关心错误,可以再封装一个函数:

// DeleteSliceElems deletes the specified elements from the slice.
// Note that the original slice will not be modified.
func DeleteSliceElems(i interface{}, elms ...interface{}) interface{} {
	res, _ := DeleteSliceElemsE(i, elms...)
	return res
}

使用示例:

sl1 := []int{1, 2, 3, 3, 2, 5}
res1, _ := DeleteSliceElems(sl1, 2, 3).([]int) // [1,5]
sl2 := []string{"foo", "bar", "baz", "bar"}
res2, _ := DeleteSliceElems(sl2, "foo", "bar").([]string) // [baz]

通过反射我们成功消灭了多余重复代码。看似美好,果然如此吗?

3.反射缺点

反射主要用于在运行时检测或修改程序行为,提高程序的灵活性。天下没有免费的午餐,反射带来灵活的同时,也带来了性能问题。

我们通过性能测试对比下通过反射和不通过反射着两种方式的性能差异。

func BenchmarkDeleteStrSliceElemsFast(b *testing.B) {
	sl := []string{"foo", "bar", "baz", "bar"}
	for n := 0; n < b.N; n++ {
		DeleteStrSliceElemsFast(sl, "foo", "bar")
	}
}

func BenchmarkDeleteStrSliceElemsReflect(b *testing.B) {
	sl := []string{"foo", "bar", "baz", "bar"}
	for n := 0; n < b.N; n++ {
		DeleteStrSliceElemsReflect(sl, "foo", "bar")
	}
}

执行性能测试命令:

go test -bench .
goos: darwin
goarch: amd64
pkg: test/slice
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkDeleteStrSliceElemsFast-12               9195922               123.5 ns/op
BenchmarkDeleteStrSliceElemsReflect-12            2258203               524.0 ns/op
PASS
ok      test/slice      3.338s

可见性能差距接近 5 倍。如果是密集型操作或对性能要求较高的场景,在 Go 支持范型前(听说 Go 1.18 开始支持范型),建议还是乖乖地写对应类型的切片删除函数。

4.go-huge-util

以上反射版本的实现已经开源至 go-huge-util,欢迎使用。

package main

import (
	"fmt"

	"github.com/dablelv/go-huge-util/slice"
)

func main() {
	sl1 := []int{1, 2, 3, 3, 2, 5}
	res1, _ := slice.DeleteSliceElems(sl1, 2, 3).([]int)
	sl2 := []string{"foo", "bar", "baz", "bar"}
	res2, _ := slice.DeleteSliceElems(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,欢迎大家协同共建,添砖加瓦。

5.Go 1.18 泛型

自 Go 1.18 开始,Go 引入泛型,并在实验包 golang.org/x/exp/slices 提供了对任意元素类型切片的基础操作,其中包含删除。

func Delete[S ~[]E, E any](s S, i, j int) S

建议大家使用官方提供的泛型方式来删除任意类型切片的元素。

参考文献