Golang 删除切片指定元素

Posted 恋喵大鲤鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang 删除切片指定元素相关的知识,希望对你有一定的参考价值。

1.删除指定类型切片

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

// DeleteSliceElms 删除切片指定元素(不许改原切片)
func DeleteSliceElms(sl []int, elms ...int) []int 
	if len(sl) == 0 || len(elms) == 0 
		return sl
	
	// 先将元素转为 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 := []int1, 2, 3, 3, 2, 5
res := DeleteSliceElms(sl, 2, 3) // [1,5]

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

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

// DeleteStrSliceElms 删除切片指定元素(不许改原切片)
func DeleteStrSliceElms(sl []string, elms ...string) []string 
	if len(sl) == 0 || len(elms) == 0 
		return sl
	
	// 先将元素转为 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 := []int1, 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 := []int1, 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

以上是关于Golang 删除切片指定元素的主要内容,如果未能解决你的问题,请参考以下文章

Golang 切片删除指定元素的几种方法

Golang 切片删除指定元素的几种方法

golang 中 怎么实现slice 删除指定的元素

Golang basic_leaming2 语言容器

Golang basic_leaming2 语言容器

go的数组和切片初始化