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

软件维
• 阅读 671

大家好,我是煎鱼。

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

新增 Concat 函数

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

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

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

输出结果:

[煎鱼 咸鱼 摸鱼 炸鱼 水鱼 煎鱼]

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

可能的实现如下:

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)
}

输出结果:

[煎鱼 炸鱼 水鱼 摸鱼 煎鱼]

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

但是!

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

Concat 函数签名如下:

func Concat[S ~[]E, E any](slices ...S) S

使用案例如下:

import (
    "fmt"
    "slices"
)

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

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

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

// 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 相关函数

旧版本:

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

输出结果:

s1: [11 14 13 14]
s2: [11 14]

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

s1: [11 14 0 0]
s2: [11 14]

Compact 函数

旧版本:

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

输出结果:

s1: [11 12 15 12 15]
s2: [11 12 15]

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

s1: [11 12 15 0 0]
s2: [11 12 15]

Replace 函数

旧版本:

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

输出结果:

s1: [11 99 14 14]
s2: [11 99 14]

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

s1: [11 99 14 0]
s2: [11 99 14]

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

旧版本:

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

输出结果:

s1: [煎鱼 炸鱼 水鱼]
s2: [煎鱼 炸鱼 水鱼]

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

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 不是有兼容性保障吗?这么多函数的行为变更,是可以这么干的吗?

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

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

总结

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

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

推荐阅读

点赞
收藏
评论区
推荐文章
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
go语言中,数组与切片的区别?
切片是Go语言核心的数据结构,然而刚接触Go的程序员经常在切片的工作方式和行为表现上被绊倒。比如,明明说切片是引用类型但在函数内对其做的更改有时候却保留不下来,有时候却可以。究其原因是因为我们很多人用其他语言的思维来尝试猜测Go语言中切片的行为,切片这个内置类型在Go语言底层有其单独的类型定义,而不是我们通常理解的其他语言中数组的概念。文章
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql中时间比较的实现
MySql中时间比较的实现unix\_timestamp()unix\_timestamp函数可以接受一个参数,也可以不使用参数。它的返回值是一个无符号的整数。不使用参数,它返回自1970年1月1日0时0分0秒到现在所经过的秒数,如果使用参数,参数的类型为时间类型或者时间类型的字符串表示,则是从1970010100:00:0
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Linux应急响应(四):盖茨木马
0x00前言Linux盖茨木马是一类有着丰富历史,隐藏手法巧妙,网络攻击行为显著的DDoS木马,主要恶意特点是具备了后门程序,DDoS攻击的能力,并且会替换常用的系统文件进行伪装。木马得名于其在变量函数的命名中,大量使用Gates这个单词。分析和清除盖茨木马的过程,可以发现有很多值得去学习和借鉴的地方。0x01应急场景
Wesley13 Wesley13
3年前
初探 Objective
作者:Cyandev,iOS和MacOS开发者,目前就职于字节跳动0x00前言异常处理是许多高级语言都具有的特性,它可以直接中断当前函数并将控制权转交给能够处理异常的函数。不同语言在异常处理的实现上各不相同,本文主要来分析一下ObjectiveC和C这两个语言。为什么要把ObjectiveC和
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(