golang学习--slice


针对切片的理解,通过几个例子来掌握切片相关内容

切片定义

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

数组与切片

切片的数据实际是通过数组来保存的,每个切片都有三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个栗子,底层数组 a := [8]int{0, 1, 2, 3, 4, 5, 6, 7};

  • 切片 s1 := a[:5],切片和数组对应关系:
    Go-slice-2020-05-12-15-23-13

  • 切片 s2 := a[3:6],切片和数组对应关系:
    Go-slice-2020-05-12-15-44-43

指向同一个底层数组的切片修改值

切片是指向底层数组的引用类型,指向同一个底层数组的切片底层数据存放都是在同一个位置,修改某个切片会影响到在同一个范围的切片

import (
    "fmt"
    "testing"
)

func TestSliceShareMemory(t *testing.T) {
    year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
    Q2 := year[3:6]
    t.Log(Q2, len(Q2), cap(Q2))
    summer := year[5:8]
    t.Log(summer, len(summer), cap(summer))
    summer[0] = "Unkonw"
    t.Log(Q2)
    t.Log(year)
}
// === RUN   TestSliceShareMemory
//     TestSliceShareMemory: slice_test.go:36: [Apr May Jun] 3 9
//     TestSliceShareMemory: slice_test.go:38: [Jun Jul Aug] 3 7
//     TestSliceShareMemory: slice_test.go:40: [Apr May Unkonw]
//     TestSliceShareMemory: slice_test.go:41: [Jan Feb Mar Apr May Unkonw Jul Aug Sep Oct Nov Dec]
// --- PASS: TestSliceShareMemory (0.00s)
// PASS

切片表达式

切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定 low 和 high 两个索引界限值的简单的形式,另一种是除了 low 和 high 索引界限值外还指定容量的完整的形式:

  1. 切片 len()是可访问长度,容量 cap()是总空间大小。通过数组生成的切片, len 为首尾索引之差,cap 为从切片首索引到数组末尾长度
  2. 切片 s[low:high:max],从切片 s 的 low 处到 high 处所获得的切片,len=high-low,cap=max-low
func TestSliceExpression(t *testing.T) {
    a := [5]int{1, 2, 3, 4, 5}
    // b := a[1:3:7]
    b := a[1:3:5]
    fmt.Printf("b:%v len(b):%v cap(b):%v\n", b, len(b), cap(b))
}
// === RUN   TestSliceExpression
// b:[2 3] len(b):2 cap(b):4
// --- PASS: TestSliceExpression (0.00s)
// PASS

切片不能比较

两个切片不能直接比较,会报错:

func TestSliceCompare(t *testing.T) {
    a := []int{1, 2, 3, 4}
    b := []int{1, 2, 3, 4}
    if a == b {
        t.Log("a==b")
    }
}
// invalid operation: a == b (slice can only be compared to nil)
// FAIL    go_learn/go_test/slice_test [build failed]
// FAIL

切片的 append

切片通过 append()添加元素时,未超过 newcap 时底层数组地址不变,超过的话底层数组会申请新的内存地址。新申请的容量大小计算分成了两步,有关 append 的源码在$GOROOT/src/runtime/slice.go,可以自己去分析。

计算逻辑 newcap

  1. new cap > old * 2 直接申请新容量大小;
  2. 小于 2 倍时,len<1024 翻倍,len>1024 加上 1/4
//$GOROOT/src/runtime/slice.go
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
  newcap = cap
} else {
  if old.len < 1024 {
    newcap = doublecap
  } else {
    // Check 0 < newcap to detect overflow
    // and prevent an infinite loop.
    for 0 < newcap && newcap < cap {
      newcap += newcap / 4
    }
    // Set newcap to the requested cap when
    // the newcap calculation overflowed.
    if newcap <= 0 {
      newcap = cap
    }
  }
}

//...略
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
//...略

实际申请内存大小

上面先算了个逻辑上的 newcap,实际申请内存的时候,由于内存对齐的关系不会直接就用 newcap。上面的代码就是在算好了 newcap 后会调用roundupsize()得到实际的大小。

