前言
Go 1.22
版本于 2024 年 2 月 6 日发布,引入了几个重要的特性和改进。在标准库层面上,该版本对 slices
库进行了更新,更新内容包括以下三个方面:
- 新增
Concat
函数:该函数能够高效地拼接多个切片。 - 零化处理:
Delete
、DeleteFunc
、Compact
、CompactFunc
和Replace
函数在原切片中将 被移除的元素 置为零值(被移除的元素 是指从原切片中移除的指定元素,在新切片中不存在)。 - 越界插入优化:在使用
Insert
函数时,若参数i
超出切片的范围,则总会触发panic
。而在Go 1.22
版本之前,即使i
越界了,在没有指定插入元素的情况下,该行为不会触发panic
。
本文将详细介绍 Go
语言 slices
库在 Go 1.22
版本中的更新内容。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
新增函数 Concat:高效拼接切片
Concat
函数接受一个不定参数 slices
,参数类型为切片,该函数用于将多个切片拼接到一个新的切片里并返回新切片。
代码示例:
代码语言:go复制package main
import (
"fmt"
"slices"
)
func main() {
s1 := []string{"Go slices", "Go maps"}
s2 := []string{"Go strings", "Go strconv"}
s3 := []string{"程序员", "陈明勇"}
s4 := slices.Concat(s1, s2, s3)
fmt.Printf("cap: %d, len: %dn", cap(s4), len(s4))
fmt.Println(s4)
}
代码运行结果如下所示:
代码语言:bash复制cap: 6, len: 6
[Go slices Go maps Go strings Go strconv 程序员 陈明勇]
根据运行结果可知,Concat
函数将所给定的切片集的元素都拼接到一个新切片里,并且新切片的容量和长度是所给定切片集长度的总和。
我们来看看 Concat
函数的源码实现:
// 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
}
Concat
函数的源码实现非常简洁,它在拼接切片之前先计算了新切片所需的长度,然后利用 Grow
函数初始化新切片。这样做的好处是避免了后续 append
操作中因为切片扩容而导致的内存重新分配和复制问题,使得函数更加高效。
这里留一个悬念:你们知道在什么情况下, if size < 0 这个看似不会成立的分支会成立吗?^_^ 欢迎在评论区发表你的见解。
零化处理
在 Go 1.22
版本中,对 Delete
、DeleteFunc
、Compact
、CompactFunc
和 Replace
函数进行了更新。这些函数的共同点是接受一个给定的切片参数,记为 s1
,并返回一个新切片,记为 s2
。被移除的元素会在 s1
中被置为零值(被移除的元素 是指从 s1
中移除的指定元素,在s2
中不存在)。
Delete 函数
通过不同 Go
版本的代码示例来感受 Delete
函数 零化处理 的更新。
Go 1.21
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []int{1, 2, 3, 4, 5}
s2 := slices.Delete(s1, 3, 5)
fmt.Println(s1)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[1 2 3 4 5]
[1 2 3]
Go 1.22
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []int{1, 2, 3, 4, 5}
s2 := slices.Delete(s1, 3, 5)
fmt.Println(s1)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[1 2 3 0 0]
[1 2 3]
通过对比不同版本的代码运行结果可知,被移除的元素 在原切片里被置为了 零值。
DeleteFunc 函数
通过不同 Go
版本的代码示例来感受 DeleteFunc
函数 零化处理 的更新。
Go 1.21
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []int{1, 2, 3, 4, 5}
s2 := slices.DeleteFunc(s1, func(e int) bool {
return e%2 == 0
})
fmt.Println(s1)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[1 3 5 4 5]
[1 3 5]
Go 1.22
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []int{1, 2, 3, 4, 5}
s2 := slices.DeleteFunc(s1, func(e int) bool {
return e%2 == 0
})
fmt.Println(s1)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[1 3 5 0 0]
[1 3 5]
通过对比不同版本的代码运行结果可知,被移除的元素 在原切片里被置为了 零值。
Compact 函数
通过不同 Go
版本的代码示例来感受 Compact
函数 零化处理 的更新。
Go 1.21
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []int{1, 2, 2, 3, 3, 4, 5}
s2 := slices.Compact(s1)
fmt.Println(s1)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[1 2 3 4 5 4 5]
[1 2 3 4 5]
Go 1.22
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []int{1, 2, 2, 3, 3, 4, 5}
s2 := slices.Compact(s1)
fmt.Println(s1)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[1 2 3 4 5 0 0]
[1 2 3 4 5]
通过对比不同版本的代码运行结果可知,被移除的元素 在原切片里被置为了 零值。
CompactFunc 函数
通过不同 Go
版本的代码示例来感受 CompactFunc
函数 零化处理 的更新。
Go 1.21
版本的代码示例
package main
import (
"fmt"
"slices"
"strings"
)
func main() {
s1 := []string{"Gopher", "MingYong Chen", "mingyong chen"}
s2 := slices.CompactFunc(s1, func(a, b string) bool {
return strings.ToLower(a) == strings.ToLower(b)
})
fmt.Printf("%#vn", s1)
fmt.Printf("%#vn", s2)
}
代码运行结果如下所示:
代码语言:bash复制[]string{"Gopher", "MingYong Chen", "mingyong chen"}
[]string{"Gopher", "MingYong Chen"}
Go 1.22
版本的代码示例
package main
import (
"fmt"
"slices"
"strings"
)
func main() {
s1 := []string{"Gopher", "MingYong Chen", "mingyong chen"}
s2 := slices.CompactFunc(s1, func(a, b string) bool {
return strings.ToLower(a) == strings.ToLower(b)
})
fmt.Printf("%#vn", s1)
fmt.Printf("%#vn", s2)
}
代码运行结果如下所示:
代码语言:bash复制[]string{"Gopher", "MingYong Chen", ""}
[]string{"Gopher", "MingYong Chen"}
通过对比不同版本的代码运行结果可知,被移除的元素 在原切片里被置为了 零值。
Replace 函数
通过不同 Go
版本的代码示例来感受 Replace
函数 零化处理 的更新。
Go 1.21
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []int{1, 6, 7, 4, 5}
s2 := slices.Replace(s1, 1, 3, 2)
fmt.Println(s1)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[1 2 4 5 5]
[1 2 4 5]
Go 1.22
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []int{1, 6, 7, 4, 5}
s2 := slices.Replace(s1, 1, 3, 2)
fmt.Println(s1)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[1 2 4 5 0]
[1 2 4 5]
在示例代码中,主要功能是将元素 2
替换原切片中 [1, 3]
区间的元素。因为一个元素替换了两个元素,所以第二个元素会被移除。通过对比不同版本的代码运行结果可知,被移除的元素 在原切片里被置为了 零值。
越界插入优化
Go 1.22
版本对 slices
库的 Insert
函数进行了优化。在使用 Insert
函数时,若参数 i
超出切片的范围,总会触发 panic
。而在 Go 1.22
版本之前,即使 i
越界了,在没有指定插入元素的情况下,该行为不会触发 panic
。
通过不同 Go
版本的代码示例来感受该优化。
Go 1.21
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []string{"程序员", "陈明勇"}
s2 := slices.Insert(s1, 3)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制[程序员 陈明勇]
Go 1.22
版本的代码示例
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []string{"程序员", "陈明勇"}
s2 := slices.Insert(s1, 3)
fmt.Println(s2)
}
代码运行结果如下所示:
代码语言:bash复制panic: runtime error: slice bounds out of range [3:2]
goroutine 1 [running]:
slices.Insert[...]({0xc000068020?, 0xc000046740?, 0x0?}, 0x60?, {0x0?, 0xc000076058?, 0x524258?})
/usr/local/go-faketime/src/slices/slices.go:133 0x486
main.main()
/tmp/sandbox4036609674/prog.go:12 0x68
在示例代码中,调用 slices.Insert
函数时,仅传递了切片 s
和插入位置索引 i
参数,而缺少了待插入元素值 v
参数。
通过对比不同版本的代码运行结果可知,在 Go 1.21
版本下,该代码正常运行结束而不会触发 panic
,但在 Go 1.22
版本下,会触发 panic
。
小结
本文详细介绍了 Go 1.22
版本中 slices
库的更新内容,总结起来有三个方面:
- 新增了
Concat
函数。 - 对部分函数新增了零化处理的逻辑,包括
Delete
、DeleteFunc
、Compact
、CompactFunc
和Replace
函数。 - 对
Insert
函数进行了越界插入优化。
推荐阅读
- Go 1.22 for 循环的两处重要更新
- 玩转 Go Slices 切片泛型库