Go1.22 新特性:Slices 变更 Concat、Delete、Insert 等函数,对开发挺有帮助!

大家好,我是煎鱼。

在 Go1.22 这个新版本起,切片(Slices)新增和变更了一些行为。对于开发者相对更友好了一点。

新增 Concat 函数

在以前的 Go 版本中,有一个很常见的使用场景,如果我们想要拼接两个切片。必须要手写类似如下的代码:

go 复制代码
func main() {
	s1 := []string{"煎鱼", "咸鱼", "摸鱼"}
	s2 := []string{"炸鱼", "水鱼", "煎鱼"}

	s3 := append(s1, s2...)
	fmt.Println(s3)
}

输出结果:

css 复制代码
[煎鱼 咸鱼 摸鱼 炸鱼 水鱼 煎鱼]

如果在 Go 工程中常用到,大家还会在类似 util 包上补一个这种函数,便于复用。搞不好还要基于不同的数据类型都实现一遍。

可能的实现如下:

go 复制代码
func concatSlice[T any](first []T, second []T) []T {
	n := len(first)
	return append(first[:n:n], second...)
}

func main() {
	s1 := []string{"煎鱼", "炸鱼"}
	s2 := []string{"水鱼", "摸鱼", "煎鱼"}
	s3 := concatSlice(s1, s2)

	fmt.Println(s3)
}

输出结果:

css 复制代码
[煎鱼 炸鱼 水鱼 摸鱼 煎鱼]

如果要合并超过 2 个的切片,这个函数的实现就更复杂一些。

但是!

在 Go1.22 起,新增了 Concat 函数,可以用于拼接(连接)多个切片。不需要自己维护一个公共方法了。

Concat 函数签名如下:

go 复制代码
func Concat[S ~[]E, E any](slices ...S) S

使用案例如下:

go 复制代码
import (
	"fmt"
	"slices"
)

func main() {
	s1 := []string{"煎鱼"}
	s2 := []string{"炸鱼", "青鱼", "咸鱼"}
	s3 := []string{"福寿鱼", "煎鱼"}
	resp := slices.Concat(s1, s2, s3)
	fmt.Println(resp)
}

该函数是基于泛型实现的,不需要像以前一样,每个类型都在内部实现一遍。用户使用起来非常方便。但需要确保传入的切片类型都是一致的。

其内部函数实现也比较简单。如下代码:

go 复制代码
// 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
}

需要注意的是:当 size < 0 时会触发 panic。但我感觉这更多只是一个防御性的编程处理。一般情况下不会被触发。

变更 Delete 等函数行为结果

Go1.22 起,将会对于会缩小切片片段/大小的相关函数的结果行为进行调整,切片经过缩小后新长度和旧长度之间的元素将会归为零值

将会涉及如下函数:Delete、DeleteFunc、Replace、Compact、CompactFunc 等函数。

以下是一些具体的案例。分为旧版本(Go1.21)、新版本(Go1.22 及以后)。

Delete 相关函数

旧版本:

go 复制代码
func main() {
	s1 := []int{11, 12, 13, 14}
	s2 := slices.Delete(s1, 1, 3)
	fmt.Println("s1:", s1)
	fmt.Println("s2:", s2)
}

输出结果:

ini 复制代码
s1: [11 14 13 14]
s2: [11 14]

新版本程序不变,运行结果发生了改变,输出结果为:

ini 复制代码
s1: [11 14 0 0]
s2: [11 14]

Compact 函数

旧版本:

go 复制代码
func main() {
	s1 := []int{11, 12, 12, 12, 15}
	s2 := slices.Compact(s1)
	fmt.Println("s1:", s1)
	fmt.Println("s2:", s2)
}

输出结果:

ini 复制代码
s1: [11 12 15 12 15]
s2: [11 12 15]

新版本程序不变,运行结果发生了改变,输出结果为:

ini 复制代码
s1: [11 12 15 0 0]
s2: [11 12 15]

Replace 函数

旧版本:

go 复制代码
func main() {
	s1 := []int{11, 12, 13, 14}
	s2 := slices.Replace(s1, 1, 3, 99)
	fmt.Println("s1:", s1)
	fmt.Println("s2:", s2)
}

输出结果:

ini 复制代码
s1: [11 99 14 14]
s2: [11 99 14]

新版本程序不变,运行结果发生了改变,输出结果为:

ini 复制代码
s1: [11 99 14 0]
s2: [11 99 14]

变更 Insert 函数行为,可能会 panic

旧版本:

go 复制代码
func main() {
	s1 := []string{"煎鱼", "炸鱼", "水鱼"}
	s2 := slices.Insert(s1, 4)
	fmt.Println("s1:", s1)
	fmt.Println("s2:", s2)
}

输出结果:

ini 复制代码
s1: [煎鱼 炸鱼 水鱼]
s2: [煎鱼 炸鱼 水鱼]

新版本程序不变,运行结果发生了改变,输出结果为:

go 复制代码
panic: runtime error: slice bounds out of range [4:3]

goroutine 1 [running]:
slices.Insert[...]({0xc00010e0c0?, 0x10100000010?, 0x7ecd5be280a8?}, 0xc00010aee8?, {0x0?, 0x60?, 0x10052e4c0?})
	...

以上场景是使用 slices.Insert 函数下,以前没有填入具体要插入的元素,是会正常运行的。在新版本起,会直接导致 panic。

当然,如果一开始就有填入。无论是新老版本,都会导致 panic。相当于是修复了一个边界值了。

一点质疑

可能会有同学说,这不对劲啊。Go1 不是有兼容性保障吗?这么多函数的行为变更,是可以这么干的吗?

从官方文档角度来看是可以的,因为其强调了其当前文档并未承诺将过时元素归零或不归零。也就是没有承诺不变,但承诺过可能产生变更。

总结

今天我们针对切片(Slices)的各函数变更和新增进行了实际的讲解和案例分享。整体来看,还是基于泛型做的修修补补,虽然不是大功能特性。但是对于我们实际做 Go 工程开发的同学来讲,这是比较实在的。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo... 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

推荐阅读

相关推荐
研究司马懿12 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰1 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘1 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤1 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo