详解Go中的append函数

1. 前言

此文章是个人学习归纳的心得,如有不对,还望指正,感谢!

如何判断是否有阅读本文章的必要,你可以观看下面的样例,并且分析最终打印的结果,如果答案正确,那就没有阅读本文的必要,答案在样例后面

1.1样例

go 复制代码
package main

func one(s []int) {
   s = append(s, 0)
   for i := range s {
      s[i]++
   }
}

func tow() {
   s1 := []int{1, 2}
   s2 := s1
   s2 = append(s2, 3)
   one1(s1)
   one1(s2)
   fmt.Printf("%v,%v", s1, s2)

}

func main(){
  tow()
}

1.2样例的答案

如果和你预期的答案不一样,那么请接着往下看

2. append函数详解

如果要提append函数的话,我们不可避免的谈到切片,因此,我们就先来聊一下切片

2.1 切片的由来

go语言是一种强类型的语言,这种强不止体现在只能相同类型的元素进行运算,还体现在数组的身上,长度也是数组的类型的判断标准之一,这样可以规避很多风险,但也带来了不方便--数组的长度不可扩展,这对于我们操作数据来说,很不方便,因此就有了切片这一类型,其实切片可以类比其他语言中的数组,而go语言中的数组与其他类型的语言有很大的差距

2.2 切片的底层

在Go语言中,切片的底层是一个结构体,该结构体包含三个字段:

  1. 指向底层数组的指针(ptr):指向切片所引用的底层数组的指针。
  2. 切片的长度(len):表示切片当前包含的元素个数。
  3. 切片的容量(cap):表示切片从第一个元素开始到底层数组末尾的元素个数。

切片的结构体定义如下:

go 复制代码
type slice struct {
    ptr *elementType   // 指向底层数组的指针
    len int            // 切片的长度
    cap int            // 切片的容量
}

其中,elementType是底层数组中元素的类型。

切片的底层数组可以是一个固定大小的数组,也可以是一个动态分配的数组。当切片的容量不足以容纳更多元素时,Go语言会自动分配一个更大的底层数组,并将切片的指针指向新的底层数组。这种自动扩容的机制使得切片在使用时非常灵活和方便。

2.3切片的创建

我们可以从切片的创建来看:

  • 1.先创建数组,然后通过截取,来得到该数组的切片
  • 2.使用make函数来创建切片

第二种方法其实就是把第一种方法进行了封装

其实用make函数来创建的实际流程是,go编译器会先创建一个数组,然后再创建这个切片,并不是直接创建了切片,底层还是数组

go 复制代码
package main

import "fmt"
//切片的创建
func main() {

   // 方法一
   // 1.先声明数组
   arr := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
   // 2.声明该数组的切片
   arrslice1 := arr[:]                    //直接把这个数组全部当做切片
   arrslice2 := arr[0:]                   //第二个值不写的话,默认到最后
   arrslice3 := arr[:8]                   // 第一个值不写的话,默认从0开始
   arrslice4 := arr[2:3]                  // 切片是[2,3)的区间,所以就取下标为2的值
   arrslice5 := arr[0:8]                  //可以简写成切片1的
   fmt.Printf("数组的类型:%T\n", arr)          //数组的类型:[8]int
   fmt.Printf("数组切片1的类型:%T\n", arrslice1) //数组切片1的类型:[]int
   fmt.Printf("数组切片1的值:%v\n", arrslice1)  //数组切片1的值:[1 2 3 4 5 6 7 8]
   fmt.Printf("数组切片2的值:%v\n", arrslice2)  //数组切片2的值:[1 2 3 4 5 6 7 8]
   fmt.Printf("数组切片3的值:%v\n", arrslice3)  //数组切片3的值:[1 2 3 4 5 6 7 8]
   fmt.Printf("数组切片4的值:%v\n", arrslice4)  //数组切片4的值:[3]
   fmt.Printf("数组切片5的值:%v\n", arrslice5)  //数组切片5的值:[1 2 3 4 5 6 7 8]


   //方法二
   //切片其实也是一种数据类型,可以像一般类型那样进行声明创建
   arrslice6 := []string{"yzc", "tjh", "tzr", "lcc"}
   //arrslice7 := []string{}
   fmt.Printf("字符串切片类型:%T\n", arrslice6)
   fmt.Printf("字符串切片的值:%v", arrslice6)
}

2.4 切片中元素的增加-append函数

上面的内容,其实我是想说,切片的底层还是数组,切片中元素的增加是与底层数组有关,下面先介绍一下go语言内置的两个用来测量的函数 len(),cap()

2.4.1 len()函数和cap()函数

go 复制代码
arr := [7]int{}
fmt.Printf("长度:%v\n",len(arr))
fmt.Printf("容积:%v\n",cap(arr))
fmt.Printf("具体内容:%v\n",arr)

运行结果如下:

2.4.2 append函数

append()是Go语言内置的函数,用于向切片中追加元素。

它的基本语法如下:

go 复制代码
append(slice []T, elements ...T) []T

其中,slice表示要追加元素的切片,elements表示要追加的元素。

append()函数会将元素追加到切片的末尾,并返回一个新的切片。如果原始切片的容量足够大,那么append()函数会直接将元素追加到原始切片的末尾。如果原始切片的容量不够大,append()函数会创建一个新的切片,并将原始切片的元素和新元素都复制到新的切片中。

