详解Go中的append函数

2023年 9月 26日 49.4k 0

1. 前言

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

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

1.1样例

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样例的答案

image.png

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

2. append函数详解

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

2.1 切片的由来

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

2.2 切片的底层

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

  • 指向底层数组的指针(ptr):指向切片所引用的底层数组的指针。
  • 切片的长度(len):表示切片当前包含的元素个数。
  • 切片的容量(cap):表示切片从第一个元素开始到底层数组末尾的元素个数。
  • 切片的结构体定义如下:

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

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

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

    2.3切片的创建

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

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

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

    其实用make函数来创建的实际流程是,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("数组的类型:%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()函数

    arr := [7]int{}
    fmt.Printf("长度:%vn",len(arr))
    fmt.Printf("容积:%vn",cap(arr))
    fmt.Printf("具体内容:%vn",arr)
    

    运行结果如下:

    image.png

    2.4.2 append函数

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

    它的基本语法如下:

    append(slice []T, elements ...T) []T
    

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

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

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

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

    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函数会创建一个更大的数组,然后把这个原数组的内容拷贝到新数组里面去,其实我们大概认为是扩容后的容积是原容积的两倍就行.

    具体的扩容策略如下:

  • 如果原始切片的长度小于1024,则新的底层数组的大小会扩大为原始切片长度的两倍。
  • 如果原始切片的长度大于等于1024,则新的底层数组的大小会扩大为原始切片长度的1.25倍。
  • 这个扩容策略是为了平衡内存分配和性能,避免频繁地进行内存分配和拷贝操作。

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

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

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

    输出结果如下:

    原始切片: [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]

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论