一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情。
程序中的越界panic
Golang里面,异常叫做panic,我们经常遇到的一种panic是越界。
比如说,你声明了一个数组,长度是4,你非要访问下标是8的元素:
var a [4]int
x := 8
fmt.Println(a[x])
运行就会报下面的错:
panic: runtime error: index out of range [8] with length 4
翻译过来就是,runtime 错误,下标[8]超范围了,长度只有4。
这种panic是runtime行为
所谓runtime行为,就是程序跑起来之后的行为,也就是说,对数组进行下标操作的时候,也就是去取一个下标的值的时候,Go 会帮你检测一下,是否越界了,如果越界了,就会panic。
既然是上面的逻辑,那么这种逻辑是帮了我们犯更大的错误,因为越界本身,可能让你取了一个完全是未知内存里的数据,导致你的业务逻辑凉凉,比如说这一块的逻辑是跟金额有关的话。
这种行为就成为 Bounds Check,翻译就是,边界检测。
Bounds Check 的坏处
说坏处的话,那么就是慢了。尤其是你如果有大量的对于数组啦,slice啦,啥的,这种取值,重新slice这种操作的时候。
比如说,图像处理啥的, 全是这种数组或者slice。如果每个操作都要进行 check,真的会很慢。
那么有没有办法能够尽量减少这种 check 呢?
Bounds Check Elimination
翻译过来,就是边界检测消除。
看一个例子就能明白:
var a [4]int
fmt.Println(a[1])
a
是一个数组,长度是4。
我们下面打印了a
中下标1
的元素,此时此刻,完全不用runtime进行检测,为啥。这不可能越界。
这种编译时期就一定会知道不会越界的,那么runtime就应该不检测。
基于这种想法,Go 团队搞了一大堆,不需要check的场景出来。
我们来看几个:
func fn1(s []int){
_ = s[2] // 第一句
_ = s[1] // 第二句
_ = s[0] // 第三句
}
请问,上面三句,哪些需要检测,哪些不用检测。
答案: 第一句需要检测,第二句和第三句都可以不用检测。这很容易理解,都能访问下标2了,那么肯定能访问下标1和下标0。
再来看:
func fn2(s []int){
if len(s) > 2 {
_, _, _ = s[0], s[1], s[2]
}
}
由于if判断了s的长度肯定是超过2的,所以下面这一行,三个下标操作,全部都可以不用检测了。
再来看一个必须检测的:
func fn3(s []int, i int){
_ = s[:i] // 第一句
_ = s[i:] // 第二句
}
第一句,没啥可说的,肯定要进行检测。
那么第二句,是否可以消掉,不检测了呢?
答案是不行。
先看第一句,第一句中的i只要满足,i<=cap(s)。
假设,很明显,第二句肯定会出错,所以第二句还是要进行正常的检测,然后panic出来。
使用编译参数获取详细信息
如果你想知道,你代码里,哪一行进行了这种检测,你只需要在
go build 或者 go run 后面带上:
-gcflags="-d=ssa/check_bce/debug=1"
看下面的例子:
package main
import (
"fmt"
"math/rand"
)
func asd() {
a := make([]int, 3, 10)
s := rand.Int()
b := a[s:]
fmt.Println(b)
}
func main() {
}
存到main.go里,执行:
go run -gcflags="-d=ssa/check_bce/debug=1" main.go
就会输出:
# command-line-arguments
./main.go:11:8: Found IsSliceInBounds
就是说,11行,要进行检测。