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

言而总之

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

相关推荐
2401_895521346 小时前
SpringBoot Maven快速上手
spring boot·后端·maven
disgare6 小时前
关于 spring 工程中添加 traceID 实践
java·后端·spring
ictI CABL6 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
小江的记录本8 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
yhole11 小时前
springboot三层架构详细讲解
spring boot·后端·架构
香香甜甜的辣椒炒肉11 小时前
Spring(1)基本概念+开发的基本步骤
java·后端·spring
@atweiwei12 小时前
深入解析gRPC服务发现机制
微服务·云原生·rpc·go·服务发现·consul
白毛大侠12 小时前
Go Goroutine 与用户态是进程级
开发语言·后端·golang
ForteScarlet12 小时前
从 Kotlin 编译器 API 的变化开始: 2.3.20
android·开发语言·后端·ios·开源·kotlin
大阿明12 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端