Golang Go语言 Slice 详解

发布于 1周前 作者 caililin 来自 Go语言

今天被一道题目恶心到了, 发现不研究这些东西可能真的活不下去了, 狠下心来读了一个多小时的源码, 写下些自己对 Slice 的见解吧.

先说说那个题目.

// https://play.golang.org/p/2fA3BylTgtf

// 请问 s1 和 s2 的值分别是? func main() { s1 := []int{1, 2, 3} s2 := s1[:0]

s2 = append(s2, 4)

fmt.Println(s1)
fmt.Println(s2)

} //========== // [4 2 3] // [4]

Slice 定义

先看看 SliceGo 底层的定义

// https://github.com/golang/go/blob/master/src/reflect/value.go#L1806

type sliceHeader struct { Data unsafe.Pointer // Array pointer Len int // slice length Cap int // slice capacity }

原理讲解

第一行

s1 := []int{1, 2, 3} 是将 [1, 2, 3] 的首地址 存入了 Data 中, 设置了 Len 为 3, 设置了 Cap 为 3.

// https://play.golang.org/p/bjP8BKtwQQl

// 验证代码. func main() { s1 := []int{1, 2, 3} // 我们可以先将它转成 *reflect.SliceHeader 类型. // *reflect.SliceHeader // 定义: https://github.com/golang/go/blob/master/src/reflect/value.go#L1800 // 顺带着再说一句 uintptr: uintptr 是整型, 可以足够保存指针的值得范围, // 在 32 平台下为 4 字节,在 64 位平台下是 8 字节 sliceHeader1 := (*reflect.SliceHeader)((unsafe.Pointer)(&s1)) fmt.Printf(“data address: %#0x, len: %d, cap: %d\n”, sliceHeader1.Data, sliceHeader1.Len, sliceHeader1.Cap) } //===== // data address: 0x10414020, len: 3, cap: 3

第二行

s2 := s1[:0] 是将 s1Data 中的值, 赋值给了 s2Data 中, 设置 Len 为 0, s1Cap 赋值给了 s2Cap.

上面这一段有可能不太好理解, 我直接拿出源码来说.

// https://github.com/golang/go/blob/master/src/reflect/value.go#1559

func (v Value) Slice(i, j int) Value { // … 略过无用代码 switch kind := v.kind(); kind { // … case Slice: typ = (*sliceType)(unsafe.Pointer(v.typ)) s := (*sliceHeader)(v.ptr) base = s.Data cap = s.Cap } // …

// Declare slice so that gc can see the base pointer in it.
var x []unsafe.Pointer

// Reinterpret as *sliceHeader to edit.
s := (*sliceHeader)(unsafe.Pointer(&x))
// 这里是给 s2.Len 进行赋值. s1[:0]  i 没有传所以为 0, j 也为 0, 所以 j - i ...
s.Len = j -
// 这里是给 s2.Cap 进行赋值. cap 在上面的 case 中 被赋值为 3, 3 - 0  emmm...
s.Cap = cap - i
// if 为真
if cap-i > 0 {
      // 所以这里是给 s2.Data 进行赋值.
      // arrayAt 的 4 个参数类型:
      // p unsafe.Pointer, i int, eltSize uintptr, whySafe string
      // base 是 s1.Data, i 是 0, eltSize 这个值是根据类型来的,
      // 在当前例子里是 []int, int 在根据系统, 32 平台下为 4 字节,在 64 位平台下是 8 字节
      // 最后一个参数 whySafe 可能是为了做个记录吧... 而且必须说明为啥安全...
      s.Data = arrayAt(base, i, typ.elem.Size(), "i < cap")
} else {
      // do not advance pointer, to avoid pointing beyond end of slice
      s.Data = base
}

}

// https://github.com/golang/go/blob/master/src/reflect/value.go#1826 func arrayAt(p unsafe.Pointer, i int, eltSize uintptr, whySafe string) unsafe.Pointer { // 以系统 64 位 为例 // 传的值分别是 s1.Data(0x10414020), 0*8, “i < len” return add(p, uintptr(i)*eltSize, “i < len”) }

// https://github.com/golang/go/blob/master/src/reflect/type.go#1079 func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer { // 所以这里就相当于 0x10414020+0 return unsafe.Pointer(uintptr§ + x) }

// https://play.golang.org/p/pA6coJh2bSg

// 验证代码
func main() {
  	s1 := []int{1, 2, 3}
  	s2 := s1[:0]
  	sliceHeader2 := (*reflect.SliceHeader)((unsafe.Pointer)(&s2))
  	fmt.Printf("data address: %#0x, len: %d, cap: %d\n",
        sliceHeader2.Data, sliceHeader2.Len, sliceHeader2.Cap)
}
//=====
// data address: 0x10414020, len: 0, cap: 3

可以看见 s1.Datas2.Data 地址都是 0x10414020

到了这里你可能会问如果地址一样, 为什么 访问 s2[2] 会报错. runtime error: index out of range

其实猜也能大概猜到, 因为你获取数据的时候 程序是判断了 s2.Len 的.

代码位置在: https://github.com/golang/go/blob/master/src/reflect/type.go#870 这个函数里面有写.

结论

emm.. 不知道....


Golang Go语言 Slice 详解

更多关于Golang Go语言 Slice 详解的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

12 回复

mark

更多关于Golang Go语言 Slice 详解的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


安利一下这篇文章,里面讲解了你遇到的问题: https://jiajunhuang.com/articles/2017_07_18-golang_slice.md.html

哦,还有作者也在 V2

预料到是这样的结果,没预料到的是并不总是这样的结果(如二楼文章所说的,取决于 append 是否扩容),这有点蛋疼。

遇到过坑,中间插入

array = append (append (array [0: index],item), array [index:]…)

然而这段代码能运行却不能实现中间插入的效果

看来还是得老老实实用 copy

对 slice 的修改是可能会修改原 array 的,需要特别注意。

哈哈哈, 看了你这篇文章学到了不少东西, 原来 runtime 包里面还有更神奇的东西…

使用 slice 时要时刻提醒自己 slice 只是指向底层数组的一部分,即便是以 slice 做参数,也知识传递一个 slice 的 header 的复制,在涉及添加操作时一定要使用指针。不得不说确实容易导致 bug。

以下是对Golang中Slice的详细解析:

Slice是Go语言中的一种动态数组类型,提供了比固定长度的数组更灵活的操作方式。Slice的结构体包含三个字段:指向底层数组的指针(array)、长度(len)和容量(cap)。

Slice的创建可以通过make函数,也可以从数组或已有的Slice中进行切片操作。使用make函数创建Slice时,可以指定其长度和容量。Slice的数据访问和数组类似,可以使用下标来访问元素。

Slice的一个重要特性是其动态扩容能力。当向Slice添加元素而容量不足时,Slice会自动扩容。扩容策略通常是将容量翻倍,以减少扩容次数。但需要注意的是,扩容会导致内存重新分配和数据复制,因此频繁扩容会影响性能。

此外,Slice支持多种操作,如追加元素(使用append函数)、切片操作(获取子集)、复制Slice(使用copy函数)、删除元素(通过切片操作)等。这些操作使得Slice在处理动态数据集时非常有用。

总的来说,Slice是Go语言中非常强大和灵活的数据结构,适用于各种场景,如函数参数传递、并发操作、数据聚合和算法实现等。但开发者在使用Slice时,也需要注意其性能特性,如预分配内存以减少扩容开销等。

回到顶部