Go语言初见——Slice之我的浅薄认识

前言

Go语言不愧为21世纪的C语言,有着很多现代语言的特点。它有很多特别的设计,Slice是其中有趣的一个。 作为一个初学者,在这里做些笔记,记录一些浅薄的认识。

切片的本质

go/src/runtime/slice.go 源码

go 复制代码
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

从其源码来看,切片(slice)是一个结构体,保护长度(len)、容量(cap)和一个指向数组(array)的指针。

  • len()获取长度
  • cap()获取容量,及最长可以达到多少
  • cap >= len,如果不定义cap,默认len=cap
  • unsafe.Pointer指针决定了切片是引用类型

由此可见,在Go语言中切片的实现基于数组,换言之,切片是可变的数组。

切片的创建

  1. 直接声明
go 复制代码
var sli1 []int

内容为:

sli1 len cap
[] 0 0
  1. 使用new函数
go 复制代码
sli2 := *new([]int)

内容为:

sli2 len cap
[] 0 0
  1. 通过字面量
go 复制代码
sli3 := []int{1, 2, 3}

内容为:

sli3 len cap
[1 2 3] 3 3
  1. 通过make函数
go 复制代码
sli4 := make([]int, 5, 10)

内容为:

sli4 len cap
[0 0 0 0 0 ] 5 10

make的三个参数分别为:切片类型、长度、容量。

  1. 从切片或数组截取
go 复制代码
array := [5]int{1, 3, 5, 7, 9}
sli5 := array[1:5]
sli6 := sli5[1:3]

内容为:

sli5 len cap
[3 5 7 9] 4 4
sli6 len cap
[5 7] 2 3

切片的追加

如何体现切片的动态性,这就要提到切片的追加(append())。它可以在原切片基础上追加元素,同时无需考虑追加后的切片长度超出容量,底层实现会进行适当的扩容,以满足切片的动态变化。

go 复制代码
nums := make([]int, 3, 4)
fmt.Println(nums, len(nums), cap(nums))
nums = append(nums, 3)
fmt.Println(nums, len(nums), cap(nums))
nums = append(nums, 6)
// 超出cap时,追加元素时会扩容(扩容系数随着原容量增大而减小)
fmt.Println(nums, len(nums), cap(nums))

输出分别为:

0 0 0\] 3 4 \[0 0 0 3\] 4 4 \[0 0 0 3 6\] 5 8

这里,切片会在追加操作时自动扩容,其扩容的具体实现见go源码:

go/src/runtime/slice.go

go 复制代码
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
......
   newcap := oldCap
	doublecap := newcap + newcap
	if newLen > doublecap {
		newcap = newLen
	} else {
		const threshold = 256
		if oldCap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < newLen {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = newLen
			}
		}
	}
......
}

总结来看,其扩容倍数是递减的:

oldcap 扩容系数
256 2.0
512 1.63
1024 1.44
2048 1.35
4096 1.30

这有利于在保证满足切片容量需求的同时,节省了内存。通过计算,我们可以试着求出扩容系数的极限值为1.25(当oldcap远大于256)。

切片的截取

go 复制代码
// 截取 
num1 := nums[0:3]
nums[0] = 10 // nums元素发生修改,num1也会跟着修改
fmt.Println(nums)
fmt.Println(num1)
fmt.Printf("p-nums:%p, p-num2:%p\n", &nums, &num1)              // 切片本身地址
fmt.Printf("p-num-arr:%p, p-num1-arr:%p\n", nums, num1)         // 切片是指向数组的指针
fmt.Printf("p-num-arr:%p, p-num1-arr:%p\n", &nums[0], &num1[0]) // 切片指向数组的地址,第一个元素的地址即数组的首地址

输出为:

10 0 0 3 6

10 0 0

p-num1:0xc0000b6090, p-num2:0xc0000b60f0

p-nums-arr:0xc00009e080, p-num1-arr:0xc00009e080

p-nums-arr:0xc00009e080, p-num1-arr:0xc00009e080

可见,截取的过程是浅拷贝,截取获得的子切片与原切片指向同一块内存空间。 截取的范围是左闭右开的,各种奇技淫巧的写法这里不做讨论,必要时灵活运用即可。

切片的拷贝

那么如何实现深拷贝呢,这就需要用到copy()了。

go 复制代码
num2 := make([]int, 3)
copy(num2, num1)
fmt.Println(num1)
fmt.Println(num2)

fmt.Printf("p-num1:%p, p-num2:%p\n", &num1, &num2)       // 切片本身的地址
fmt.Printf("p-num1-arr:%p, p-num2-arr:%p\n", num1, num2) // 切片指向数组的地址

输出为:

10 0 0

10 0 0

p-num1:0xc0000b60f0, p-num2:0xc0000b6168

p-num1-arr:0xc00009e080, p-num2-arr:0xc0000a0030

拷贝前后的切片及其指向的数组的内存地址都是不同的,说明在执行copy()的过程中,开启了一个新的内存空间用于存放数组,并开辟一个空间用于存放指向数组的指针,完成深拷贝。

言而总之

切片的功能还是非常强大的,其丰富而灵活的语法可以满足我们对数据集合的操作,而切片作为数据类型也多次被官方修改和优化,这些修改大部分是积极的、趋于便利的、符合当代语言特性的。

相关推荐
bobz96510 分钟前
小语言模型是真正的未来
后端
DevYK1 小时前
企业级 Agent 开发实战(一) LangGraph 快速入门
后端·llm·agent
一只叫煤球的猫2 小时前
🕰 一个案例带你彻底搞懂延迟双删
java·后端·面试
冒泡的肥皂2 小时前
MVCC初学demo(一
数据库·后端·mysql
颜如玉3 小时前
ElasticSearch关键参数备忘
后端·elasticsearch·搜索引擎
卡拉叽里呱啦4 小时前
缓存-变更事件捕捉、更新策略、本地缓存和热key问题
分布式·后端·缓存
David爱编程4 小时前
线程调度策略详解:时间片轮转 vs 优先级机制,面试常考!
java·后端
码事漫谈5 小时前
C++继承中的虚函数机制:从单继承到多继承的深度解析
后端
阿冲Runner5 小时前
创建一个生产可用的线程池
java·后端
写bug写bug5 小时前
你真的会用枚举吗
java·后端·设计模式