Go 基础丨切片 slice

1. 底层

  • runtime/slice.go

    type slice struct {
    	array unsafe.Pointer		// 指向底层数组
    	len   int								// 切片元素数量
    	cap   int								// 底层数组容量
    }
    
  • reflect/value.go

    type SliceHeader struct {
    	Data uintptr
    	Len  int
    	Cap  int
    }
    

2. 创建

  • 根据数组创建

    s := arr[0:3]
    
  • 字面量:编译时插入创建数组的代码

    s := []int{1, 2, 3}
    
  • make:运行时创建数组

    s := make([]int, 10)
    

3. 测试

3.1 字面量创建切片底层

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

查看 Plan9 汇编代码,运行:

go build -gcflags -S main.go

重点关注 s := []int{1,2,3} 对应的部分:

LEAQ    type.[3]int(SB), AX						#创建一个大小为3,类型为int的数组
PCDATA  $1, $0
NOP
CALL    runtime.newobject(SB)				  #新建一个结构体(slice)的值,并往里面塞3个数组
MOVQ    $1, (AX)
MOVQ    $2, 8(AX)
MOVQ    $3, 16(AX)

3.2 make 创建切片

func main() {
	s := make([]int, 3)
	fmt.Println(s)
}

查看 Plan9 汇编代码,运行:

go build -gcflags -S main.go

重点关注 s := make([]int, 3) 对应的部分:

LEAQ    type.int(SB), AX
MOVL    $3, BX
MOVQ    BX, CX
PCDATA  $1, $0
CALL    runtime.makeslice(SB) 		#直接调用 makeslice 方法

4. 访问

  • 下标访问
  • range 遍历
  • len 查看切片长度
  • cap 查看数组容量

5. 追加

s := []int{1, 2, 3}
s = append(s, 4, 5)

如果 append 后,len > cap,则需要做扩容。

6. 扩容

底层调用 growslice() 方法:

6.1 Go1.17 及之前的 growslice()

func growslice(et *_type, old slice, cap int) slice {
  // 检查
	...

  // 确定新 cap
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
    // ① 如果新 cap 大于两倍旧 cap,则直接使用新 cap
		newcap = cap
	} else {
    // ② 如果新 cap 小于两倍旧 cap 且旧 cap 小于 1024,则 cap 直接翻倍
		if old.cap < 1024 {
			newcap = doublecap
		} else {
      // ③ 如果新 cap 小于两倍旧 cap 且旧 cap 大于 1024,则每次增长 25%
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	
  // 新建数组,复制,字节对齐
  ....
}

6.2 Go1.18 的 growslice()

func growslice(et *_type, old slice, cap int) slice {
  // 检查
	...

  // 确定新 cap
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
    // ① 如果新 cap 大于两倍旧 cap,则直接使用新 cap
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
      // ② 如果新 cap 小于两倍旧 cap 且旧 cap 小于 256,则 cap 直接翻倍
			newcap = doublecap
		} else {
			// ③ 如果新 cap 小于两倍旧 cap 且旧 cap 大于 256,则增加幅度逐渐从 2x 降到 1.25x
      // 原始容量  			扩容系数
      //	 256					2.0
      //	 512          1.63
      //   1024         1.44
      //   2048         1.35
      //   4096         1.30
			for 0 < newcap && newcap < cap {
				newcap += (newcap + 3*threshold) / 4
			}
			...
		}
	}
	
  // 新建数组,复制,字节对齐
  ....
}

总结

  • Go1.17 及以前
    • 如果 newcap 大于 2*oldcap,则直接使用 newcap
    • 否则
      • 如果 oldcap < 1024,则 2*oldcap
      • 如果 oldcap >= 1024,则 1.25*oldcap
  • Go1.18
    • 如果 newcap 大于 2*oldcap,则直接使用 newcap
    • 否则
      • 如果 oldcap < 256,则 2*oldcap
      • 如果 oldcap >= 256,则 oldcap += (oldcap + 3*256) / 4

在 Go1.18 中,优化了切片在容量较大时扩容的策略,让底层数组大小的增长更加平滑:通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从 2 到 1.25 的突变。该 commit 作者给出了几种原始容量下对应的“扩容系数”:

原始容量扩容系数
2562.0
5121.63
10241.44
20481.35
40961.30

PS:slice 在扩容的时候是并发不安全的,在并发访问的时候,需要加锁。

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后端工程师孔乙己

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值