Функции в Go
В Go функции являются первоклассными гражданами. Функции — это базовая составляющая Go и его ядро.
Объявление
Формат объявления функции:
func имя_функции([список_параметров]) [возвращаемое_значение] {
тело_функции
}Объявить функцию можно двумя способами: непосредственно с помощью ключевого слова func или через ключевое слово var:
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}Сигнатура функции состоит из имени функции, списка параметров и возвращаемого значения. Ниже приведён полный пример: функция называется Sum, имеет два параметра типа int — a и b, возвращаемое значение типа int.
func Sum(a int, b int) int {
return a + b
}Очень важный момент: в 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 могут быть безымянными, обычно это используется в объявлениях интерфейсов или типов функций. Однако для читаемости рекомендуется давать параметрам имена:
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}Для параметров одного типа можно указать тип один раз, при условии, что они находятся рядом:
func Log(format string, a1, a2 any) {
...
}Параметры переменной длины могут принимать ноль или более значений и должны быть объявлены в конце списка параметров. Наиболее типичный пример — функция fmt.Printf:
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}Стоит отметить, что в Go параметры функций передаются по значению, то есть при передаче параметра копируется его значение. Если вы беспокоитесь, что при передаче среза или отображения будет скопировано много памяти, могу заверить: этого не произойдёт, поскольку эти структуры данных по сути являются указателями.
Возвращаемые значения
Ниже приведён простой пример возвращаемого значения функции. Функция Sum возвращает значение типа int:
func Sum(a, b int) int {
return a + b
}Когда функция не возвращает значение, не требуется void — просто не указывайте возвращаемое значение:
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go позволяет функциям возвращать несколько значений. В этом случае необходимо заключить возвращаемые значения в скобки:
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 не может быть делителем")
}
return a / b, nil
}Go также поддерживает именованные возвращаемые значения, которые не должны совпадать с именами параметров. При использовании именованных возвращаемых значений ключевое слово return может не указывать значения:
func Sum(a, b int) (ans int) {
ans = a + b
return
}Как и с параметрами, при нескольких именованных возвращаемых значениях одного типа можно опустить повторяющиеся объявления типов:
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}Независимо от того, как объявлены именованные возвращаемые значения, приоритет всегда у значений после ключевого слова return:
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 не имеет имени, поэтому мы можем вызвать её, сразу добавив скобки после тела функции:
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}При вызове функции, когда её параметр является типом функции, имя больше не важно — можно напрямую передать анонимную функцию:
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) — концепция, известная в некоторых языках как лямбда-выражение, используется вместе с анонимными функциями. Замыкание = функция + ссылки на окружение. Пример:
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 является замыканием.
Используя замыкания, можно очень просто реализовать функцию для вычисления чисел Фибоначчи:
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 позволяет отложить вызов функции на некоторое время. Перед возвратом из функции все отложенные вызовы будут выполнены по очереди. Пример:
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}Вывод:
2
1Поскольку defer выполняется перед возвратом из функции, вы можете изменить возвращаемое значение в defer:
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}Когда имеется несколько отложенных вызовов, они выполняются в порядке LIFO (последний пришёл — первый ушёл), как стек:
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:
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}Вывод:
4
3
2
1
0Результат этого кода корректен, но процесс, возможно, неправилен. В Go при каждом создании defer выделяется память в текущей горутине. Если в приведённом примере это не просто цикл for n, а сложный процесс обработки данных, то при резком увеличении количества внешних запросов在短时间内 будет создано много defer. При большом или неопределённом количестве итераций цикла это может привести к резкому росту потребления памяти — так называемой утечке памяти.
Предварительное вычисление параметров
Для отложенных вызовов есть некоторые неочевидные детали. Например:
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() — неожиданно. Следующий пример ещё более нагляден:
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. Если использовать замыкание вместо отложенного вызова, результат изменится:
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}Вывод замыкания — 7. А если совместить отложенный вызов и замыкание?
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}Теперь всё нормально, вывод — 7. Изменим ещё раз, без замыкания:
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, её параметры вычисляются заранее. Это и приводит к странному явлению в первом примере. На эту ситуацию следует обращать особое внимание, особенно когда возвращаемое значение функции передаётся как аргумент в отложенном вызове.
