likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 41

前言

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

切片的本质

go/src/runtime/slice.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. 直接声明
var sli1 []int

内容为:

sli1lencap
[]00
  1. 使用new函数
sli2 := *new([]int)

内容为:

sli2lencap
[]00
  1. 通过字面量
sli3 := []int{1, 2, 3}

内容为:

sli3lencap
[1 2 3]33
  1. 通过make函数
sli4 := make([]int, 5, 10)

内容为:

sli4lencap
[0 0 0 0 0 ]510

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

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

内容为:

sli5lencap
[3 5 7 9]44
sli6lencap
[5 7]23

切片的追加

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

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

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扩容系数
2562.0
5121.63
10241.44
20481.35
40961.30

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

切片的截取

// 截取 
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()了。

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

言而总之

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

转载自:https://juejin.cn/post/7306330038399369256
评论
请登录