返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

5.3 切片

发布于 2024-10-12 19:15:47 字数 10451 浏览 0 评论 0 收藏 0

切片以指针引用底层数组片段,限定读写区域。类似胖指针,而非动态数组或数组指针。

  +---------+            +---+---+----//---+----+
  |  array -|----------> | 0 | 1 | ... ... | 99 |
  +---------+            +---+---+----//---+----+
  |  len    |            
  +---------+            array
  |  cap    |
  +---------+
  
     header
     
// runtime/slice.go

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
  • 引用类型,未初始化为 nil
  • 基于数组、初始化值或 make 函数创建。
  • 函数 len 返回元素数量, cap 返回容量。
  • 仅能判断是否为 nil ,不支持其他 ==!= 操作。
  • 以索引访问元素,可获取底层数组元素指针。
  • 底层数组可能在堆上分配。

有关切片内部实现,请阅读《下卷:8.3 切片》。

+---+---+---+---+---+---+---+---+---+---+   
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |   array
+---+---+---+---+---+---+---+---+---+---+   
        |               |       |           slice: [low : high : max]
        |<--- s.len --->|       |           
        |                       |           len = high - low
        |<------- s.cap ------->|           cap = max  - low
          
func main() {
	a := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // array
    
	s := a[2:6:8]
    
	fmt.Println(s)
	fmt.Println(len(s), cap(s))

    // 引用原数组。
    
	fmt.Printf("a: %p ~ %p\n", &a[0], &a[len(a) - 1])
	fmt.Printf("s: %p ~ %p\n", &s[0], &s[len(s) - 1])
}

/*

 s: [2 3 4 5], len = 4, cap = 6

 a: 0xc00007a000 ~ 0xc00007a048
 s: 0xc00007a010 ~ 0xc00007a028
 
*/
  • slice.len :限定索引或迭代读取数据范围。
  • slice.cap :重新切片(reslice)允许范围。
func main() {
	a := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s := a[2:6:8]

	// fmt.Println(s[5])
	//             ~~~~ index out of range [5] with length 4

	for i, x := range s {
		fmt.Printf("s[%d]: %d\n", i, x)
	}
}

/*

# 切片引用原数组,此图只为方便理解。


+---+---+---+---+---+---+---+---+---+---+   
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |   a: [10]int
+---+---+---+---+---+---+---+---+---+---+   
        .                       .
        +---+---+---+---+---+---+
        | 2 | 3 | 4 | 5 |   |   |           s: a[2:6:8]
        +---+---+---+---+---+---+
        0   1   2   3   


s[0]: 2
s[1]: 3
s[2]: 4
s[3]: 5

*/
func main() {
	a := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s := a[2:6:8]

	s1 := s[0:2:4]
	fmt.Println(s1, len(s1), cap(s1))

	s2 := s[1:6]
	fmt.Println(s2, len(s2), cap(s2))
    
	// _ = s[1:7]
	//     ~~~~~~ slice bounds out of range [:7] with capacity 6
}

/*

# 切片的数据来自底层数组,重切片只是调整引用范围。
# 重切片受原 cap 限制,而非 len。


        +---+---+---+---+---+
        | 3 | 4 | 5 | 6 | 7 |     s2: s[1:6]
        +---+---+---+---+---+
        .                   .
    +---+---+---+---+---+---+
    | 2 | 3 | 4 | 5 |   |   |     s
    +---+---+---+---+---+---+
    .               .
    +---+---+---+---+
    | 2 | 3 |   |   |             s1: s[0:2:4]
    +---+---+---+---+


s1: [2 3],       len = 2, cap = 4
s2: [3 4 5 6 7], len = 5, cap = 5

*/

构造切片的常见方法。

func main() {
	a := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s := a[:]

    fmt.Println(s, len(s), cap(s))
}

/*

 expr       slice                  len   cap  
----------+-----------------------+----+-----+---------------
 a[:]       [0 1 2 3 4 5 6 7 8 9]   10    10    a[0:len(a)]
 a[2:5]     [2 3 4]                  3     8
 a[2:5:7]   [2 3 4]                  3     5
 a[4:]      [4 5 6 7 8 9]            6     6    a[4:len(a)]
 a[:4]      [0 1 2 3]                4     10   a[0:4]
 a[:4:6]    [0 1 2 3]                4     6    a[0:4:6]

*/
func main() {
    
    // 按初始化值,自动分配底层数组。
	s1 := []int{ 0, 1, 2, 3 }
	fmt.Println(s1, len(s1), cap(s1))

    // 自动创建底层数组。
	s2 := make([]int, 5)
	fmt.Println(s2, len(s2), cap(s2))

	s3 := make([]int, 0, 5)
	fmt.Println(s3, len(s3), cap(s3))
}

