Вариативные функции в Go

Вариативная функция – это функция, которая в качестве единого аргумента принимает ноль, одно или несколько значений. Хотя вариативные функции используются не так часто, в отдельных случаях они могут сделать ваш код чище и читабельнее.

Все же, вариативные функции встречаются чаще, чем кажется на первый взгляд. Наиболее распространенной является функция Println из пакета fmt.

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

Функция с параметром, которому предшествует многоточие (…), считается вариативной функцией. Многоточие означает, что предоставленный параметр может принимать ноль, одно или несколько значений. В пакете fmt.Println указывается, что параметр a является вариативным.

Читайте также: Определение и вызов функций в Go

Давайте создадим программу, которая использует функцию fmt.Println и передает ей ноль, одно или несколько значений:

package main
import "fmt"
func main() {
fmt.Println()
fmt.Println("one")
fmt.Println("one", "two")
fmt.Println("one", "two", "three")
}

При первом вызове fmt.Println мы не передаем никаких аргументов. Во второй раз, когда мы вызываем fmt.Println, мы передаем только один аргумент со значением one. Затем мы передаем one и two, а в конце one, two и three.

Давайте запустим программу с помощью следующей команды:

go run print.go

Мы увидим следующий вывод:

one
one two
one two three

Первая строка вывода пуста. Это потому, что мы не передавали никаких аргументов при первом вызове fmt.Println. Во второй раз отображается значение one. Затем идут one и two, а в последней строке one, two и three.

Теперь, когда вы знаете, как вызывать вариативную функцию, давайте посмотрим, как можно ее определить.

Определение вариативной функции

Мы можем определить вариативную функцию, используя многоточие (…) перед аргументом. Давайте создадим программу, которая приветствует пользователей, когда их имена отправляются в функцию:

package main
import "fmt"
func main() {
sayHello()
sayHello("Sammy")
sayHello("Sammy", "Jessica", "Drew", "Jamie")
}
func sayHello(names ...string) {
for _, n := range names {
fmt.Printf("Hello %s\n", n)
}
}

Мы создали функцию sayHello, которая принимает только один параметр, names. Параметр является вариативным, так как перед типом данных идет многоточие: …string. Так Go понимает, что функция может принимать ноль, один или несколько аргументов.

Функция sayHello получает параметр names в виде среза. Поскольку тип данных — string, параметр names можно рассматривать как слайс строк ([]string) внутри тела функции. Мы можем создать цикл с оператором range и перебрать этот срез.

Если мы запустим программу, мы получим следующий вывод:

Hello Sammy
Hello Sammy
Hello Jessica
Hello Drew
Hello Jamie

При первом вызове sayHello ничего не выводит. Это потому, что вариативный параметр был пустым срезом строк. Поскольку мы перебираем срез, а перебирать нечего, fmt.Printf не вызывается.

Давайте отредактируем программу, чтобы отобразить случаи, когда никакие значения не отправлялись:

package main
import "fmt"
func main() {
sayHello()
sayHello("Sammy")
sayHello("Sammy", "Jessica", "Drew", "Jamie")
}
func sayHello(names ...string) {
if len(names) == 0 {
fmt.Println("nobody to greet")
return
}
for _, n := range names {
fmt.Printf("Hello %s\n", n)
}
}

Теперь программа использует оператор if: если пользователь не передал имя, длина имени будет равна 0, и мы получим такую строку:

nobody to greet
Hello Sammy
Hello Sammy
Hello Jessica
Hello Drew
Hello Jamie

Вариативный параметр может сделать ваш код более читабельным. Давайте создадим функцию, которая объединяет слова вместе с указанным разделителем. Но сначала создадим эту программу без вариативной функции, чтобы показать, как она будет выглядеть:

package main
import "fmt"
func main() {
var line string
line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"})
fmt.Println(line)
line = join(",", []string{"Sammy", "Jessica"})
fmt.Println(line)
line = join(",", []string{"Sammy"})
fmt.Println(line)
}
func join(del string, values []string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}

