从样例中分析Go语言中的append函数给切片添加值时的执行逻辑

2023-10-12 10:26:43 浏览数 (1)

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编译器会先创建一个数组,然后再创建这个切片,并不是直接创建了切片,底层还是数组

代码语言:txt复制
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("数组的类型:%Tn", arr)          //数组的类型:[8]int
   fmt.Printf("数组切片1的类型:%Tn", arrslice1) //数组切片1的类型:[]int
   fmt.Printf("数组切片1的值:%vn", arrslice1)  //数组切片1的值:[1 2 3 4 5 6 7 8]
   fmt.Printf("数组切片2的值:%vn", arrslice2)  //数组切片2的值:[1 2 3 4 5 6 7 8]
   fmt.Printf("数组切片3的值:%vn", arrslice3)  //数组切片3的值:[1 2 3 4 5 6 7 8]
   fmt.Printf("数组切片4的值:%vn", arrslice4)  //数组切片4的值:[3]
   fmt.Printf("数组切片5的值:%vn", arrslice5)  //数组切片5的值:[1 2 3 4 5 6 7 8]


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

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

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

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

代码语言:go复制
arr := [7]int{}
fmt.Printf("长度:%vn",len(arr))
fmt.Printf("容积:%vn",cap(arr))
fmt.Printf("具体内容:%vn",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))

输出结果如下:

代码语言:txt复制
原始切片: [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 我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

0 人点赞