01
介绍
Go 1.21 标准库中新增的 slices 提供了很多方便处理 slice 的函数。
Go 1.22 标准库 slices 引入一些新特性,其中包括新增函数 Concat、优化函数 Delete,DeleteFunc,Compact,CompactFunc,Replace 和 Insert。
本文我们介绍 Go 1.22 标准库 slices 的新增函数和优化旧函数的新特性,关于它们原有的一些特性,本文不再赘述。
02
新增函数 Concat
新函数 Concat 连接多个切片,返回一个连接多个切片的切片。
示例代码:
func main() { s1 := []int{1, 2, 3} s2 := []int{4, 5, 6} s3 := []int{7, 8, 9} s := slices.Concat(s1, s2, s3) fmt.Println(s) }
输出结果:
[1 2 3 4 5 6 7 8 9]
阅读上面这段代码,我们定义 3 个切片类型的变量 s1、s2、s3,使用 go 1.22 标准库 slices 新增函数 Concat 拼接为 1 个切片。
在此之前,我们想要拼接 3 个切片为一个切片,需要使用 append 内置函数。
示例代码:
func main() { s1 := []int{1, 2, 3} s2 := []int{4, 5, 6} s3 := []int{7, 8, 9} //s := slices.Concat(s1, s2, s3) var s []int s = append(s, s1...) s = append(s, s2...) s = append(s, s3...) fmt.Println(s) }
输出结果:
[1 2 3 4 5 6 7 8 9]
阅读上面这段代码,我们使用内置函数 append 拼接 3 个切片为 1 个切片,需要先定义 1 个切片类型的新变量 s,然后使用内置函数 append 每次追加 1 个切片类型的变量到变量 s,实现方式比较繁琐。
源码实现:
// Concat returns a new slice concatenating the passed in slices. func Concat[S ~[]E, E any](slices ...S) S { size := 0 for _, s := range slices { size += len(s) if size < 0 { panic("len out of range") } } newslice := Grow[S](nil, size) for _, s := range slices { newslice = append(newslice, s...) } return newslice }
阅读源码,我们可以发现,func Concat[S ~[]E, E any](slices ...S) S 函数的实现比较简单,并且是基于泛型实现,不需要每个类型都实现一个对应的函数。
需要注意的是,它在拼接多个切片之前,先计算新切片的长度,然后使用 Grow 函数创建一个新切片,作为内置函数 append 的参数,这样可以避免内存分配。
为了避免触发 Grow 函数的 panic,它先对 Grow 函数的参数 size 进行了小于 0 的判断,感兴趣的读者朋友可以阅读 Grow 函数的源码。
03
元素归零优化
Go 1.22 优化缩小切片大小的函数,将新长度和旧长度之间的元素归零,包括函数 Delete,DeleteFunc,Compact,CompactFunc,Replace。
我们通过示例代码,分别在 Go 1.22 和 Go 1.21 中运行,查看运行结果的变化。
Delete
标准库 slices 的函数 func Delete[S ~[]E, E any](s S, i, j int) S 删除切片 s 中 s[i:j] 中的元素,返回修改后的切片。
示例代码:
func main() { s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} s2 := slices.Delete(s1, 2, 5) fmt.Printf("len=%d\tcap=%d\tval=%d\n", len(s1), cap(s1), s1) fmt.Printf("len=%d\tcap=%d\tval=%d\n", len(s2), cap(s2), s2) }
输出结果:
// go 1.22 len=9 cap=9 val=[1 2 6 7 8 9 0 0 0] len=6 cap=9 val=[1 2 6 7 8 9] // go 1.21 len=9 cap=9 val=[1 2 6 7 8 9 7 8 9] len=6 cap=9 val=[1 2 6 7 8 9]
阅读上面这段代码,我们可以发现 Go 1.22 标准库 slices 中的函数 Delete 将新切片长度 6 和旧切片长度 9 之间的元素改为切片元素的类型零值。
DeleteFunc
标准库 slices 的函数 func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S 在切片 s 中删除函数类型参数 del 返回值为 true 的任意元素,返回修改后的切片。
示例代码:
func main() { s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} s2 := slices.DeleteFunc(s1, func(i int) bool { return i%2 != 0 }) fmt.Printf("len=%d\tcap=%d\tval=%d\t\n", len(s1), cap(s1), s1) fmt.Printf("len=%d\tcap=%d\tval=%d\t\n", len(s2), cap(s2), s2)
输出结果:
// go 1.22 len=9 cap=9 val=[2 4 6 8 0 0 0 0 0] len=4 cap=9 val=[2 4 6 8] // go 1.21 len=9 cap=9 val=[2 4 6 8 5 6 7 8 9] len=4 cap=9 val=[2 4 6 8]
阅读上面这段代码,我们可以发现 Go 1.22 标准库 slices 中的函数 DeleteFunc 将新切片长度 4 和旧切片长度 9 之间的元素改为切片元素的类型零值。
Compact
标准库 slices 的函数 func Compact[S ~[]E, E comparable](s S) S 将切片中连续重复的元素去重。Compact 修改切片的内容并返回修改后的切片,该切片的长度可能比原切片的长度小。
示例代码:
func main() { s1 := []int{1, 2, 2, 3, 4, 3, 5, 4} s2 := slices.Compact(s1) fmt.Printf("len=%d\tcap=%d\tval=%d\t\n", len(s1), cap(s1), s1) fmt.Printf("len=%d\tcap=%d\tval=%d\t\n", len(s2), cap(s2), s2) }
输出结果:
// go 1.22 len=8 cap=8 val=[1 2 3 4 3 5 4 0] len=7 cap=8 val=[1 2 3 4 3 5 4] // go 1.21 len=8 cap=8 val=[1 2 3 4 3 5 4 4] len=7 cap=8 val=[1 2 3 4 3 5 4]
阅读上面这段代码,我们可以发现 Go 1.22 标准库 slices 中的函数 Compact 将新切片长度 7 和旧切片长度 8 之间的元素改为切片元素的类型零值。
CompactFunc
标准库 slices 的函数 func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S 在切片 s 中将函数类型参数 eq 返回值为 true 的两个元素去重,返回修改后的切片。
示例代码:
func main() { s1 := []string{"go", "php", "php", "java", "python"} s2 := slices.CompactFunc(s1, strings.EqualFold) fmt.Printf("len=%d\tcap=%d\tval=%#v\n", len(s1), cap(s1), s1) fmt.Printf("len=%d\tcap=%d\tval=%#v\n", len(s2), cap(s2), s2) }
输出结果:
// go 1.22 len=5 cap=5 val=[]string{"go", "php", "java", "python", ""} len=4 cap=5 val=[]string{"go", "php", "java", "python"} // go 1.21 len=5 cap=5 val=[]string{"go", "php", "java", "python", "python"} len=4 cap=5 val=[]string{"go", "php", "java", "python"}
阅读上面这段代码,我们可以发现 Go 1.22 标准库 slices 中的函数 CompactFunc 将新切片长度 4 和旧切片长度 5 之间的元素改为切片元素的类型零值。
Replace
标准库 slices 函数 func Replace[S ~[]E, E any](s S, i, j int, v ...E) S 将切片 s 中 s[i:j] 中的元素修改为 v,返回修改后的切片。
示例代码:
func main() { s1 := []string{"php", "java", "go", "python"} s2 := slices.Replace(s1, 1, 3, "rust") fmt.Printf("len=%d\tcap=%d\tval=%#v\n", len(s1), cap(s1), s1) fmt.Printf("len=%d\tcap=%d\tval=%#v\n", len(s2), cap(s2), s2) }
输出结果:
// go 1.22 len=4 cap=4 val=[]string{"php", "rust", "python", ""} len=3 cap=4 val=[]string{"php", "rust", "python"} // go 1.21 len=4 cap=4 val=[]string{"php", "rust", "python", "python"} len=3 cap=4 val=[]string{"php", "rust", "python"}
阅读上面这段代码,,我们可以发现 Go 1.22 标准库 slices 中的函数 Replace 当 len(v) < (j-i) 时,将新切片长度 2 和旧切片长度 3 之间的元素改为切片元素的类型零值。
04
函数 Insert 越界优化
Go 1.22 优化 Insert 函数,当参数 i 超出切片范围,则 Insert 函数将运行时触发 panic,此前,如果没有要插入的元素,该情况运行时不会触发 panic。
我们通过示例代码,分别在 Go 1.22 和 Go 1.21 中运行,查看运行结果的变化。
Insert
标准库 slices 的函数 func Insert[S ~[]E, E any](s S, i int, v ...E) S 在切片 s 的 i 索引位置,插入元素 v,返回修改后的切片(s[i:] 处的元素被向上移动以腾出空间)。
例代码:
func main() { s1 := []string{"Go", "PHP", "Java", "Rust"} s2 := slices.Insert(s1, 5) fmt.Printf("len=%d\tcap=%d\tval=%#v\n", len(s1), cap(s1), s1) fmt.Printf("len=%d\tcap=%d\tval=%#v\n", len(s2), cap(s2), s2) }
输出结果:
// go 1.22 panic: runtime error: slice bounds out of range [5:4] // ... // go 1.21 len=4 cap=4 val=[]string{"Go", "PHP", "Java", "Rust"} len=4 cap=4 val=[]string{"Go", "PHP", "Java", "Rust"}
阅读上面这段代码,我们可以发现 Go 1.22 标准库 slices 中的函数 Insert 在 i 超出切片的范围,即使没有插入元素,运行时也会触发 panic。
需要注意的是,如果在 i 超出切片的范围时,插入新元素,Go 1.22 和 Go 1.21 运行时都会引发 panic。
05
总结
本文我们介绍 Go 1.22 关于标准库 slices 的新增函数 Concat 和一些旧函数的优化。
其中,新增函数 Concat 使连接多个切片为一个切片的实现代码更加优雅;其余优化函数对比 go 1.21 的变化,也需要我们特别关注。