第十二篇 可变参数函数

欢迎来到Golang教程系列的第十二篇

什么是可变参数函数

函数一般都只接收固定数量的参数,而可变参数函数则可以接收可变数量的参数。如果函数的最后一个参数的前缀是省略号...,那么这个函数就可以接收任意数量的参数。

只有函数的最后一个参数才可以作为可变参数,我们会在这个教程下一个节学习这个的原因

语法

func hello(a int, b ...int) {

}

在上面的函数中,(paramer)形参数b是可变参数,因此它以...为前缀,它可以接收任意数目的(argument)实参数。上面函数可以用下面的语法调用。

hello(1, 2) // 给b传递实参2
hello(5, 6, 7,8, 9) //给b传递实参5,6,7,8,9

在上面的代码块,第一个函数调用只传了一个实参2给形参b,第二个函数调用传递了5, 6, 7,8, 9给形参b。

当然,也可以不传参数给b

hello(1)

在上面代码中,我们调用hello时没有给b传递任何参数,这个也是没有问题的。

现在,我想你应该明白为什么可变参数只能是最后一个参数了。

如果说我们非要让第一个参数为可变参数的话,就会跟下面一样。

func hello(b ...int, a int) {

}

在上面的函数中,你是不可能给a赋值的,因为你传递的任何参数都会赋值给第一个参数b,因为它是可变参数。因此,可变参数只能出现在函数的最后一位定义。所以上面的函数编译时就会报错syntax error: cannot use ... with non-final parameter b

例子&理解可变参数函数是怎么运行的

我们创建一个我们的可变参数函数。我们写一个简单的程序来判断一个整数是否在输入的整数列表中。

package main

import (
	"fmt"
)

func find(num int, nums ...int) {
	fmt.Printf("type of nums is %T\n", nums)
	found := false
	for i,v := range nums {
		if v == num {
			fmt.Println(num, "found at index", i, "in", nums)
			found = true 
		}
	}
	if !found {
		fmt.Println(num, "not found in ", nums)
	}

	fmt.Printf("\n")
}

func main() {

	find(89, 89, 90, 95)
    find(45, 56, 67, 45, 90, 109)
    find(78, 38, 56, 98)
    find(87)
}

在上面的程序中,func find(num int, nums ...int) num形参接收不同数量的实参,在find函数里面,nums的类型是[]int,一个整数切片。

可变参数函数的运作方式是把可变参数转变成切片类型,例如在上面的程序中,find(89, 89, 90, 95)89, 90, 95find函数的可变参数。find函数期望一个可变的int类型参数。因此,这三个参数会被编译器转换成int类型的切片[]int{89, 90, 95},然后传输给find函数的nums变量

find函数中,for循环遍历nums切片,判断当前切片的值是否等于num,然后打印num,不然则打印没找到对应数字。

上面的程序输出:

type of nums is []int
89 found at index 0 in [89 90 95]

type of nums is []int
45 found at index 2 in [56 67 45 90 109]

type of nums is []int
78 not found in  [38 56 98]

type of nums is []int
87 not found in  []

在上面程序中,find(87)函数调用只有一个参数,我们并没有给可变参数nums ...int传递任何实参。就如早先讨论的,这是完全合法的。nums会成为一个nil切片,并且容量capacity为0。

Slice arguments vs Variadic arguments

我们肯定有一个问题在你脑海中挥之不去,在上面的章节中,我们学习了函数中可变参数转变成切片。那为什么我们需要可变参数,而不是直接使用切片来实现呢?下面我用切片重写了上面的程序。

package main

import (
	"fmt"
)

func find(num int, nums []int) {
	fmt.Printf("type of nums is %T\n", nums)
	found := false
	for i,v := range nums {
		if v == num {
			fmt.Println(num, "found at index", i, "in", nums)
			found = true 
		}
	}
	if !found {
		fmt.Println(num, "not found in ", nums)
	}

	fmt.Printf("\n")
}

func main() {

	find(89, []int{89, 90, 95})
    find(45, []int{56, 67, 45, 90, 109})
    find(78, []int{38, 56, 98})
    find(87, []int{})
}

