Skip to content

Функции в Go

В Go функции являются первоклассными гражданами. Функции — это базовая составляющая Go и его ядро.

Объявление

Формат объявления функции:

go
func имя_функции([список_параметров]) [возвращаемое_значение] {
  тело_функции
}

Объявить функцию можно двумя способами: непосредственно с помощью ключевого слова func или через ключевое слово var:

go
func sum(a int, b int) int {
  return a + b
}

var sum = func(a int, b int) int {
  return a + b
}

Сигнатура функции состоит из имени функции, списка параметров и возвращаемого значения. Ниже приведён полный пример: функция называется Sum, имеет два параметра типа inta и b, возвращаемое значение типа int.

go
func Sum(a int, b int) int {
   return a + b
}

Очень важный момент: в Go функции не поддерживают перегрузку. Следующий код не скомпилируется:

go
type Person struct {
  Name    string
  Age     int
  Address string
  Salary  float64
}

func NewPerson(name string, age int, address string, salary float64) *Person {
  return &Person{Name: name, Age: age, Address: address, Salary: salary}
}

func NewPerson(name string) *Person {
  return &Person{Name: name}
}

Философия Go такова: если сигнатуры различаются — это две совершенно разные функции, поэтому им не следует давать одинаковые имена. Перегрузка функций может сделать код запутанным и трудным для понимания. Правильна ли эта философия — вопрос спорный, но по крайней мере в Go вы можете понять назначение функции только по её имени, не ища, какая именно это перегрузка.

Параметры

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

go
type ExWriter func(io.Writer) error

type Writer interface {
  ExWrite([]byte) (int, error)
}

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

go
func Log(format string, a1, a2 any) {
  ...
}

Параметры переменной длины могут принимать ноль или более значений и должны быть объявлены в конце списка параметров. Наиболее типичный пример — функция fmt.Printf:

go
func Printf(format string, a ...any) (n int, err error) {
  return Fprintf(os.Stdout, format, a...)
}

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

Возвращаемые значения

Ниже приведён простой пример возвращаемого значения функции. Функция Sum возвращает значение типа int:

go
func Sum(a, b int) int {
   return a + b
}

Когда функция не возвращает значение, не требуется void — просто не указывайте возвращаемое значение:

go
func ErrPrintf(format string, a ...any) {
  _, _ = fmt.Fprintf(os.Stderr, format, a...)
}

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

go
func Div(a, b float64) (float64, error) {
  if a == 0 {
    return math.NaN(), errors.New("0 не может быть делителем")
  }
  return a / b, nil
}

Go также поддерживает именованные возвращаемые значения, которые не должны совпадать с именами параметров. При использовании именованных возвращаемых значений ключевое слово return может не указывать значения:

go
func Sum(a, b int) (ans int) {
  ans = a + b
  return
}

Как и с параметрами, при нескольких именованных возвращаемых значениях одного типа можно опустить повторяющиеся объявления типов:

go
func SumAndMul(a, b int) (c, d int) {
  c = a + b
  d = a * b
  return
}

Независимо от того, как объявлены именованные возвращаемые значения, приоритет всегда у значений после ключевого слова return:

go
func SumAndMul(a, b int) (c, d int) {
  c = a + b
  d = a * b
    // c, d не будут возвращены
  return a + b, a * b
}

Анонимные функции

Анонимная функция — это функция без имени. Например, функция func(a, b int) int не имеет имени, поэтому мы можем вызвать её, сразу добавив скобки после тела функции:

go
func main() {
   func(a, b int) int {
      return a + b
   }(1, 2)
}

При вызове функции, когда её параметр является типом функции, имя больше не важно — можно напрямую передать анонимную функцию:

go
type Person struct {
  Name   string
  Age    int
  Salary float64
}

func main() {
  people := []Person{
    {Name: "Alice", Age: 25, Salary: 5000.0},
    {Name: "Bob", Age: 30, Salary: 6000.0},
    {Name: "Charlie", Age: 28, Salary: 5500.0},
  }

  slices.SortFunc(people, func(p1 Person, p2 Person) int {
    if p1.Name > p2.Name {
      return 1
    } else if p1.Name < p2.Name {
      return -1
    }
    return 0
  })
}

Это пример пользовательской сортировки. slices.SortFunc принимает два параметра: срез и функцию сравнения. Если не требуется повторное использование, можно напрямую передать анонимную функцию.

Замыкания

Замыкание (Closure) — концепция, известная в некоторых языках как лямбда-выражение, используется вместе с анонимными функциями. Замыкание = функция + ссылки на окружение. Пример:

go
func main() {
  grow := Exp(2)
  for i := range 10 {
    fmt.Printf("2^%d=%d\n", i, grow())
  }
}

func Exp(n int) func() int {
  e := 1
  return func() int {
    temp := e
    e *= n
    return temp
  }
}