//$GOROOT/src/runtime/msize.go
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
    if size < _MaxSmallSize {
        if size <= smallSizeMax-8 {
            return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
        } else {
            return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])
        }
    }
    if size+_PageSize < size {
        return size
    }
    return alignUp(size, _PageSize)
}

roundupsize()中的 class_to_size、size_to_class8 是存了具体大小的数组,根据传入的 newcap 来算出下标,拿到对应的大小值。这些数组在//$GOROOT/src/runtime/sizeclasses.go,这个文件又是//$GOROOT/src/runtime/mksizeclasses.go.go生成的,生成规则就先不去看了。

//$GOROOT/src/runtime/sizeclasses.go
// Code generated by mksizeclasses.go; DO NOT EDIT.
//go:generate go run mksizeclasses.go
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128 ...}

var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25 ...}

了解了上面的内容之后就可以理解下面的几个例子了:

func TestSliceAppend(t *testing.T) {
    var a = make([]int, 5, 10)
    for i := 0; i < 10; i++ {
        a = append(a, i)
        fmt.Printf("a ptr: %p\n", a)
    }
    fmt.Println(a)
}
// === RUN   TestSliceAppend
// a ptr: 0xc00000c320
// a ptr: 0xc00000c320
// a ptr: 0xc00000c320
// a ptr: 0xc00000c320
// a ptr: 0xc00000c320
// a ptr: 0xc0000100a0
// a ptr: 0xc0000100a0
// a ptr: 0xc0000100a0
// a ptr: 0xc0000100a0
// a ptr: 0xc0000100a0
// [0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
// --- PASS: TestSliceAppend (0.00s)
// PASS

func TestSliceAppend2(t *testing.T) {
    s := []int{1, 2, 3, 4}
    a := make([]int, 3, 6)
    b := append(a, 10)
    a[0] = 50
    fmt.Printf("a: %v\tptr: %p\tfirst: %v\n", a, a, a[0])
    fmt.Printf("b: %v\tptr: %p\tfirst: %v\n", b, b, b[0])

    b = append(a, s...)
    a[0] = 100
    fmt.Printf("a: %v\tptr: %p\tfirst: %v\n", a, a, a[0])
    fmt.Printf("b: %v\tptr: %p\tfirst: %v\n", b, b, b[0])
}
// === RUN   TestSliceAppend2
// a: [50 0 0]    ptr: 0xc00000a330    first: 50
// b: [50 0 0 10]    ptr: 0xc00000a330    first: 50
// a: [100 0 0]    ptr: 0xc00000a330    first: 100
// b: [50 0 0 1 2 3 4]    ptr: 0xc00001a4e0    first: 50
// --- PASS: TestSliceAppend2 (0.00s)
// PASS

func TestSliceAppend3(t *testing.T) {
    a1 := make([]int, 20)
    b1 := make([]int, 40)
    a1 = append(a1, b1...)
    fmt.Println(len(a1), cap(a1))

    a2 := make([]int, 20)
    b2 := make([]int, 42)
    a2 = append(a2, b2...)
    fmt.Println(len(a2), cap(a2))
}
// === RUN   TestSliceAppend3
// 60 60
// 62 64
// --- PASS: TestSliceAppend3 (0.00s)
// PASS

切片元素删除

在要删除的元素左右切两下:a1 = append(a1[:1], a1[2:]…),删除其实是将所删元素后面的往前挪。

func TestSliceDelete(t *testing.T) {
    a := []int{30, 31, 32, 33, 34, 35, 36, 37}
    // 要删除索引为2的元素
    a = append(a[:2], a[3:]...)
    t.Log(a)
}
// === RUN   TestSliceDelete
//     TestSliceDelete: slice_test.go:98: [30 31 33 34 35 36 37]
// --- PASS: TestSliceDelete (0.00s)
// PASS

参考内容

文章目录
  1. 1. 切片定义
  2. 2. 数组与切片
  3. 3. 指向同一个底层数组的切片修改值
  4. 4. 切片表达式
  5. 5. 切片不能比较
  6. 6. 切片的 append
    1. 6.1. 计算逻辑 newcap
    2. 6.2. 实际申请内存大小
  7. 7. 切片元素删除
  8. 8. 参考内容
| | 81.7k