go slice切片的详细知识(包含底层扩容)

目录

切片初始化

var sli []int 和 sli := []int{}的区别在哪里

切片扩容——append

扩容会指向新的底层数组

抛出两个例子加深关于底层数组的理解

扩展

[]*int 和 *[]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

参考:Go 知识点(15)— 切片长度和容量

Golang中,切片扩容是通过内置函数append来实现的。具体实现方法是使用slice结合golang内置方法append进行动态扩容。\[1\]当切片的容量不足以容纳新的元素时,append函数会创建一个新的底层数组,并将来的元素复制到新的数组中。然后,将新的元素添加到新的数组中,并返回一个新的切片。这样就实现了切片扩容切片底层也是在连续的内存块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。\[2\]切片是一个非常小的对象,它是对底层数组进行了抽象,并且提供了相关的操作方法。它拥有三个字段,分别是指向底层数组的指针、长度和容量。通过对切片再次切片,可以缩小一个切片的大小。\[3\]所以,通过使用append函数和切片的特性,可以实现切片的动态扩容。 #### 引用[.reference_title] - *1* *3* [golang slice扩容机制](https://blog.csdn.net/qq_52696089/article/details/126171790)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Golang Slice切片如何扩容](https://blog.csdn.net/moer0/article/details/122933748)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值