下面是使用可变参数代替切片的优势:

  1. 不需要在每一个函数调用时创建一个切片。如果你看到上面的程序,每一个函数调用都需要创建一个新的切片。而使用可变参数,就可以避免额外的切片创建处理。

  2. 在上面程序中,find(87, []int{})我们创建了一个空白的切片,只是为了满足find函数的签名(参数要求)?当使用可变参数函数时,直接find(87)即可。

  3. 我个人觉得使用可变参数函数比直接使用切片具有更好的可读性。

Append 是一个可变参数函数

不知道你有没有疑惑过,标准库中的append函数是怎么给切片附加上任意数的参数的?因为它就是个可变参数函数。

func append(slice []Type, elems ...Type) []Type

上面就是append函数的定义,而elems就是一个可变参数。因此,append函数可以接收不同数量的参数。

给可变参数函数传递切片

我们给可变参数函数传递切片,找出在下面的程序中会发生什么?

package main

import (
	"fmt"
)

func find(num int, nums ...int) {
	fmt.Printf("type of nums is %T\n", nums)
	found := false
	for i,v := range nums {
		if v == num {
			fmt.Println(num, "found at index", i, "in", nums)
			found = true 
		}
	}
	if !found {
		fmt.Println(num, "not found in ", nums)
	}

	fmt.Printf("\n")
}

func main() {

	nums := []int{89, 90, 95}
	find(89, nums)
}

在上面程序中,find函数要求的是可变数量的参数,但我们传递了个切片。

所以上面的程序无效,编译时会报错cannot use nums (type []int) as type int in argument to find

那为什么程序无效呢?答案是相当直接的,下面提供的是find函数的函数签名(function signature)(A function's signature includes the function's name and the number, order and type of its formal parameters.)

func find(num int, nums ...int)

根据可变参数函数的定义,nums ...int意思是只接收可变数量的int类型的参数。

在上面程序中,find(89, nums), 传递的是切片,而find函数要求的是可变的int类型参数。正如我们之前讨论的,这些int类型参数会转换成int类型的切片。在上面程序中,nums是一个[]int切片,而编译器会尝试区创建一个新的[]int.

find(89, []int(nums))

上面的执行会失败,因为nums[]int而不是int

那有么有办法给可变参数函数传递切片呢?答案是当然的!

有个语法糖是可以传递切片给可变参数函数,你必须在切片变量后面加省略号,这样,切片就会直接传递给函数,而不是再从新生成一个切片

在上面程序中,可以用find(89, nums...)代替find(89, nums),程序可以编译输出:

type of nums is []int
89 found at index 0 in [89 90 95]

下面是完整的代码引用:

package main

import (
	"fmt"
)

func find(num int, nums ...int) {
	fmt.Printf("type of nums is %T\n", nums)
	found := false
	for i,v := range nums {
		if v == num {
			fmt.Println(num, "found at index", i, "in", nums)
			found = true 
		}
	}
	if !found {
		fmt.Println(num, "not found in ", nums)
	}

	fmt.Printf("\n")
}

func main() {

	nums := []int{89, 90, 95}
	find(89, nums...)
}

Gotcha

当你修改函数内部的切片时,要确保你知道你在干嘛。

让我们看个例子:

package main

import (
	"fmt"
)

func change(s ...string) {
	s[0] = "Go"
}
func main() {

	welcome := []string{"hello", "world"}
	change(welcome...)
	fmt.Println(welcome)
}

你知道上面程序的输出嘛?如果你猜[Go world],那恭喜你,你已经理解可变参数函数和切片了。如果你猜错了,我来解释下为什么会有这个输出。

在上面程序中,我们使用了语法糖...,然后给change函数传递了切片。

正如我们之前讨论的,如果我们使用...,那么切片welcome就会把它自身传递给函数,而不是新增一个切片。

change函数内部,改变了切片的第一个元素,因此程序输出,

[Go world]

下面给多一个程序让你理解可变参数函数。

package main

import (
	"fmt"
)

func change(s ...string) {
	s[0] = "Go"

	s = append(s, "playground")
	fmt.Println(s)
}
func main() {

	welcome := []string{"hello", "world"}
	change(welcome...)
	fmt.Println(welcome)
}

上面程序的输出是什么?动手试下吧。

以上就是本篇的全部,感谢阅读,这是我第一次翻译,难免会有翻译不当的地方,如果有什么反馈和评论,欢迎提出来!

原文地址: https://golangbot.com/variadic-functions/