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语言中,切片的底层是一个结构体,该结构体包含三个字段:
- 指向底层数组的指针(ptr):指向切片所引用的底层数组的指针。
- 切片的长度(len):表示切片当前包含的元素个数。
- 切片的容量(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函数会创建一个更大的数组,然后把这个原数组的内容拷贝到新数组里面去,其实我们大概认为是扩容后的容积是原容积的两倍就行.
具体的扩容策略如下:
- 如果原始切片的长度小于1024,则新的底层数组的大小会扩大为原始切片长度的两倍。
- 如果原始切片的长度大于等于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]