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

言而总之

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

相关推荐
weixin_985432112 小时前
Spring Boot 中的 @ConditionalOnBean 注解详解
java·spring boot·后端
猎人everest2 小时前
快速搭建运行Django第一个应用—投票
后端·python·django
啾啾Fun4 小时前
精粹汇总:大厂编程规范(持续更新)
后端·规范
yt948324 小时前
lua读取请求体
后端·python·flask
IT_10245 小时前
springboot从零入门之接口测试!
java·开发语言·spring boot·后端·spring·lua
汪子熙5 小时前
在 Word 里编写 Visual Basic 调用 DeepSeek API
后端·算法·架构
寻月隐君6 小时前
手把手教你用 Solana Token-2022 创建支持元数据的区块链代币
后端·web3·github
代码丰6 小时前
使用Spring Cloud Stream 模拟生产者消费者group destination的介绍(整合rabbitMQ)
java·分布式·后端·rabbitmq
烛阴7 小时前
Cheerio DOM操作深度指南:轻松玩转HTML元素操作
前端·javascript·后端
Hello.Reader7 小时前
在多云环境透析连接ngx_stream_proxy_protocol_vendor_module
后端·python·flask