Skip to content
GitHub

03.Go List And Slice

  • 数组是确定数量元素的集合, 数组元素类型可以不一致
  • 数组有容量和长度两个属性 cap() len() 查看数组属性
  • 数组的长度和容量始终相等 length == capacity
  • 数组元素可以修改, 但是数组长度和容量声明后就不能修改
  • 数组中未赋值的元素会使用类型的默认值
strList := [3]string{"hey", "you", "world"}      // 定义长度为 3 , 元素类型为字符串的数组
strList := [...]string{"hey", "you", "world"}    // 定义元素为字符串的数组, 根据值推断长度和容量

intList := [3]int{1, 2}                          // [1 2 0] 定义容量长度均为 3, 类型为 int 的数组
intList := [...]int{1, 2}                        // [1, 2]  容量和长度均为 2 的 int 数组

intArray := [3]int{1, 4}                         // [1 4 0] 初始化数组, 未定义的值取 0
intArray[1], intArray[2] = 2, 3                  // [1 2 3] 修改数组的值

len(strList)                                     // 3 strList 长度为 3
cap(intList)                                     // 3 intList 容量为 3

any 类型数组的元素可以是任意类型

var anyList [3]any                               // [<nil> <nil> <nil>] any 类型初始值是 nil
anyList[0], anyList[1] = "he", 20000             // [he 2 <nil>]

anyList[0] = anyList[0].(string) + " llo"        // any 转实际类型操作需要显示声明
anyList[1] = anyList[1].(int) + 1

for index, value := range anyList {
    fmt.Printf("index: %v, value: %v  type: %T\n", index, value, value)
}

> index: 0, value: hello type: string
> index: 1, value: 20000 type: int
> index: 2, value: <nil> type: <nil>

数组赋值或作为函数参数时, 传递的都是数组的拷贝
函数若想修改源数组, 传参时需要传入数组地址

func three(list [3]int) {                        // 参数是数组值传递, 外部数组不变
    list[0] = 6
}

func third(list *[3]int) {                       // 参数是数组的指针类型, 修改会同步外部数组
    list[0] = 9
}

source := [...]int{0,1,2}                       // 定义 source 数组
copy := source                                  // copy 为 source 的拷贝, 互不影响
third(&copy)                                    // 传入 copy 地址, 函数内的修改会同步给外部 copy
three(source)                                   // 传入 source 的拷贝, 函数内修改不影响外部 source
fmt.Printf("source: %v  copy: %v\n", source, copy)
> source: [0 1 2]  copy: [9 1 2]
  • 切片是一组数量可变的元素集合
  • 切片是引用类型, 切片本身不存储数据, 切片赋值传递的是地址
  • 切片会自动扩容以存储所有添加的数据

var 声明切片
make 构造切片
从数组截取切片

type slice struct {                              // 切片定义 $GOROOT/go/src/runtime/slice.go
    array unsafe.Pointer                         // 指向一个数组中某个元素的指针 (每个切片都对应一个数组)
    len int                                      // 切片长度
    cap int                                      // 切片容量
}

var strSlice []string                            // nil 切片声明, 创建一个 nil 切片, 与 nil 相等
intSlice := []int{}                              // [] 切片声明并初始化, 创建空切片, 与 nil 不相等
boolSlice := make([]bool, 1, 2)                  // [false] 初始化布尔切片, 长度为 1, 容量为 2

fmt.Println(strSlice == nil)                     // true nil 切片与 nil 一致
fmt.Println(intSlice == nil)                     // false 空切片与 nil 不一致

切片也可以从数组中截取一段
切片的长度为截取数据的数量, 切片容量为切片开端到数组结尾数量

list := [...]int{0,1,2,3,4}
slice := list[1:3]                               // [1 2] 截取数组创建切片
Printf("length: %d capacity: %d \n", len(slice), cap(slice))
> length: 2 capacity: 4                          // 切片长度为截取元素数量, 容量切片开头元素到数组结尾

声明的切片与从数组截取的切片完全一致, 声明的切片对应一个不可见的数组
切片扩容超过切片容量时, 切片会指向一个更大容量的数组, 并把旧数组的数据复制到新数组

切片是引用类型, 切片不存储数据, 赋值或参数时传递的是地址
切片扩容超过容量后会生成新的切片
切片扩容未容量不会生成新的切片

