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]
}