/*

s1: [0 1 2 3],   len = 4, cap = 4
s2: [0 0 0 0 0], len = 5, cap = 5
s3: [],          len = 0, cap = 5

*/

作为引用类型,初始化与否很重要。

编译器可能将零长度对象( len == 0 && cap == 0 )指向固定全局变量 zerobase

func p(s []int) {
	fmt.Printf("%t, %d, %#v\n", 
		s == nil,
		unsafe.Sizeof(s),
		(*reflect.SliceHeader)(unsafe.Pointer(&s)))
}

func main() {
    
    // 仅分配 header 内存,未初始化。
	var s1 []int
    
    // 初始化。
	s2 := []int{}
    
    // 调用 makeslice 初始化。
	s3 := make([]int, 0)

	p(s1)
	p(s2)
	p(s3)
}

/*

s1: true,  24, &SliceHeader{Data:0x0,      Len:0, Cap:0}
s2: false, 24, &SliceHeader{Data:0x5521d0, Len:0, Cap:0}
s3: false, 24, &SliceHeader{Data:0x5521d0, Len:0, Cap:0}

$ nm test | grep 5521d0
00000000005521d0 B runtime.zerobase

*/

不支持比较操作。

func main() {
	s1 := []int{ 1, 2 }
	s2 := []int{ 1, 2, 3 }

	println(s1 == nil)

	// println(s1 == s2)
	//         ~~~~~~~~ invalid: slice can only be compared to nil
}

函数 clear 仅将 [0, len) 范围内的元素重置为零值(memclr),不修改相关属性。

func main() {
	a := [...]int{ 1, 2, 10:100 }
	s := a[:2:6]

	fmt.Printf("%v, len:%d, cap:%d, ptr: %p\n", s, len(s), cap(s), &s[0])

	clear(s)
	fmt.Printf("%v, len:%d, cap:%d, ptr: %p\n", s, len(s), cap(s), &s[0])
}

/*
 [1 2], len:2, cap:6, ptr: 0xc000062060
 [0 0], len:2, cap:6, ptr: 0xc000062060
*/

转换

切片可直接转回数组或数组指针。

func main() {
	var a [4]int = [...]int{ 0, 1, 2, 3 }
	
	// array -> slice: 指向原数组
	var s  []int = a[:]
	println(&s[0] == &a[0])     // true

	// slice -> array: 复制底层数组(片段)
	a2 := [4]int(s)
	println(&a2[0] == &a[0])   // false

	// slice -> *array: 返回底层数组(片段)指针
	p2 := (*[4]int)(s)
	println(p2 == &a)         // true
	
}

指针

可获取元素指针,但不能以切片指针访问元素。

func main() {
	a := [...]int{ 0, 1, 2, 3 }
	s := a[:]

	p := &s      // 切片指针
	e := &s[1]   // 元素指针

	// 数组指针直接指向元素所在内存。
	// 切片指针指向 header 内存。

	// _ = p[1]
    //     ~~~~ invalid: cannot index p (variable of type *[]int)

	_ = (*p)[1]


	// 元素指针指向数组。

	*e += 100

	fmt.Println(e == &a[1])  // true
	fmt.Println(a)           // [0 101 2 3]
}

指针相关操作。

func main() {
	var a [4]int = [...]int{ 0, 1, 2, 3 }

	// 基于数组指针创建切片。
	var p *[4]int = &a
	var s []int   = p[:]

	println(&s[2] == &a[2])   // true

	// 基于非数组指针创建切片。
	p2 := (*byte)(unsafe.Pointer(&a[2]))  // 元素指针
	var s2 []byte = unsafe.Slice(p2, 8)
	
	fmt.Println(s2)
}

// [2 0 0 0 0 0 0 0]
func main() {
	var a [3]byte = [...]byte{ 'a', 'b', 'c' }
	var s []byte  = a[:]

	// 返回切片底层数组首个元素指针。
	println(unsafe.SliceData(s) == &a[0])          // true

	// 构建字符串,返回底层数组指针。
	var str string = unsafe.String(&s[0], len(s))
	println(unsafe.StringData(str) == &a[0])      // true
}

切片本身只是 3 个整数字段的小对象,可直接值传递。
另外,编译器尽可能将底层数组分配在栈上,以提升性能。

package main

//go:noinline
func sum(s []int) (n int) {
	for _, v := range s {
		n += v
	}

	return
}

func main() {
	s := []int{ 1, 2, 3 }
	println(sum(s))
}

