Go 对参数的处理偏向保守,不支持有默认值的可选参数,不支持命名实参。调用时,必须按签名顺序传递指定类型和数量的实参,就算以 _
命名的参数也不能忽略。
在参数列表中,相邻的同类型参数可合并。
参数可视作函数局部变量,因此不能在相同层次定义同名变量。
形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数等。
不管是指针、引用类型,还是其他类型参数,都是值拷贝传递(pass-by-value)。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。
其实在栈上复制小对象只须很少的指令即可完成,远比运行时进行堆内存分配要快得多。另外,并发编程也提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态,自然使用指针更好。
下面是一个指针参数导致实参变量被分配到堆上的简单示例。可对比传值参数的汇编代码,从中可看出具体的差别。
func test(p *int) {
go func() { // 延长p生命周期
println(p)
}()
}
func main() {
x := 100
p := &x
test(p)
}
$go build-gcflags"-m" // 输出编译器优化策略
moved to heap:x
&x escapes to heap // 逃逸
$go tool objdump-s"main\.main"test
TEXT main.main(SB)test.go
CALL runtime.newobject(SB) // 在堆上为x分配内存
CALL main.test(SB)
如果函数参数过多,建议将其重构为一个复合结构类型,也算是变相实现可选参数和命名实参功能。
type serverOption struct {
address string
port int
path string
timeout time.Duration
log *log.Logger
}
func newOption() *serverOption{
return &serverOption{ // 默认参数
address: "0.0.0.0",
port: 8080,
path: "/var/test",
timeout: time.Second * 5,
log: nil,
}
}
func server(option *serverOption) {}
func main() {
opt := newOption()
opt.port = 8085 // 命名参数设置
server(opt)
}
将过多的参数独立成 option struct,既便于扩展参数集,也方便通过 newOption 函数设置默认配置。这也是代码复用的一种方式,避免多处调用时烦琐的参数配置。
变参
变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。
将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片。
func test(a ...int) {
fmt.Println(a)
}
func main() {
a := [3]int{ 10, 20, 30 }
test(a[:]...) // 转换为slice后展开
}
既然变参是切片,那么参数复制的仅是切片自身,并不包括底层数组,也因此可修改原数据。如果需要,可用内置函数 copy 复制底层数据。
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]
}