slice是Go的核心数据结构,标准库中大量使用slice作为容器,保存数据。下面看一下slice结构是怎样的?
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice 的底层数据结构共分为三部分,如下:
-
array:指向所引用的数组指针(
unsafe.Pointer可以表示任何可寻址的值的指针) -
len:长度,当前引用切片的元素个数
-
cap:容量,当前引用切片的容量(底层数组的元素总数)
-
上面的结构很简单,仅包含三个成员,通过len和cap控制底层数组的大小。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]
下面看一下slice与range的用法:
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中,并不会改变range后s变量。
如何在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
如果从一个很大的切片中获取一小部分元素的地址,且该大切片就没有使用价值时,获取切片元素的地址是不值得的。因为即使获取其中一个元素地址,这个大切片也不会被垃圾回收,导致占用大量内存。更好的做法是获取大切片元素副本的地址。