1、参数
在 Go 中,不管是指针、引用类型,还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象,还是拷贝指针而已。在函数面前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象的生命周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。
其实在栈上分配小对象只需要很少的指令即可完成,远比运行时进行堆内存分配要快得多。另外,并发编程也提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态,自然使用指针更好。
package main
func test(p *int) {
go func() {
println(p)
}()
}
func main() {
x := 100
p := &x
test(p)
}
可执行:go build -gcflags “-m”
2、变参
变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。
既然变参是切片,那么参数复制的仅是切片自身,并不包括底层数组,也因此可修改原数据。如果需要,可用内置函数 copy 复制底层数据。
package main
import "fmt"
func test(a ...int) {
for i := range a {
a[i] += 100
}
}
func main() {
a := []int{10, 20, 30}
test(a...)
fmt.Println(a)
}
输出结果: [110 120 130]
3、闭包
闭包是其在词法上下文中引用了自由变量的函数,或者说是函数和其引用环境的组合体。
package main
func test(x int) func() {
return func() {
println(x)
}
}
func main() {
f := test(123)
f()
}
输出结果: 123
就这段代码而言,test 返回的匿名函数会引用上下文环境变量 x。当该函数在 main 中执行时,它依然可正确读取 x 的值,这种现象称作闭包。
package main
func test(x int) func() {
println(&x)
return func() {
println(&x, x)
}
}
func main() {
f := test(0x100)
f()
}
输出结果:
0xc000018070
0xc000018070 256
通过输出指针,我们注意到闭包直接引用了原环境变量。闭包是函数和引用环境的组合体。
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓“延迟求值”的特性。
package main
func test() []func() {
var s []func()
for i := 0; i < 2; i++ {
s = append(s, func() {
println(&i, i)
})
}
return s
}
func main() {
for _, f := range test() {
f()
}
}
输出:
0xc000018070 2
0xc000018070 2
针对这个结果,由于 for 循环复用局部变量i,那么每次添加的匿名函数引用的自然是同一变量。添加操作仅仅是将匿名函数放入列表,并未执行。因此,当 main 执行这些函数时,它们读取的是函数变量 i 最后一次循环时的值,2。
解决方法是每次用不同的环境变量或传参复制,让各自闭包环境各不相同。
package main
func test() []func() {
var s []func()
for i := 0; i < 2; i++ {
x := i
s = append(s, func() {
println(&x, x)
})
}
return s
}
func main() {
for _, f := range test() {
f()
}
}
输出:
0xc000018070 0
0xc000018078 1
多个匿名函数引用同一环境变量,也会让事情变得更加复杂。任何的修改行为都会影响其他函数的取值,在并发模式下可能需要做同步处理。
package main
func test(x int) (func(), func()) {
return func() {
println(x)
x += 10
}, func() {
println(x)
}
}
func main() {
a, b := test(100)
a()
b()
}
输出:
100
110
4、延迟调用
多个延迟注册按 FIFO 次序执行。
package main
func main() {
defer println("a")
defer println("b")
}
输出:
b
a
编译器通过插入额外指令来实现延迟调用执行,而 return 和 panic 语句都会终止当前函数流程,引发延迟调用。另外,return 语句不是 ret 汇编指令,它会先更新返回值。
package main
func test() (z int) {
defer func() {
println("defer:", z)
z += 100
}()
return 100
}
func main() {
println("test:", test())
}
输出:
defer: 100
test: 200
相比直接用 CALL 汇编指令调用函数,延迟调用则须花费更大代价。这其中包括注册、调用等操作,还有额外的缓存开销。
go test -v -bench=.
输出:
goos: darwin
goarch: amd64
BenchmarkCall-8 100000000 14.4 ns/op
BenchmarkDefer-8 30000000 43.4 ns/op
5、错误处理
在代码中,若连续调用 panic,仅最后一个会被 recover 捕获。
package main
import "log"
func main() {
i := 0
defer func() {
for {
i++
if i >= 5 {
break
}
if err := recover(); err != nil {
log.Println(err)
} else {
log.Println("fatal")
}
}
}()
defer func() {
panic("hello world")
}()
panic("hi~~, world")
}
执行结果为:
2019/09/12 08:38:08 hello world
2019/09/12 08:38:08 fatal
2019/09/12 08:38:08 fatal
2019/09/12 08:38:08 fatal