GO数据结构——slice

195 阅读3分钟

1. 引言

go语言中的切片提供了一种方便有效的方法来处理类型化数据序列。本文重点关注于切片的数据结构,在函数和方法中的传递,切片长度和容量,切片拷贝

2. 切片的定义

切片是一种数据结构,描述了与切片变量本身区分开的一片连续存储的数组空间。切片不是数组,而是描述了一块连续的数组区域

A slice is a data structure describing a contiguous section of an array stored separately from the slice variable itself. A slice is not an array. A slice describes a piece of an array.

切片由三个字段的数据结构组成,这些数据结构包含Go语言需要操作底层数组的元数据。见图1

slice底层数据结构 相关数据结构如下,这三个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

3. 切片在函数中的传递

切片本身是一个包含三个元素的数据结构,切片在函数中的传递是sliceheader的拷贝。 case1:函数中传递切片,不改变切片的长度

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}
func main() {
    buffer := [100]int{0}
    slice := buffer[10:20]
    for i := 0; i < len(slice); i++ {
        slice[i] = byte(i)
    }
    fmt.Println("before", slice)
    AddOneToEachElement(slice)
    fmt.Println("after", slice)
}

输出结果如下:

before [0 1 2 3 4 5 6 7 8 9]
after [1 2 3 4 5 6 7 8 9 10]

即使切片头通过值传递到了函数中,但是原始的切片数据结构和传递到函数中的切片数据结构拷贝描述的是同一块数据空间。因此函数结果返回时,修改过的数据在原始切片中也能看见。底层数据结构如下图2所示:

函数调用之后两个切片指向同一个底层数组 case2:函数中传递切片,改变传递切片的长度

func SubtractOneFromLength(slice []byte) []byte {
    slice = slice[1 : len(slice)-1]
    slice[0] = 2
    return slice
}

func main() {
    buffer := [256]byte{0}
    slice := buffer[10:20]
    for i := 0; i < len(slice); i++ {
        slice[i] = byte(i)
    }
    fmt.Println("Before: len(slice) =", len(slice))
    newSlice := SubtractOneFromLength(slice)
    fmt.Println("After:  len(slice) =", len(slice))
    fmt.Println("After:  len(newSlice) =", len(newSlice))
    fmt.Printf("After slice %+v\n", slice)
    fmt.Printf("After newSlice %+v\n", newSlice)
}

输出结果如下:

Before: len(slice) = 10
After:  len(slice) = 10
After:  len(newSlice) = 8
After slice [0 2 2 3 4 5 6 7 8 9]
After newSlice [2 2 3 4 5 6 7 8]

正如我们所看到的,slice变量的内容是可以在函数中被改变的,但是sliceheader不能,存储在sliceheader中的长度是不可以被函数修改的。传到函数中的sliceheader是原来切片的一个副本,如果我们想修改sliceheader中的内容,则必须在返回值中包含此切片。

4. 切片在方法中的传递

我们知道go语言的方法可以接受值或者指针。那么传递指向切片的指针或者切片头又会发生什么呢。这有如下的例子 case1: 传递切片指针

type path []byte

func (p *path) TruncateAtFinalSlash() {
    i := bytes.LastIndex(*p, []byte("/"))
    if i >= 0 {
        *p = (*p)[0:i]
    }
}

func main() {
    pathName := path("/usr/bin/tso") // Conversion from string to path.
    pathName.TruncateAtFinalSlash()
    fmt.Printf("%s\n", pathName)
}

输出结果如下:

/usr/bin

通过运行结果可以得知,传递指向sliceheader的指针,使用此方式达到在方法中修改切片头,切片所描述的内容的目的 case2:传递值

type path []byte

func (p path) ToUpper() {
    for i, b := range p {
        if 'a' <= b && b <= 'z' {
            p[i] = b + 'A' - 'a'
        }
    }
}

func main() {
    pathName := path("/usr/bin/tso")
    pathName.ToUpper()
    fmt.Printf("%s\n", pathName)
}

输出结果如下:

/USR/BIN/TSO

此方法接受的是值,传递的slice header是原切片变量的拷贝。在未破坏原切片的底层指向内容时,两个切片头部指向的是同一块内存区域。所以原切片也可以看见修改过后的内容

5. 切片容量

通过对切片头结构的了解,切片的第三个变量为切片的容量,描述当前切片可以容纳的最大元素长度。在切片容量未发生变化时,切片头部的多个副本在底层共享同一块内存区域。当切片的容量发生变化之后,新的底层数组就会原来的数组分离。

6. 切片增长

相对于数组而言,使用切片的一个好处是可以按需增加切片的容量,Go语言内置的append函数会处理增长长度的内部逻辑。当切片容量不足时,切片容量少于1024,扩容按照倍数增长。如果大于1024,则按照25%的步长增长知道满足需要的容量

7. 参考阅读