/*

$ go build -o test
$ go tool objdump -S -s "main\.main" ./test

TEXT main.main(SB)
func main() {
        s := []int{ 1, 2, 3 }
  0x462c20              MOVQ $0x1, 0x20(SP)  ; array
  0x462c29              MOVQ $0x2, 0x28(SP)
  0x462c32              MOVQ $0x3, 0x30(SP)
        println(sum(s))                      ; header {
  0x462c3b              LEAQ 0x20(SP), AX    ;   .array = AX
  0x462c40              MOVL $0x3, BX        ;   .len   = BX
  0x462c45              MOVQ BX, CX          ;   .cap   = CX
  0x462c48              CALL main.sum(SB)    ; }
  0x462c4d              MOVQ AX, 0x18(SP)    ; sum.return

  0x462c52              CALL runtime.printlock(SB)
  0x462c57              MOVQ 0x18(SP), AX
  0x462c60              CALL runtime.printint(SB)
  0x462c65              CALL runtime.printnl(SB)
  0x462c6a              CALL runtime.printunlock(SB)
}

*/

交错

如果元素也是切片,可实现类似 交错数组 (jagged array)功能。

不同于多维数组元素等长,交错数组的元素可以是长度不等的数组。

func main() {
	s := [][]int{
		{1, 2},
		{10, 20, 30},
		{100},
	}

	s[1][2] += 100

	fmt.Println(s[1])
}

// [10 20 130]

追加

内置函数 append 向切片追加数据。

  • 数据追加到 s[len] 处。
  • 返回新切片对象,通常复用原内存。 s = append(s, ...)
  • 超出 s.cap 限制,即便底层数组尚有空间,也会重新分配内存,并复制数据。
  • 新分配内存大小,通常是 s.cap * 2 。更大切片,会减少倍数,避免浪费。
package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func pslice(name string, p *[]int) {
	fmt.Printf("%s: %#v\n", 
		name, *(*reflect.SliceHeader)(unsafe.Pointer(p)))
}

func main() {
	a := [...]int{ 0, 1, 2, 3, 99:0}
	fmt.Printf("a: %p ~ %p\n", &a[0], &a[len(a) - 1])

	s := a[:4:8]
	pslice("s", &s)

	// -----------------------------

    // 未超出 s.cap 限制。
	s = append(s, []int{4, 5, 6}...)
	pslice("a", &s)

    // 超出! 新分配数组,复制数据。
	s = append(s, []int{7, 8}...)
	pslice("a", &s)

	// -----------------------------

	fmt.Println("a:", a[:len(s)])
	fmt.Println("s:", s)
}

/*

a: 0xc00007a000 ~ 0xc00007a318
s: {Data:0xc00007a000, Len:4, Cap:8}

a: {Data:0xc00007a000, Len:7, Cap:8}
a: {Data:0xc00001e080, Len:9, Cap:16}

a: [0 1 2 3 4 5 6 0 0]
s: [0 1 2 3 4 5 6 7 8]

*/

为切片预留足够容量(cap),可有效减少内存分配和复制。

s := make([]int, 0, 10000)

拷贝

在两个切片间复制数据:

  • 允许指向同一数组。
  • 允许目标区间重叠。
  • 复制长度以较短( len )切片为准。
func main() {
	s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

	// 在同一底层数组的不同区间复制。
	src := s[5:8]
	dst := s[4:]

	n := copy(dst, src) 
	fmt.Println(n, s)

	// 在不同数组间复制。
	dst = make([]int, 6) 

	n = copy(dst, src)
	fmt.Println(n, dst)
}

/*

3 [0 1 2 3 5 6 7 7 8 9]
3 [6 7 7 0 0 0]

*/

还可直接从字符串中复制数据到字节切片。

func main() {
	b := make([]byte, 3)
	n := copy(b, "abcde")
	
	fmt.Println(n, b)
}

// 3 [97 98 99]

若切片引用大数组,那么应考虑新建并复制。及时释放大数组,避免内存浪费。

package main

import (
	"runtime"
	"time"
)

func main() {
	d := [...]byte{ 100<<20: 10 }
	
	// -- 1 --------------
	s := d[:2]

	// -- 2 --------------
	// s := make([]byte, 2)
	// copy(s, d[:])

	for i := 0; i < 5; i++ {
		time.Sleep(time.Second)
		runtime.GC()
	}

	runtime.KeepAlive(&s)
}

/*

$ go build && GODEBUG=gctrace=1 ./test

-- 1 ------------
gc 5 @6.188s 0%: ..., 100->100->100 MB, 200 MB goal, ..., 2 P (forced)

-- 2 ------------
gc 5 @5.827s 0%: ..., 0->0->0 MB, 4 MB goal, ..., 2 P (forced)

*/

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文