目录
var sli []int 和 sli := []int{}的区别在哪里
本篇文章是之前我在学习切片时遇到的一些问题,也在网上搜索了许多材料+实践。下面的代码不敢说一定正确,但一定是亲自实践过的。
切片初始化
切片可以通过字面量来初始化,也可以通过内置函数 make() 初始化。
- var sli []int 或者 sli := []int{}
- s := make([]int,len,cap) 通过内置函数 make() 初始化切片 s,cap为可选参数
var sli []int 和 sli := []int{}的区别在哪里
var sli []int // nil切片
s := []int{} // 空切片
nil切片:是一个切片变量,但它没有指向任何数据,也没有分配任何内存空间。nil切片的底层数组指针是nil,表示没有指向任何底层数组。
空切片:是一个有效的切片,它指向了一块已分配的内存空间,但该空间中没有任何元素。空切片的底层数组指针是指向分配的内存空间的起始位置,该内存空间的长度为0。
var sli1 []int // nil切片,
// sli1[0] = 1 // 报错,因为在声明时没有指定切片的长度,所以 sli1 是一个nil切片,但是可以用 append 来追加元素
fmt.Printf("%v %p\n", sli1, sli1) // [] 0x0 sli1指向的地址是 0x0,是空的
var sli2 = []int{} // 空切片
fmt.Printf("%v %p\n", sli2, sli2) // [] 0x1165060
var sli3 = make([]int, 0)
// sli3[0] = 1 // 报错,没有容量来存储元素
// 解决方法一:在初始化的时候分配长度 make([]int, 10) ;解决方法二:用append添加元素
fmt.Printf("%v %p\n", sli3, sli3) // [] 0x1165060
从上面的例子中我们可以得到两个问题:
问题1:sli1 明明是nil切片,指向的地址也是0x0,但是为什么打印出来是[],而不是nil?
答:在Go语言中,空切片和nil切片在打印时的表现形式是相同的,都是[]。这是语言设计的一种约定,为了在输出时提供一致的表现形式,无论切片是空的还是nil。
虽然空切片和nil切片在内存中的状态是不同的,但它们的打印形式是一样的。这种设计有助于简化代码和提高可读性,同时也减少了对切片状态的混淆和误解。所以说就算切片是nil,但是这个切片打印出来是[],而不是nil。——ChatGPT
问题2:为什么容量不足的时候可以用 append() 来添加元素,而不能直接用索引赋值呢?
答:append 函数对于nil切片的行为是特殊的:当向一个nil切片追加元素时,append 函数会创建一个新的切片,并将元素追加到新的切片中。这意味着即使原始的切片是nil,你仍然可以通过 append 函数向它添加元素,而不会导致运行时错误。(后面会明确讲解append(),此处做为一个引子)
切片扩容——append
扩容会指向新的底层数组
- 当不需要扩容时,append 函数返回的是原底层数组的原切片(内存地址不变);
- 当切片需要扩容时,append 函数返回的是新底层数组的新切片(切片内存地址发生了改变)。
package main
import "fmt"
func main() {
var sli []int
sli = append(sli, 1)
fmt.Printf("%p ", sli)
fmt.Println(len(sli), cap(sli)) // 0xc0000b0008 1 1
// 开辟了新的更大的内存,地址也变化了
sli = append(sli, 1)
fmt.Printf("%p ", sli)
fmt.Println(len(sli), cap(sli)) // 0xc0000b0020 2 2
sli = append(sli, 1)
fmt.Printf("%p ", sli)
fmt.Println(len(sli), cap(sli)) // 0xc0000b4020 3 4
sli = append(sli, 1)
fmt.Printf("%p ", sli)
fmt.Println(len(sli), cap(sli)) // 0xc0000b4020 4 4
sli = append(sli, 1)
fmt.Printf("%p ", sli)
fmt.Println(len(sli), cap(sli)) // 0xc0000b6000 5 8
}
可得出结论:每扩容一次,指向的底层数组的地址就发生了变化。
抛出两个例子加深关于底层数组的理解
例1:截取切片
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s) // len=6 cap=6 [2 3 5 7 11 13]
s = s[:0]
printSlice(s) // len=0 cap=6 []
s = s[:4]
printSlice(s) // len=4 cap=6 [2 3 5 7]
s = s[2:]
printSlice(s) // len=2 cap=4 [5 7]
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
问题:s = s[:0] 这时候s应该没有元素了,但是s = s[:4]又突然有元素了?
答:s = s[:0] 将切片 s 的长度(len)设置为 0,但它并没有改变切片的底层数组。因此,底层数组仍然包含之前的元素。当你调用 fmt.Println(s[:4]) 时,它会打印出切片 s 中索引位置 0 到 3 的元素,这些元素仍然是底层数组中的前四个元素,因此结果是 [2 3 5 7]。即使 s 的长度被设置为 0,底层数组仍然保留在内存中,只要仍然有对这个底层数组的引用,它就不会被释放。
具体如下:
1,第一个输出为[2,3,5,7,11,13],长度为6,容量为6;
2,左指针和右指针同时指向s[0],所以长度为0,容量为6;
3,左指针指向s[0],右指针指向s[4],由于切片概念是只包含左边元素不包含右边元素,所以长度为4,但左指针在s[0]处,走过0个元素,所以容量仍然为6;
4,在经历步骤3切片后的基础上,左指针指向s[2],右指针指向最右边,所以长度为2,由于左指针走过两个元素,离最右边还剩4个元素,所以容量为4。
例2:切片作为参数传递
package main
import "fmt"
func main() {
var sli []int = []int{1, 2}
sli2 := sli
fmt.Printf("%p %p\n", sli, sli2) // 0xc000126010 0xc000126010 指向同一个底层数组
sli2 = append(sli2, 3) // 1 2 3 sli2在这里发生扩容,sli2会指向新的底层数组,但是sli1还指向原先的底层数组,所以两个切片的地址发生了改变,不在指向同一个底层数组了
fmt.Printf("%p %p\n", sli, sli2) // 0xc000126010 0xc00012a020
// 在函数里面更改切片的元素会影响到原切片
// 在函数里面额外添加元素:
// 1.如果没有发生扩容,会影响到原切片,但是由于结构体限制了切片的长度,直接打印切片不会显示额外添加的元素
// 2.如果发生了扩容,不会影响到原切片(因为底层数组的指向变了)
test(sli)
// sli切片的变化为:
// append:底层为 [1 2],切片有效长度为 [1 2]
// 自增:底层为 [2 3],切片有效长度为 [2 3]
test(sli2) // 2 3 4
// sli2切片的变化为:
// append:底层为 [1 2 3 0],切片有效长度为 [1 2 3]
// 自增:底层为 [2 3 4 1],切片有效长度为 [2 3 4]
// 至于为什么两个切片在test函数里面的长度变化不同?
// 是因为:sli在test()里面发生了扩容,地址变化了;sli2在main()里面发生了扩容,进入test()里面没有发生扩容
// 所以sli不能取到 sli[:3],会报错,sli2可以取到 sli2[:4] (结构体限制了切片的长度,直接打印切片sli2不会显示额外添加的元素,要显示打印sli2[:4])
fmt.Println(sli, sli[:2]) // [1 2] [1 2]
fmt.Println(sli2, sli2[:4]) // [2 3 4] [2 3 4 1]
}
func test(sli []int) {
sli = append(sli, 0)
for i, _ := range sli {
sli[i]++
}
}
注意:混合使用切片截取和 append 非常容易犯错,要尽量避免两种用法混合。
扩展
[]*int 和 *[]int
[]*int 表示一个切片,其中每个元素都是一个指向 int 类型的指针,即每个元素都是 *int 类型。
sli := make([]*int, 0)
a := 1
sli = append(sli, &a)
// sli = append(sli, 1) // 报错
fmt.Println(sli) // [0xc00018c008]
*[]int 表示一个指向 []int 类型的指针。
var sli1 *[]int // 相当于new()一个[]int 切片
sli2 := new([]int)
fmt.Println(reflect.TypeOf(sli1), reflect.TypeOf(sli2)) // *[]int *[]int
fmt.Printf("%T %T\n", sli1, sli2) // *[]int *[]int
// 归根结底 sli1 和 sli2 属于指针类型
fmt.Println(reflect.TypeOf(sli1).Kind(), reflect.TypeOf(sli2).Kind()) // ptr ptr
fmt.Println(reflect.ValueOf(sli1).Kind(), reflect.ValueOf(sli2).Kind()) // ptr ptr