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()的过程中,开启了一个新的内存空间用于存放数组,并开辟一个空间用于存放指向数组的指针,完成深拷贝。

言而总之

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

相关推荐
姑苏洛言30 分钟前
搭建一款结合传统黄历功能的日历小程序
前端·javascript·后端
你的人类朋友32 分钟前
🍃认识一下boomi
后端
苏三说技术37 分钟前
MySQL的三大日志
后端
豌豆花下猫1 小时前
让 Python 代码飙升330倍:从入门到精通的四种性能优化实践
后端·python·ai
南雨北斗2 小时前
TP6使用PHPMailer发送邮件
后端
你的人类朋友2 小时前
🤔什么时候用BFF架构?
前端·javascript·后端
争不过朝夕,又念着往昔3 小时前
Go语言反射机制详解
开发语言·后端·golang
绝无仅有4 小时前
企微审批对接错误与解决方案
后端·算法·架构
叹人间,美中不足今方信4 小时前
gRPC服务发现
rpc·go·服务发现
Code季风4 小时前
将 gRPC 服务注册到 Consul:从配置到服务发现的完整实践(上)
数据库·微服务·go·json·服务发现·consul