golang 之 slice & defer 关键字

今天我们讲讲golang的slice和defer。

slice是go里面一个很重要的数据结构,使用这种结构来管理数据集合。slice 类似其他语言中的数组,但是又有一些其他不同的属性。

在正式开始讲slice之前我们来说一说golang 中的defer,原本觉得自己对defer有一定了解了。但是昨天看了一本书,才发现自己对defer 的了解是这么浅,在没有运行以下代码前你能否得出正确答案呢?运行代码之后能否回答为什么?

package main
import "fmt"
func f() (result int) {
	defer func() {
		result++
	}()
	return 0
}
func f2() (r int) {
	t := 5
	defer func() {
		t = t + 5
	}()
	return t
}
func f3() (r int) {
	defer func(r int) {
		r = r + 5
	}(r)
	return 1
}

func main() {
	fmt.Println(f())
	fmt.Println(f2())
	fmt.Println(f3())
}

答案留在最后讲解

slice

数组

在说slice之前,我们先来了解下数组。

数组是指定长度和元素类型的数据集合。比如以下数组

var intArr [3]int//创建了长度为3的int数组
a[0]=1 //根据索引访问元素
fmt.println(a[1])//int 类型数组默认值为0

数组不需要显式初始化。Go的数组是值类型。数组变量表示整个数组,它不是指向数组的第一个元素的指针,这就意味着赋值和函数传参操作时,将会复制数组的内容(后续和slice对比,slice是创建指针指向地址)。可以写一个简单的程序将数组的地址打印出来,进行对比。

slice

在实际使用中我们很少使用到数组,因为数组不够灵活。比如:一旦数组中的数据足够大,每次使用数组都要重新复制一遍,耗费大量内存和时间。slice并不会实际复制一份数据,它只是创建一个新的数据结构,包含了另外的一个指针,一个长度和一个容量数据。我们来看下slice在源码中的定义

type slice struct {
	array unsafe.Pointer// 指向底层数组的指针
	len   int// 长度,切片可用元素的个数,slice的下标不能超过长度 
	cap   int// 容量 >= 长度,在底层不扩容的情况下,cap是len的最大限度
}

需要注意的是:底层数组是可以被多个slice同时指向的,也就是对一个slice的元素进行修改会影响到其他的slice。

slice的扩容

在对slice进行append等操作时,可能会造成slice的自动扩容。我们来看下源码是怎么扩容的

// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
	//忽略部分源码
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
//忽略部分源码
	return slice{p, old.len, newcap}
}

当slice中的cap不够使用是会调用growslice函数进行扩容,具体的扩容规则是

  • 如果新的大小是当前大小2倍以上,则大小增长为新大小
  • 如果当前大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。

扩容之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。最后向 growslice 函数调用者返回一个新的 slice,这个 slice 的长度并没有变化,而容量却增大了。

nil slice 和空slice

nil slice 是描述一个不存在的切片,也就是说它的指针指向nil,没有实际上地址,长度和容量也都为0。

而空的切片是是描述空的一个集合,但是它的指针指向了一个地址,长度和容量也为0。

nil切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil 切片。

创建方式 nil切片 空切片
方式一 var s1 []int var s2 = []int{}
方式二 var s4 = *new([]int) var s3 = make([]int, 0)
长度 0 0
容量 0 0
和nil比较 true false

slice 和unsafe.Pointer相互转换

在上一篇的cgo 部分中将c的数组转到go slice 部分中有涉及到unsafe.Pointer。

我们先来看看从slice中获取一块内存地址

slice:=make([]int,10)
ptr:=unsafe.Pointer(&slice[0])//获取数组中第一个元素的内存地址

从内存指针构造出Go的slice结构会比较麻烦,总共有三种方式

第一种,先将ptr强制类型转换为另一种指针,一个指向[1<<10]int数组的指针,这里数组大小其实是假的。然后用slice操作取出这个数组的前10个,于是s就是一个10个元素的slice。

var ptr unsafe.Pointer
s := ((*[1<<10]int)(ptr))[:10]

第二种,模拟go 底层的slice结构,将结构体赋值给s。和第一种相比,这里的cap和1<<10意思是相同的

var ptr unsafe.Pointer
var s1 = struct {
    addr uintptr
    len int
    cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))

第三种方法,通过reflect.SliceHeader的方式来构造slice。

var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)

defer

不知道你是否得出文章开始代码的正确结果呢?

defer 会在当前函数或者方法返回之前执行传入的函数。它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。要真正理解文章前面的问题,我们要理解return xxx这一条语句并不是一条原子指令!

函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。

defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。

其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:

返回值 = xxx
调用defer函数
空的return

这样你就能理解为啥会得出文章一开始函数的结果了。我们来查看下改写之后的代码

func f() (result int) {
	result = 0 //return语句不是一条原子调用,return xxx其实是赋值+ret指令
	func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
		result++
	}()
	return
}
func f2() (r int) {
	t := 5
	r = t //赋值指令
	func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
		t = t + 5
	}()
	return //空的return指令
}
func f3() (r int) {
	r = 1 //给返回值赋值
	func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值
		r = r + 5
	}(r)
	return //空的return
}

接下来我们来看看另外一小段代码

func main(){
  startedAt := time.Now()
	defer fmt.Println(time.Since(startedAt))
	time.Sleep(time.Second)
}
//输出结果并不是1s,而是几百纳秒

上面的输出结果背后的原因是什么呢?经过分析,我们会发现调用 defer 关键字会立刻对函数中引用的外部参数进行拷贝,所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出。当然上面的解决办法是很简单的。只要defer 那添加一个匿名函数就可以解决了。我们总结一下defer 关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果。

参考:深入解析go中slice底层实现

深度解析Go语音之slice

深入解析Go


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!