变参函数
本教程共 80 篇 · 第 23 篇 · 更新于 2026-07-27 · 约 6 分钟阅读
23. 变参函数
本节目标:掌握 Go 变参函数的语法,学会传递切片给变参函数,理解变参函数的实际应用。
什么是变参函数
变参函数(Variadic function)是参数个数可变的函数。你在调用时可以传 0 个、1 个或多个值。
最典型的例子就是你一直在用的 fmt.Println:
fmt.Println("a")
fmt.Println("a", "b")
fmt.Println("a", "b", "c", 1, 2, 3, true)
它接收任意数量、任意类型的参数。
声明变参函数
在参数类型前加 ... 就变成了变参:
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
nums ...int 表示 nums 是一个 []int 类型的切片。在函数内部,nums 就是个普通切片。
调用
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20)) // 30
fmt.Println(sum()) // 0
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
}
你可以传任意数量的参数,甚至不传。
变参的本质
变参函数的参数在函数内部是一个切片:
func printArgs(args ...string) {
fmt.Printf("类型:%T\n", args) // 类型:[]string
fmt.Printf("长度:%d\n", len(args))
for i, v := range args {
fmt.Printf("args[%d] = %s\n", i, v)
}
}
调用 printArgs("Go", "Python", "Rust") 输出:
类型:[]string
长度:3
args[0] = Go
args[1] = Python
args[2] = Rust
...string 和 []string 在函数内部完全等价,区别只在调用方式。
传递切片
如果你已经有一个切片,想传给变参函数,在切片后面加 ...:
func main() {
nums := []int{1, 2, 3, 4, 5}
fmt.Println(sum(nums...)) // 15
}
nums... 表示把切片展开成一个个独立的参数。
Note
sum(nums...)和sum(nums[0], nums[1], nums[2], nums[3], nums[4])是等价的,只是写法更简洁。
变参与普通参数混用
变参可以和普通参数一起用,但变参必须放在最后:
func printf(format string, args ...any) {
fmt.Printf(format, args...)
}
func main() {
printf("名字:%s,年龄:%d\n", "Go", 18)
}
format 是普通参数,args 是变参。调用时先传普通参数,剩下的都归变参。
错误示例
// 报错:变参不在最后
func bad(args ...int, name string) {}
变参必须是参数列表的最后一个。
实际示例
求最大值
func max(nums ...int) int {
if len(nums) == 0 {
return 0
}
m := nums[0]
for _, n := range nums[1:] {
if n > m {
m = n
}
}
return m
}
func main() {
fmt.Println(max(3, 1, 4, 1, 5, 9, 2, 6)) // 9
}
TipGo 1.21 引入了内置函数
max和min,现在你不需要自己写了。直接max(3, 1, 4, 1, 5)就行。
日志函数
func log(message string, args ...any) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
fmt.Printf("[%s] %s\n", timestamp, fmt.Sprintf(message, args...))
}
func main() {
log("用户 %s 登录", "admin")
log("查询到 %d 条记录", 42)
log("服务器启动")
}
拼接字符串
func join(sep string, parts ...string) string {
if len(parts) == 0 {
return ""
}
result := parts[0]
for _, p := range parts[1:] {
result += sep + p
}
return result
}
func main() {
fmt.Println(join(", ", "Go", "Python", "Rust")) // Go, Python, Rust
}
Note实际开发中用
strings.Join更高效,这里只是演示变参函数。
变参函数的注意事项
不传参数时是 nil 切片
func test(args ...int) {
fmt.Println(args) // []
fmt.Println(args == nil) // true
}
func main() {
test() // 不传参数
}
不传参数时,args 是 nil,但 len(args) 是 0,range 也不会报错。
修改变参切片不影响原切片
func modify(nums ...int) {
for i := range nums {
nums[i] *= 2
}
}
func main() {
nums := []int{1, 2, 3}
modify(nums...)
fmt.Println(nums) // [1 2 3],没有被修改
}
变参函数内部收到的是切片的副本,修改元素不影响原切片(除非元素是指针类型)。
但如果用 append 扩容了底层数组,原切片更不受影响:
func appendItem(nums ...int) []int {
return append(nums, 999)
}
func main() {
nums := []int{1, 2, 3}
result := appendItem(nums...)
fmt.Println(nums) // [1 2 3]
fmt.Println(result) // [1 2 3 999]
}
传递切片时不会复制
func test(nums ...int) {
fmt.Printf("内部地址:%p\n", nums)
}
func main() {
nums := []int{1, 2, 3}
fmt.Printf("外部地址:%p\n", nums)
test(nums...)
}
虽然参数是值传递,但切片底层包含指针,所以 nums... 传递时不会复制底层数组,只是复制了切片头(指针、长度、容量)。
any 类型变参
fmt.Printf 的签名是 func Printf(format string, a ...any),用 any 接收任意类型的参数。
any 是 Go 1.18 引入的 interface{} 的别名:
type any = interface{}
用 ...any 可以让函数接收任意类型、任意数量的参数:
func printAll(args ...any) {
for _, v := range args {
fmt.Printf("%v (%T)\n", v, v)
}
}
func main() {
printAll("hello", 42, 3.14, true)
}
// 输出:
// hello (string)
// 42 (int)
// 3.14 (float64)
// true (bool)
常见问题
传切片忘了加 …
nums := []int{1, 2, 3}
sum(nums) // 报错:cannot use nums as int
变参函数期望的是一个个 int,你传了一个 []int,类型不匹配。需要用 nums... 展开。
变参传给变参
func outer(args ...int) {
inner(args...) // 正确,用 ... 展开
}
func inner(args ...int) {
// ...
}
要把变参传递给另一个变参函数,加 ... 展开就行。
变参和 nil
func test(args ...int) {
// ...
}
test(nil) // 报错:cannot use nil as int
不能直接传 nil。如果需要传空,不传参数就行:test()。
学完这节你能做什么
- 用
...T声明变参函数 - 用
slice...传递切片给变参函数 - 变参和普通参数混用
- 理解变参在函数内部是切片
- 用
...any接收任意类型参数
下一节讲匿名函数和闭包。