intSlice := []int{0,0,0,0}                       // 整数切片初始化, 长度容量均为 4
intArray := intSlice                             // 切片赋值, 传递的是地址, 两切片指向同一个数组
intArray[0] = 1                                  // intArray 序号 0 重新赋值, intSlice 跟着变化
intArray = append(intArray, 2)                   // append 添加元素, 超出容量, 扩容返回新的切片, intArray 变更指向的数组
intArray[1] = 1                                  // intArray 序号 1 重新赋值
fmt.Printf("intArray: %v intSlice: %v\n", intArray, intSlice)
> intArray: [1 1 0 0 2] intSlice: [1 0 0 0]      // 切片扩容后, 两切片指定数组不同,值互不影响

slice := make([]int, 3, 5)                       // 构造长度为 3 容量为 5 的切片
seq := slice                                     // seq slice 指向同一组数据
seq = append(seq, 1)                             // [0 0 0 1] seq 添加数据, 未超出容量, seq 和 slice 仍指向同一组数据
slice = append(slice, 2)                         // [0 0 0 2] slice 添加数据, 未超出容量, seq 和 slice 仍指向同一组数据
Printf("seq: %v slice: %v\n", seq, slice)        // seq 和 slice 一直指向同一组数据
> seq: [0 0 0 2] slice: [0 0 0 2]                // slice 变更覆盖 seq 变更

Golang 1.20 切片的扩容策略 $GOROOT/go/src/runtime/slice.go func growslice

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {                             // 所需容量大于两倍的原有容量
    newcap = cap                                 // 扩容后容量等于所需容量
} else {                                         // 所需容量小于两倍原有容量情况下
    const threshold = 256                        // 设置变更策略的容量阈值
    if old.cap < threshold {
        newcap = doublecap                       // 原有容量小于 256, 新数组容量为原有两倍
    } else {
        // Check 0 < newcap to detect overflow
        // and prevent an infinite loop.
        for 0 < newcap && newcap < cap {         // 原有容量大于 256, 旧容量自增 192 加 旧容量的四分之一
            // Transition from growing 2x for small slices
            // to growing 1.25x for large slices. This formula
            // gives a smooth-ish transition between the two.
            newcap += (newcap + 3*threshold) / 4
        }
        // Set newcap to the requested cap when
        // the newcap calculation overflowed.
        if newcap <= 0 {
            newcap = cap
        }
    }
}

Golang 扩容增速从 2 倍逐渐减少至 1.25 倍
不同类型切片在扩容时候还有特殊的偏移增长机制

slice := make([]int, 80, 80)

for i:=0; i < 2049; i ++ {
    oldCap := cap(slice)
    slice = append(slice, 1)
    newcap := cap(slice)

    if oldCap != newcap {
        Printf("int old: %d  new %d \n", oldCap, newcap)
    }
}

> int old: 80  new 160
> int old: 160  new 336
> int old: 336  new 672
> int old: 672  new 1184
> int old: 1184  new 1696
> int old: 1696  new 2384

> int8 old: 80  new 160
> int8 old: 160  new 320
> int8 old: 320  new 640
> int8 old: 640  new 1024
> int8 old: 1024  new 1536
> int8 old: 1536  new 2304

切片使用 append 添加 删除 插入元素

 slice := []int{0,0,0,0}
 slice = append(slice, []int{1, 1}...)           // append 添加数据 [0 0 0 0 1 1]
 slice = append(slice[:1], slice[3:]...)         // append 删除数据 [0 0 1 1]
 slice = append(slice[:1], append([]int{2}, slice[1:]...)...)

 fmt.Printf("slice: %v\n", slice)
 > slice: slice: [0 2 0 1 1]                     // append 插入数据

切片索引 0 <= index < length <= capacity
截取切片片段 list[low:high]
切片索引不能为负数
截取切片时以 list[low:high:max] 设定长度和容量

list := [...]int{0,1,2,3}

list[3]                                          // 3         切片索引 从左往右 0 开始第 3 个数
list[len(list)-1]                                // 3         切片索引 最后一个数据
list[:2]                                         // [0 1]     从切片开端到索引 2 截取片段
list[1:]                                         // [1 2 3]   从索引 1 到切片结尾
list[1:3]                                        // [1 2]     切片索引扁片(左闭右开)
list[:]                                          // [0 1 2 3] 切片全部数据

list[1:2]                                        // 忽略 high [low:]    容量 3 (length - low)
list[1:2:3]                                      // 忽略 high [low:max] 容量 2 (max - low)
slice := []int{4,1,6,2}
fmt.Println(sort.Ints(slice))                    // 切片排序
> [1 2 4 6]

copy(destSlice, sourceSlice)                    // 切片复制, dest 容量会影响复制结果