Вывод:

2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512

Функция Exp возвращает функцию, которая здесь называется grow. При каждом вызове переменная e экспоненциально увеличивается. Функция grow ссылается на две переменные функции Exp: e и n. Они созданы в области видимости функции Exp. В обычных условиях после завершения вызова Exp память этих переменных освободилась бы при выходе из стека. Однако поскольку grow ссылается на них, они не освобождаются, а «сбегают» в кучу. Даже после завершения жизненного цикла функции Exp, жизненный цикл переменных e и n не заканчивается — их можно изменять внутри grow. Функция grow является замыканием.

Используя замыкания, можно очень просто реализовать функцию для вычисления чисел Фибоначчи:

go
func main() {
    // 10 чисел Фибоначчи
  fib := Fib(10)
  for n, next := fib(); next; n, next = fib() {
    fmt.Println(n)
  }
}

func Fib(n int) func() (int, bool) {
  a, b, c := 1, 1, 2
  i := 0
  return func() (int, bool) {
    if i >= n {
      return 0, false
    } else if i < 2 {
      f := i
      i++
      return f, true
    }

    a, b = b, c
    c = a + b
    i++

    return a, true
  }
}

Вывод:

0
1
1
2
3
5
8
13
21
34

Отложенный вызов

Ключевое слово defer позволяет отложить вызов функции на некоторое время. Перед возвратом из функции все отложенные вызовы будут выполнены по очереди. Пример:

go
func main() {
  Do()
}

func Do() {
  defer func() {
    fmt.Println("1")
  }()
  fmt.Println("2")
}

Вывод:

2
1

Поскольку defer выполняется перед возвратом из функции, вы можете изменить возвращаемое значение в defer:

go
func main() {
  fmt.Println(sum(3, 5))
}

func sum(a, b int) (s int) {
  defer func() {
    s -= 10
  }()
  s = a + b
  return
}

Когда имеется несколько отложенных вызовов, они выполняются в порядке LIFO (последний пришёл — первый ушёл), как стек:

go
func main() {
  fmt.Println(0)
  Do()
}

func Do() {
  defer fmt.Println(1)
  fmt.Println(2)
  defer fmt.Println(3)
  defer fmt.Println(4)
  fmt.Println(5)
}
0
2
5
4
3
1

Отложенные вызовы обычно используются для освобождения файловых ресурсов, закрытия сетевых подключений и т.д. Ещё одно применение — перехват panic, но это будет рассмотрено в разделе об обработке ошибок.

Циклы

Хотя явного запрета нет, обычно не рекомендуется использовать defer в циклах for:

go
func main() {
  n := 5
  for i := range n {
    defer fmt.Println(i)
  }
}

Вывод:

4
3
2
1
0

Результат этого кода корректен, но процесс, возможно, неправилен. В Go при каждом создании defer выделяется память в текущей горутине. Если в приведённом примере это не просто цикл for n, а сложный процесс обработки данных, то при резком увеличении количества внешних запросов在短时间内 будет создано много defer. При большом или неопределённом количестве итераций цикла это может привести к резкому росту потребления памяти — так называемой утечке памяти.

Предварительное вычисление параметров

Для отложенных вызовов есть некоторые неочевидные детали. Например:

go
func main() {
  defer fmt.Println(Fn1())
  fmt.Println("3")
}

func Fn1() int {
  fmt.Println("2")
  return 1
}

Эта ловушка довольно скрытна. Автор сам однажды долго не мог понять причину. Попробуйте угадать вывод. Ответ:

2
3
1

Многие думают, что вывод будет таким:

3
2
1

По замыслу программиста, fmt.Println(Fn1()) должно выполняться после завершения тела функции. fmt.Println действительно выполняется последним, но Fn1() — неожиданно. Следующий пример ещё более нагляден:

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer fmt.Println(sum(a, b))
  a = 3
  b = 4
}

func sum(a, b int) int {
  return a + b
}

Вывод обязательно будет 3, а не 7. Если использовать замыкание вместо отложенного вызова, результат изменится:

go
func main() {
  var a, b int
  a = 1
  b = 2
  f := func() {
    fmt.Println(sum(a, b))
  }
  a = 3
  b = 4
  f()
}

Вывод замыкания — 7. А если совместить отложенный вызов и замыкание?

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer func() {
    fmt.Println(sum(a, b))
  }()
  a = 3
  b = 4
}

Теперь всё нормально, вывод — 7. Изменим ещё раз, без замыкания:

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer func(num int) {
    fmt.Println(num)
  }(sum(a, b))
  a = 3
  b = 4
}

Вывод снова стал 3. Сравнивая примеры выше, можно увидеть, что этот код:

defer fmt.Println(sum(a,b))

эквивалентен:

defer fmt.Println(3)

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

Golang by www.golangdev.cn edit