GO 笔记 - slice

279 阅读3分钟

slice是Go的核心数据结构,标准库中大量使用slice作为容器,保存数据。下面看一下slice结构是怎样的?

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

slice 的底层数据结构共分为三部分,如下:

  • array:指向所引用的数组指针(unsafe.Pointer 可以表示任何可寻址的值的指针)

  • len:长度,当前引用切片的元素个数

  • cap:容量,当前引用切片的容量(底层数组的元素总数)

上面的结构很简单,仅包含三个成员,通过lencap控制底层数组的大小。slice的强大是在于其可以保存任何类型的数据,能够自动扩容,比一般的数据更具通用性。但其自动扩容的特性,也会带来一些认知上的困难。例如:slice进行了扩容,指向底层的数据可能已经改变。


下面看一下`slice`的一般用法:
s := make([]string, 2, 4) // 设置 len 和 cap
s[1] = "1"

s2 := make []string, 2)   // 设置 len
s2 = append(s2, "2")

s3 := []string{"3", "4"}

下面看一下slice再切片用法:

s := []int{1, 2, 3, 4, 5}
s2 := s[2:4] // [3, 4]
s3 := s[:len(s)] //[1, 2, 3, 4, 5]
s4 := s[2:] // [3, 4, 5]

上面需要注意的是:

  • 再切片时,窗口的范围是[n, m)。

  • 上面变量s..s4使用的都是同一个底层数组。如果s2,s3,s4其中一个进行扩容时,会分配一个新的底层数组,从此与其他3个slice脱离关系;如果s..s4是修改数据的话,修改的是同一个底层数组,修改会反映到其他3个slice这点需要注意。

下面看一下使用append扩展slice的用法:

s := []int{1, 2, 3, 4, 5}
s = append(s, []int{6, 7, 8, 9}...) // [1, 2, 3, 4, 5, 6, 6, 8, 9]

s2 := []int{1, 2}
s2 = append(s2, 3, 4) // [1, 2, 3, 4]

下面看一下slicerange的用法:

package main

import (
	"fmt"
)

func main() {
	s := []int{1, 2}
	for i, v := range s {
		s = append(s, i+10)
		fmt.Printf("v: %d, &s[%d]: %p, &v: %p\n", v, i, &s[i], &v)
	}

	fmt.Println(s)
}

输出:

v: 1, &s[0]: 0xc000020120, &v: 0xc000020108
v: 2, &s[1]: 0xc000020128, &v: 0xc000020108
[1 2 10 11]

上面的例子需要注意的是:

  • 变量v是地址固定的变量,试图使用&v获取s[i]中变量的地址是错误的。
  • range后的变量s是外部变量s的一个副本。即,如果在for-range中使用append添加数据到变量s中,并不会改变ranges变量。

如何在range循环中获取slice元素的地址呢?

package main

import (
	"fmt"
)

func main() {
	s := []int{1, 2}
	sAddr := []*int{}

	for i, v := range s {
		sAddr = append(sAddr, &s[i])
		fmt.Printf("v: %d, &s[%d]: %p, &v: %p\n", v, i, &s[i], &v)
	}

	for i, v := range sAddr {
		fmt.Printf("v: %#x, *sAddr[%d]: %d, &v: %p\n", v, i, *sAddr[i], &v)
	}
}

输出:

v: 1, &s[0]: 0xc00009a010, &v: 0xc00009a020
v: 2, &s[1]: 0xc00009a018, &v: 0xc00009a020
v: 0xc00009a010, *sAddr[0]: 1, &v: 0xc00009c028
v: 0xc00009a018, *sAddr[1]: 2, &v: 0xc00009c028

如果从一个很大的切片中获取一小部分元素的地址,且该大切片就没有使用价值时,获取切片元素的地址是不值得的。因为即使获取其中一个元素地址,这个大切片也不会被垃圾回收,导致占用大量内存。更好的做法是获取大切片元素副本的地址。