如何在 Go 中使用可变参数函数

介绍

可变参数函数 是可以接受零个、一个或多个值作为单个参数的函数。虽然可变参数函数并不常见,但它们能够使您的代码更清晰、更具可读性。

可变参数函数其实很常见。最常见的是fmt包中的 Println

  1. func Println(a ...interface{}) (n int, err error)

参数前面带有一组省略号 ( ... )的函数被视为可变参数函数。省略号表示提供的参数可以是零个、一个或多个。对于fmt.Println包,它声明参数a是可变参数。

让我们创建一个使用 fmt.Println 函数并传入零个、一个或多个值的程序:

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println()
  5. fmt.Println("one")
  6. fmt.Println("one", "two")
  7. fmt.Println("one", "two", "three")
  8. }

第一次调用 fmt.Println 时,我们不传递任何参数。第二次调用时,我们只传入一个参数,值为 one。 然后我们传递 onetwo,最后是 onetwothree 三个值。

让我们使用以下命令运行程序:

  1. $ go run print.go

我们将看到以下输出:

  1. Output
  2. one
  3. one two
  4. one two three

输出的第一行为空。这是因为我们在第一次调用 fmt.Println 时没有传递任何参数。第二次打印了 one 。然后打印 onetwo,最后打印 onetwothree

现在我们已经了解了如何调用可变参数函数,让我们看看如何定义自己的可变参数函数。

定义可变参数函数

我们可以通过在参数前面使用省略号 ( ... ) 来定义可变参数函数。让我们创建一个程序,当人们的名字被发送到函数时会进行问候:

  1. package main
  2. import "fmt"
  3. func main() {
  4. sayHello()
  5. sayHello("Sammy")
  6. sayHello("Sammy", "Jessica", "Drew", "Jamie")
  7. }
  8. func sayHello(names ...string) {
  9. for _, n := range names {
  10. fmt.Printf("Hello %s\n", n)
  11. }
  12. }

我们创建了一个 sayHello 函数,它只接受一个名为names,该参数是可变参数,因为我们在数据类型之前放置了一个省略号 (...): ...string。这告诉 Go 这个函数可以接受零个、一个或多个参数。

sayHello 函数将 names 参数作为 slice 接收。由于数据类型是 string,因此 names 可以将参数视为字符串切片 ( []string ) 。我们可以使用range运算符创建一个循环并遍历字符串切片。

如果我们运行程序,我们将得到以下输出:

  1. Output
  2. Hello Sammy
  3. Hello Sammy
  4. Hello Jessica
  5. Hello Drew
  6. Hello Jamie

请注意,我们第一次调用 sayHello 什么也没打印。这是因为可变参数是一个空的字符串切片。 由于我们会遍历切片,因此没有任何内容可以迭代,并且 fmt.Printf 永远不会被调用。

让我们修改程序以检测接收到值的情况:

  1. package main
  2. import "fmt"
  3. func main() {
  4. sayHello()
  5. sayHello("Sammy")
  6. sayHello("Sammy", "Jessica", "Drew", "Jamie")
  7. }
  8. func sayHello(names ...string) {
  9. if len(names) == 0 {
  10. fmt.Println("nobody to greet")
  11. return
  12. }
  13. for _, n := range names {
  14. fmt.Printf("Hello %s\n", n)
  15. }
  16. }

现在,通过使用if语句,如果没有传递任何值,则 names 长度将为 0,我们将打印出nobody to greet

  1. Output
  2. nobody to greet
  3. Hello Sammy
  4. Hello Sammy
  5. Hello Jessica
  6. Hello Drew
  7. Hello Jamie

使用可变参数可以使您的代码更具可读性。让我们创建一个函数,将单词通过指定的分隔符连接在一起。我们将首先创建没有可变参数函数的程序来展示它的读取方式:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var line string
  5. line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"})
  6. fmt.Println(line)
  7. line = join(",", []string{"Sammy", "Jessica"})
  8. fmt.Println(line)
  9. line = join(",", []string{"Sammy"})
  10. fmt.Println(line)
  11. }
  12. func join(del string, values []string) string {
  13. var line string
  14. for i, v := range values {
  15. line = line + v
  16. if i != len(values)-1 {
  17. line = line + del
  18. }
  19. }
  20. return line
  21. }

在这个程序中,我们将逗号 (,) 作为分隔符传递给 join 函数,再传递一个字符串切片。输出如下:

  1. Output
  2. Sammy,Jessica,Drew,Jamie
  3. Sammy,Jessica
  4. Sammy

因为该函数将字符串切片作为 values 参数,所以当我们调用 join 函数时,我们必须将所有单词包装在一个切片中。这会使代码难以阅读。

