append是golang中的一个内建函数,它的作用是官方的介绍是
The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice. It is therefore necessary to store the result of append, often in the variable holding the slice itself.
append 内置函数将元素追加到切片的末尾。如果它有足够的容量,则重新切片容纳新元素。如果没有,将分配一个新的底层数组,并且 Append 返回更新后的新的切片。因此有必要存储追加的结果,通常会保存原切片中。
package main
import "fmt"
func add(ss []string) {
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "add", ss, &ss, len(ss), cap(ss)), ss)
ss = append(ss, "4")
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "add2", ss, &ss, len(ss), cap(ss)), ss)
}
func main() {
ss := make([]string, 0, 10)
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "make", ss, &ss, len(ss), cap(ss)), ss)
//初始化赋值
ss = []string{"1", "2", "3", "2", "3"}
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "init", ss, &ss, len(ss), cap(ss)), ss)
add(ss)
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "end", ss, &ss, len(ss), cap(ss)), ss)
}
run
make 0xc0000c0000 0xc0000b4018,len(0),cap(10), []
init 0xc000094050 0xc0000b4018,len(5),cap(5), [1 2 3 2 3]
add 0xc000094050 0xc0000b4090,len(5),cap(5), [1 2 3 2 3]
add2 0xc0000c00a0 0xc0000b4090,len(6),cap(10), [1 2 3 2 3 4]
end 0xc000094050 0xc0000b4018,len(5),cap(5), [1 2 3 2 3]
Exiting.
看上述代码
- 经过string切片经过make之后,分配到了
len(ss)=0, cap(ss)=10
的切片上,指向数组地址0xc0000c0000
- 经过初始化赋值,分配到了
len(ss)=5, cap(ss)=5
,指向数组地址0xc000094050
- 进入add函数运行之后,由于append进行了扩容,ss重新分配到了
len(ss)=6, cap(ss)=10
新的切片去,指向数组地址0xc0000c00a0
,本质上是产生一个新的底层数组 - 跳出add之后,由于main函数中的ss仍为之前的切片,指向数组地址
0xc000094050
,该地址切片内容未做修改
删除初始化赋值后,即使没有扩容替换为新的底层数组,main函数输出的内容仍然为之前的切片内容。
package main
import "fmt"
func add(ss []string) {
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "add", ss, &ss, len(ss), cap(ss)), ss)
ss = append(ss, "4")
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "add2", ss, &ss, len(ss), cap(ss)), ss)
}
func main() {
ss := make([]string, 0, 10)
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "make", ss, &ss, len(ss), cap(ss)), ss)
add(ss)
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "end", ss, &ss, len(ss), cap(ss)), ss)
}
原因是:虽然func main
和func add
虽然都是指向了同一个数组引用,但是func main
中储存ss切片引用地址的值地址为0xc00000c030
,与func add
中的储存ss切片引用地址的值地址0xc00000c078
不是同一个,即这两个切片引用指向了同一个数组地址,但是不是同一个切片。
make 0xc000106000 0xc00000c030,len(0),cap(10), []
add 0xc000106000 0xc00000c078,len(0),cap(10), []
add2 0xc000106000 0xc00000c078,len(1),cap(10), [4]
end 0xc000106000 0xc00000c030,len(0),cap(10), []
Exiting.
那如果要能够影响到函数内外的数组该怎么办呢?
func add
传递切片指针即可,即*[]string
package main
import "fmt"
func add(ss *[]string) {
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "add", ss, &ss, len(*ss), cap(*ss)), ss)
*ss = append(*ss, "4")
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "add2", ss, &ss, len(*ss), cap(*ss)), ss)
}
func main() {
sss := make([]string, 0, 10)
ss := &sss
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "make", ss, &ss, len(*ss), cap(*ss)), ss)
//ss = []string{"1", "2", "3", "2", "3"}
//fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "init", ss, &ss, len(ss), cap(ss)), ss)
add(ss)
fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "end", ss, &ss, len(*ss), cap(*ss)), ss)
}
run
make 0xc00000c030 0xc00000e028,len(0),cap(10), &[]
add 0xc00000c030 0xc00000e038,len(0),cap(10), &[]
add2 0xc00000c030 0xc00000e038,len(1),cap(10), &[4]
end 0xc00000c030 0xc00000e028,len(1),cap(10), &[4]
Exiting.
可以看到函数内外已经修改了同一个数组。那么为什么呢?为了更好分析,做如下操作,打印这些信息:数组地址,切片地址,存储切片地址的单元地址
package main
import "fmt"
func add(ss *[]string) {
fmt.Println(fmt.Sprintf("%s %p %p %p,len(%v),cap(%v),", "add", *ss, ss, &ss, len(*ss), cap(*ss)), ss)
*ss = append(*ss, "4")
fmt.Println(fmt.Sprintf("%s %p %p %p,len(%v),cap(%v),", "add2", *ss, ss, &ss, len(*ss), cap(*ss)), ss)
}
func main() {
ss := make([]string, 0, 10)
sp := &ss
fmt.Println(fmt.Sprintf("%s %p %p %p,len(%v),cap(%v),", "make", ss, &ss, &sp, len(ss), cap(ss)), ss)
//ss = []string{"1", "2", "3", "2", "3"}
//fmt.Println(fmt.Sprintf("%s %p %p,len(%v),cap(%v),", "init", ss, &ss, len(ss), cap(ss)), ss)
add(&ss)
fmt.Println(fmt.Sprintf("%s %p %p %p,len(%v),cap(%v),", "end", ss, &ss, &sp, len(ss), cap(ss)), ss)
}
make 0xc000106000 0xc00000c030 0xc00000e028,len(0),cap(10), []
add 0xc000106000 0xc00000c030 0xc00000e038,len(0),cap(10), &[]
add2 0xc000106000 0xc00000c030 0xc00000e038,len(1),cap(10), &[4]
end 0xc000106000 0xc00000c030 0xc00000e028,len(1),cap(10), [4]
Exiting.
func add
传递指针,可以看到func add
的数组影响到了func main
的数组,是因为此时函数内外的切片是同一个切片,切片地址是0xc00000c030
,指向了同一个数组地址0xc000106000
。切片地址分别存在了0xc00000e028
和 0xc00000e038
小明问了:为什么都是指向同一个数组,不同的切片打印出来的内容会有差异呢?
是因为切片slice的len特性导致,切片的cap再大,切片也只能感知到len范围内的数组内容,之外的数组内容是感知不到的。即使cap很大,也就是只能打印到长度len之内的内容。
其实,上述内容也可以,简单地可以理解为
调用append函数时,append函数会返回一个新的切片接收。如果原切片长度加上新追加的长度没有超过容量则在原有数组上追加元素,新旧切片会指向相同的数组,这时对其中一个切片的修改会同时影响到另一个切片;当原切片长度加上新追加的长度如果超过容量则会新建一个数组,新旧切片会指向不同的数组。另外还有一个值得注意的点是,切片只会感知len范围内数组内容。
通过一个例子来更好地理解
可以看到虽然ss1和ss2都指向了同一个数组地址,但是ss1的len=0
,与ss2的len=1
不一致,因为ss1与ss2不是一个切片,只能感知到len范围内的元素,ss1包含0个元素,因此ss1输出的是空,ss2输出了元素1。
切片的内部结构包含地址、大小和容量
ss3和ss4也指向了同一个数组地址,但是与ss1、ss2不同的是,ss3和ss4是有该数组共享部分的,即元素1,而元素2不属于ss3,即使元素1和2都在该数组里面。
func main() {
ss1 := make([]string, 0, 10)
ss2 := append(ss1, "1")
fmt.Println(fmt.Sprintf("%s 切片指向的数组地址(%p) 切片自身的地址(%p),len(%v),cap(%v),", "ss1", ss1, &ss1, len(ss1), cap(ss1)), ss1)
fmt.Println(fmt.Sprintf("%s 切片指向的数组地址(%p) 切片自身的地址(%p),len(%v),cap(%v),", "ss2", ss2, &ss2, len(ss2), cap(ss2)), ss2)
ss3 := make([]string, 1, 10)
ss3[0] = "1"
ss4 := append(ss3, "2")
fmt.Println(fmt.Sprintf("%s 数组地址(%p) 切片指向的数组地址(%p) 切片自身的地址(%p),len(%v),cap(%v),", "ss3", &ss3[0], ss3, &ss3, len(ss3), cap(ss3)), ss3)
fmt.Println(fmt.Sprintf("%s 数组地址(%p) 切片指向的数组地址(%p) 切片自身的地址(%p),len(%v),cap(%v),", "ss4", &ss4[0], ss4, &ss4, len(ss4), cap(ss4)), ss4)
}
ss1 切片指向的数组地址(0xc0000b2000) 切片自身的地址(0xc0000a4018),len(0),cap(10), []
ss2 切片指向的数组地址(0xc0000b2000) 切片自身的地址(0xc0000a4030),len(1),cap(10), [1]
ss3 数组地址(0xc0000b20a0) 切片指向的数组地址(0xc0000b20a0) 切片自身的地址(0xc0000a40a8),len(1),cap(10), [1]
ss4 数组地址(0xc0000b20a0) 切片指向的数组地址(0xc0000b20a0) 切片自身的地址(0xc0000a40c0),len(2),cap(10), [1 2]