Golang 删除切片指定元素

本文介绍了如何在Go语言中删除切片元素,从最初的手动实现到利用反射实现泛型效果,再到Go1.18引入的泛型。通过示例展示了反射在减少重复代码方面的应用,但同时也指出了反射带来的性能问题。最后,推荐使用Go1.18的泛型功能来更高效地处理切片元素删除操作。
摘要由CSDN通过智能技术生成

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
}

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

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

使用示例:

sl1 := []int{1, 2, 3, 3, 2, 5}
res1 := DeleteElemsRef(sl1, 2, 3).([]int) // [1,5]
sl2 := []string{"foo", "bar", "baz", "bar"}
res2 := DeleteElemsRef(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.dablelv/cyan

以上反射版本的实现已经开源至 dablelv/cyan,可 import 直接使用,欢迎大家 star & pr。

package main

import (
	"fmt"

	"github.com/dablelv/cyan/slice"
)

func main() {
	is := []int{1, 2, 3, 3, 2, 5}
	res1 := slice.DeleteElemsRef(is, 2, 3).([]int)

	ss := []string{"foo", "bar", "baz", "bar"}
	res2 := slice.DeleteElemsRef(ss, "foo", "bar").([]string)

	fmt.Printf("res1 is %v, res2 is %v\n", res1, res2)
}

运行输出:

res1 is [1 5], res2 is [baz]

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

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


参考文献

reflect package

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值