现在,让我们编写相同的函数,但我们将使用可变参数函数:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var line string
  5. line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
  6. fmt.Println(line)
  7. line = join(",", "Sammy", "Jessica")
  8. fmt.Println(line)
  9. line = join(",", "Sammy")
  10. fmt.Println(line)
  11. }
  12. func join(del string, values ...string) string {
  13. var line string
  14. for i, v := range values {
  15. line = line + v
  16. if i != len(values)-1 {
  17. line = line + del
  18. }
  19. }
  20. return line
  21. }

如果我们运行该程序,可以看到我们得到了与之前的程序相同的输出:

  1. Output
  2. Sammy,Jessica,Drew,Jamie
  3. Sammy,Jessica
  4. Sammy

虽然这两个版本的 join 函数执行完全相同的操作,但可变参数函数的版本在调用时更容易阅读。

可变参数顺序

一个函数中只能有一个可变参数,并且它必须是函数定义中的最后一个参数。将可变参数放在最后位置以外的任何顺序在函数定义会导致编译错误:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var line string
  5. line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
  6. fmt.Println(line)
  7. line = join(",", "Sammy", "Jessica")
  8. fmt.Println(line)
  9. line = join(",", "Sammy")
  10. fmt.Println(line)
  11. }
  12. func join(values ...string, del string) string {
  13. var line string
  14. for i, v := range values {
  15. line = line + v
  16. if i != len(values)-1 {
  17. line = line + del
  18. }
  19. }
  20. return line
  21. }

这次我们把 values 参数放在 join 函数的首位时,将导致以下编译错误:

  1. Output
  2. ./join_error.go:18:11: syntax error: cannot use ... with non-final parameter values

定义任何可变参数函数时,只有最后一个参数可以是可变参数。

分解参数

到目前为止,我们已经看到我们可以将零个、一个或多个值传递给可变参数函数。但是,有时我们有一个切片并且我们希望将它们发送到可变参数函数。

让我们用上一节中的 join 函数尝试一下,看看会发生什么:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var line string
  5. names := []string{"Sammy", "Jessica", "Drew", "Jamie"}
  6. line = join(",", names)
  7. fmt.Println(line)
  8. }
  9. func join(del string, values ...string) string {
  10. var line string
  11. for i, v := range values {
  12. line = line + v
  13. if i != len(values)-1 {
  14. line = line + del
  15. }
  16. }
  17. return line
  18. }

如果我们运行这个程序,我们会收到一个编译错误:

  1. Output
  2. ./join-error.go:10:14: cannot use names (type []string) as type string in argument to join

即使可变参数函数将 values ...string 参数转换为字符串切片 []string,我们也不能将字符串切片作为参数传递。这是因为编译器需要的是字符串的离散参数。

为了解决这个问题,我们可以通过在 切片 后面加上一组省略号 (...) 来分解切片,将其转换为离散参数,然后传递给可变参数函数:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var line string
  5. names := []string{"Sammy", "Jessica", "Drew", "Jamie"}
  6. line = join(",", names...)
  7. fmt.Println(line)
  8. }
  9. func join(del string, values ...string) string {
  10. var line string
  11. for i, v := range values {
  12. line = line + v
  13. if i != len(values)-1 {
  14. line = line + del
  15. }
  16. }
  17. return line
  18. }

这一次,当我们调用 join 函数时,我们通过附加省略号 (...)来分解 names 切片。

这样程序就会按照预期运行:

  1. Output
  2. Sammy,Jessica,Drew,Jamie

要注意,我们仍然可以传递零个、一个或多个参数,以及我们分解的切片。以下是我们迄今为止看到的所有变体的代码:

  1. package main
  2. import "fmt"
  3. func main() {
  4. var line string
  5. line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"}...)
  6. fmt.Println(line)
  7. line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
  8. fmt.Println(line)
  9. line = join(",", "Sammy", "Jessica")
  10. fmt.Println(line)
  11. line = join(",", "Sammy")
  12. fmt.Println(line)
  13. }
  14. func join(del string, values ...string) string {
  15. var line string
  16. for i, v := range values {
  17. line = line + v
  18. if i != len(values)-1 {
  19. line = line + del
  20. }
  21. }
  22. return line
  23. }
  1. Output
  2. Sammy,Jessica,Drew,Jamie
  3. Sammy,Jessica,Drew,Jamie
  4. Sammy,Jessica
  5. Sammy

现在,我们知道了如何将零个、一个或多个参数以及我们分解的切片传递给可变参数函数。

结论

在本教程中,我们了解了可变参数函数如何使您的代码更简洁。虽然您并不总是需要使用它们,但您可能会发现它们很有用:

  • 如果你创建了一个临时切片,只是为了传递给函数。
  • 当输入参数的数量未知或调用时会发生变化。
  • 使您的代码更具可读性。

要了解有关创建和调用函数的更多信息,您可以阅读如何在 Go 中定义和调用函数