需要注意的是,append()函数返回的是一个新的切片,原始切片并没有被修改。如果想要修改原始切片,可以使用切片赋值的方式。

下面是一些append()函数的示例:

go 复制代码
slice := []int{1, 2, 3}
slice = append(slice, 4, 5)  // 追加元素4和5到切片末尾
fmt.Println(slice)  // 输出:[1 2 3 4 5]

slice2 := []int{6, 7, 8}
slice = append(slice, slice2...)  // 将切片slice2追加到切片slice末尾
fmt.Println(slice)  // 输出:[1 2 3 4 5 6 7 8]

需要注意的是,append()函数可以一次追加多个元素,并且可以追加其他切片的元素,只需要在切片名后加上...表示将切片打散作为参数传递。

2.4.3 注意

其中还有一个值得关注的事情,就是当底层数组容积不够的时候,append函数会创建一个更大的数组,然后把这个原数组的内容拷贝到新数组里面去,其实我们大概认为是扩容后的容积是原容积的两倍就行.

具体的扩容策略如下:

  1. 如果原始切片的长度小于1024,则新的底层数组的大小会扩大为原始切片长度的两倍。
  2. 如果原始切片的长度大于等于1024,则新的底层数组的大小会扩大为原始切片长度的1.25倍。

这个扩容策略是为了平衡内存分配和性能,避免频繁地进行内存分配和拷贝操作。

需要注意的是,虽然append()函数会创建一个新的更大的底层数组,但是返回的仍然是一个切片。这个切片会指向新的底层数组,原始切片并没有被修改。

下面是一个示例,演示了切片的扩容过程:

go 复制代码
slice := []int{1, 2, 3}
fmt.Println("原始切片:", slice)
fmt.Println("原始切片长度:", len(slice))
fmt.Println("原始切片容量:", cap(slice))

slice = append(slice, 4, 5, 6, 7, 8, 9, 10)
fmt.Println("追加元素后的切片:", slice)
fmt.Println("追加元素后的切片长度:", len(slice))
fmt.Println("追加元素后的切片容量:", cap(slice))

输出结果如下:

css 复制代码
原始切片: [1 2 3]
原始切片长度: 3
原始切片容量: 3
追加元素后的切片: [1 2 3 4 5 6 7 8 9 10]
追加元素后的切片长度: 10
追加元素后的切片容量: 12

可以看到,初始切片的容量是3,当追加了7个元素后,切片的容量已经扩大到12。

3.逐步分析样例

样例代码 复制代码
package main

func one(s []int) {
   s = append(s, 0)
   for i := range s {
      s[i]++
   }
}

func tow() {
   s1 := []int{1, 2}
   s2 := s1
   s2 = append(s2, 3)
   one(s1)
   one(s2)
   fmt.Printf("%v,%v", s1, s2)

}

func main(){
  tow()
}
  • 首先会执行tow()函数,在tow函数里面,会先创建一个容积和长度都为2的匿名数组,然后在此基础上创建切片,将切片赋值s1变量进行存储

  • 然后把切片s1的值传递给s2,此时s1,s2指向同一个底层的匿名数组

  • 然后用append函数给s2追加一个数字3,append函数会发现这个切片的底层数组的容积和长度相等,也就是底层数组满了,然后就会创建一个原数组容积乘以2的新数组,所以现在有一个新的数组容积为4,然后append函数会把原数组里面的内容拷贝到新数组中去,然后返回一个以这个新数组为底层数组的切片,赋值给s2

  • 此时s2的容积为4,长度为3,内部元素为 [1,2,3],而此时s1切片的容积为2,长度为2,内部元素为[1,2] ,此时两个切片的底层数组不是同一个

  • 然后执行one函数,将s1作为参数传入,在one函数里面,首先为s1追加一个元素,此时发现底层数组已满,于是创建新数组,将原来的数组复制过去,再加个0,赋值给s1这个函数内部变量,但你要发现,原来的底层数组可是没有一点变化, 而函数外面的s1的底层数组可是仍然是没有变化的那个,所以后面打印的仍然是[1,2]

  • 然后就是下一个one函数的执行,传入s2,首先为s2追加一个元素,append函数返现此时的底层数组未满(容积4,长度3),然后就正常把0加到了切片的末尾,此时底层数组容积为4,长度为4,内容为[1,2,3,0],然后执行for循环操作,底层数组的值因此就变成了[2,3,4,1],注意! 原有切片的值不会发生改变!,切片的底层是一个结构体,其中有一个变量是用于存储切片长度的,还有一个指针用来指向数据,two调用one时发生了拷贝,这两个切片不是一个切片,但是指向的数据是同一片数据,虽然指向的数据变成了[2,3,4,1],但是在原来的切片s2中记录的长度仍然是3,容积仍然是4,通俗的讲,就是你的修改,它没有发现,所以没有呈现

所以s2最终的结果是长度3,容积4,内容:[2,3,4],底层数组是[2,3,4,1]

所以最终的打印结果是[1,2],[2,3,4]

相关推荐
mtngt111 天前
AI DDD重构实践
go
Minilinux20182 天前
Google ProtoBuf 简介
开发语言·google·protobuf·protobuf介绍
Grassto2 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo7 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go
码界奇点8 天前
基于Gin与GORM的若依后台管理系统设计与实现
论文阅读·go·毕业设计·gin·源代码管理
迷迭香与樱花8 天前
Gin 框架
go·gin