返回

Go八股之Slice

什么是 Slice

切片是基于数组实现的,底层是数组,可以理解为对底层数组的抽象

数据结构

1
2
3
4
5
type slice struct{
	array unsafe.Pointer  // 指向底层数组的指针,占 8 字节
	len int               // 切片长度,占 8 字节
	cap int               // 切片容量,占 8 字节
}

初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    // 直接声明
    var s1 []int

    // 使用字面量
    s2 := []int{1, 2, 5}

    // 使用 make
    s3 := make([]int, 1, 4) // 指定长度为1.容量为4
    s4 := make([]int, 1) // 指定长度为 1,容量默认为1

    // 从其他切片或数组中截取
    arr := [5]int{1, 2, 3, 4, 5}
    s5 := arr[1:3]
}

基于切片和数组创建的切片会和原数组或切片共享底层空间,修改切片会影响原数组或切片

扩容

1
2
3
4
5
6
7
func main() {
	s := make([]int, 0)
	s = append(s, 1, 2)    // 追加元素
	fmt.Println(s)                // [1 2]
	s = append(s, []int{3, 4}...) //  追加切片
	fmt.Println(s)                // [1 2 3 4]
}
  • V1.18 版本之前
    • 如果所需容量大于原容量的二倍,则新容量为所需容量。
    • 如果原slice容量小于1024,则新slice容量将扩大为原来的二倍。
    • 如果原slice容量大于或等于1024,则新slice容量将扩大为原来的1.25倍。
  • V1.18 版本
    • 如果所需容量大于原容量的二倍,则新容量为所需容量。
    • 如果原slice容量小于256,则新slice容量将扩大为原来的二倍。
    • 如果原容量大于或等于256,进入一个循环,每次容量增加(旧容量 + 3 * threshold)/ 4 。

而每次扩容后,切片将会开辟一块新的空间,将原切片中的数据拷贝到新地址中,而原先的那片空间就会被抛弃(仅限于没有其他指针指向原先的老空间)

Array 和 Slice 的区别

  • 数组是固定长度,其大小在定义阶段就必须确定,函数传递中数组传递值。它的物理空间连续,性能相对较高,适合存储已知数量元素的场景。

  • 切片是动态的序列,其是一个引用类型,指向了底层数组,大小可变,函数传递中传递引用。性能较低,但是灵活性高,适合数量未知或者频繁需要增删的场景

Slice 线程安全嘛

slice 底层结构并没有使用加锁等方式,不支持并发读写,所以并不是线程安全的,使用多个 goroutine 对类型为 slice 的变量进行操作,每次输出的值大概率都不会一样,与预期值不一致; slice在并发执行中不会报错,但是数据会丢失

Slice 深拷贝和浅拷贝

深拷贝

拷贝的是数据本身,创造一个新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
    s1 := []int{1, 2, 3, 4, 5}
    s2 := make([]int, 5, 5)
    fmt.Printf("s1: %v, %p\n", s1, s1)  // s1: [1 2 3 4 5], 0xc00000e420
    copy(s2, s1)
    fmt.Printf("s2: %v, %p\n", s2, s2)  // s2: [1 2 3 4 5], 0xc00000e450
    s3 := make([]int, 0, 5)
    for _, v := range s1 {
        s3 = append(s3, v)
    }
    fmt.Printf("s3: %v, %p\n", s3, s3)  // s3: [1 2 3 4 5], 0xc00000e480
}

浅拷贝

拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化

1
2
3
4
5
6
func main() {
    s1 := []int{1, 2, 3, 4, 5}
    fmt.Printf("s1: %v, %p\n", s1, s1)  // s1: [1 2 3 4 5], 0xc00000e420
    s2 := s1
    fmt.Printf("s2: %v, %p\n", s2, s2)  // s2: [1 2 3 4 5], 0xc00000e420
}

nil slice 和 空slilce

  • nil slice 表示未初始化的切片,不分配任何内存,其值为 nil

  • 空 slice 表示已初始化但长度为零,分配内存

  • 二者并不相等,但是对这两个都可以进行 append 操作

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    func main() {
        var a []int  // nil slice
        b := []int{} // empty slice
        fmt.Println(a == nil) // true
        fmt.Println(b == nil) // false
        fmt.Println(len(a), cap(a)) // 0 0
        fmt.Println(len(b), cap(b)) // 0 0
        a = append(a, 1) // a现在是包含一个元素的切片 [1]
        fmt.Println(a, len(a), cap(a))  // [1] 1 1
        b = append(b, 1) // b现在也是包含一个元素的切片 [1]
        fmt.Println(a, len(a), cap(a))  // [1] 1 1
    }
    
Licensed under CC BY-NC-SA 4.0
载入天数...载入时分秒...
使用 Hugo 构建
主题 StackJimmy 设计