В этой программе мы передаем функции join запятую (,) в качестве разделителя. Затем мы передаем срез значений. Вот таким будет вывод:

Sammy,Jessica,Drew,Jamie
Sammy,Jessica
Sammy

Поскольку функция принимает в качестве параметра values срез строк, нам пришлось обернуть все наши слова в срезе при вызове join. Это может затруднить чтение кода.

Теперь давайте напишем ту же функцию, но сделаем ее вариативной:

package main
import "fmt"
func main() {
var line string
line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
fmt.Println(line)
line = join(",", "Sammy", "Jessica")
fmt.Println(line)
line = join(",", "Sammy")
fmt.Println(line)
}
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}

Если мы запустим программу, мы увидим, что мы получаем тот же результат, что и в предыдущий раз:

Sammy,Jessica,Drew,Jamie
Sammy,Jessica
Sammy

То есть обе версии функции join  выполняют одну и ту же задачу, но вариативная версия функции намного легче читается.

Порядок вариативных аргументов

В функции может быть только один вариативный параметр – он обязательно должен быть последним параметром, определенным в функции. Определение параметров в вариативной функции без учета порядка приведет к ошибке компиляции:

package main
import "fmt"
func main() {
var line string
line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
fmt.Println(line)
line = join(",", "Sammy", "Jessica")
fmt.Println(line)
line = join(",", "Sammy")
fmt.Println(line)
}
func join(values ...string, del string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}

На этот раз параметр values идет первым в функции join. Это приведет к следующей ошибке компиляции:

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

В любой вариативной функции только последний параметр может быть вариативным.

Взрывающиеся аргументы

До сих пор мы передавали ноль, одно или несколько значений в вариативную функцию. Однако бывают случаи, когда в вариативную функцию нужно отправить срез значений.

Давайте посмотрим на нашу функцию join из предыдущего раздела:

package main
import "fmt"
func main() {
var line string
names := []string{"Sammy", "Jessica", "Drew", "Jamie"}
line = join(",", names)
fmt.Println(line)
}
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}

Если мы запустим эту программу, мы получим ошибку компиляции:

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

Несмотря на то, что вариативная функция преобразует параметр values …string в срез строк []string, мы не можем передать срез строк в качестве аргумента. Это происходит потому, что компилятор ожидает дискретных аргументов строк.

Чтобы обойти это, мы можем «взорвать» срез, добавив многоточие (…) и превратив его в дискретные аргументы, которые будут переданы в вариативную функцию.

package main
import "fmt"
func main() {
var line string
names := []string{"Sammy", "Jessica", "Drew", "Jamie"}
line = join(",", names...)
fmt.Println(line)
}
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}

На этот раз, когда мы вызвали функцию join , мы «взорвали» срез names, добавив многоточие (…).

Это позволяет программе работать правильно:

Sammy,Jessica,Drew,Jamie

Важно отметить: вы все равно можете передать ноль, один или несколько аргументов, а также вызываемый срез. Этот код передает все варианты, которые мы рассматривали:

package main
import "fmt"
func main() {
var line string
line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"}...)
fmt.Println(line)
line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
fmt.Println(line)
line = join(",", "Sammy", "Jessica")
fmt.Println(line)
line = join(",", "Sammy")
fmt.Println(line)
}
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}
Sammy,Jessica,Drew,Jamie
Sammy,Jessica,Drew,Jamie
Sammy,Jessica
Sammy

Теперь вы знаете, как передать в вариативную функцию ноль, один или несколько аргументов, а также «взорванный» срез.

Заключение

В этом мануале вы увидели, как сделать код чище с помощью вариативной функции. Их  нужно использовать не всегда, но в отдельных ситуациях они будут особенно полезными:

  • Когда вы создаете временный срез просто для передачи функции.
  • Когда количество входных параметров при вызове неизвестно или будет меняться.
  • Когда нужно сделать код более читабельным.

Чтобы узнать больше о создании и вызове функций, вы можете прочитать мануал Определение и вызов функций в Go.

Tags: ,