Урок курса
Объявление функций, параметры, несколько возвращаемых значений и область видимости
Golang с нуляВ предыдущем уроке вы управляли потоком выполнения через циклы: повторяли блок кода, пока выполняется условие, и досрочно выходили через break. Теперь пора научиться выносить логику в отдельные именованные блоки — функции. Это принципиальный шаг: код перестаёт быть одним длинным main, а отдельные задачи получают собственное имя и границы.
Синтаксис объявления функции: параметры и возвращаемый тип
Функция в Go объявляется ключевым словом func, за ним — имя, список параметров в круглых скобках и, если функция что-то возвращает, тип результата:
func add(a int, b int) int {
return a + b
}
Здесь a и b — параметры типа int, а int после закрывающей скобки — тип возвращаемого значения. Когда выполнение доходит до return a + b, функция немедленно завершается и передаёт вычисленное значение обратно туда, откуда её вызвали.
Если несколько параметров имеют одинаковый тип, их можно объединить — тип пишется один раз в конце группы:
func add(a, b int) int {
return a + b
}
Оба варианта полностью эквивалентны; сокращённая форма просто убирает дублирование.
Вызов функции выглядит предсказуемо:
result := add(3, 5)
fmt.Println(result) // 8
add(3, 5) — это выражение, которое вычисляется в 8, и это значение присваивается result.
Если функции нечего возвращать — например, она только печатает что-то — тип после скобок просто не указывается:
func greet(name string) {
fmt.Println("Hello,", name)
}
В таком случае return можно писать без аргумента или вовсе опустить — функция завершится, когда выполнение дойдёт до закрывающей фигурной скобки.
Один важный момент про return: он останавливает выполнение функции в той точке, где стоит. Весь код ниже return в том же блоке уже не выполнится. Именно это свойство мы используем чуть позже, когда нужно выйти из функции по условию раньше времени.
Несколько возвращаемых значений и пустой идентификатор _
Go позволяет функции возвращать сразу несколько значений. Типы перечисляются в скобках после списка параметров:
func divide(a, b float64) (float64, bool) {
if b == 0 {
return 0, false
}
return a / b, true
}
Функция возвращает два значения: частное и признак того, что деление прошло успешно. Если делитель равен нулю — возвращаем 0 и false, не пытаясь делить. Никаких исключений — второе значение просто сигнализирует о проблеме.
При вызове оба значения принимаются через множественное присваивание:
result, ok := divide(10, 3)
if ok {
fmt.Println(result) // 3.3333333333333335
} else {
fmt.Println("деление на ноль")
}
Переменная ok по соглашению используется в Go для bool-признака успеха — это устойчивый паттерн, который вы будете встречать постоянно.
Если результаты функции нужно принять в переменные, число позиций слева от := должно совпадать с числом возвращаемых значений. Когда одно из значений не нужно, его позицию занимает _ — пустой идентификатор:
result, _ := divide(10, 3) // принимаем только частное
fmt.Println(result)
_ не создаёт переменную и не требует дальнейшего использования — компилятор просто игнорирует попавшее туда значение в контексте присваивания.
Обратная ситуация — нужен только признак успеха:
_, ok := divide(10, 0)
if !ok {
fmt.Println("деление невозможно")
}
Есть и ещё один вариант: если результаты функции вообще не нужны, вызов можно написать как самостоятельную инструкцию — тогда все возвращаемые значения молча отбрасываются без какого-либо _:
divide(10, 3) // результат никуда не идёт — допустимо
Правило про _ касается именно присваивания: если вы пишете x, y := f(), позиций слева должно быть ровно столько, сколько значений возвращает f. Пропустить позицию нельзя — только заменить её на _.
Пакетная и локальная область видимости: затенение переменных
Посмотрите на такой код:
package main
import "fmt"
var counter int = 10
func reset() {
counter := 0 // опасно!
fmt.Println("внутри reset:", counter) // 0
}
func main() {
reset()
fmt.Println("в main:", counter) // 10, а не 0
}
Переменная counter объявлена на уровне пакета через var — это пакетная переменная, доступная из всех функций в том же пакете. Но внутри reset стоит :=, и компилятор воспринимает это как объявление новой локальной переменной с тем же именем — потому что пакетная counter находится во внешней области видимости, а не в том же блоке. Обращения к counter в теле функции идут к локальной копии. После выхода из reset локальная переменная исчезает, а пакетная остаётся нетронутой.
Код компилируется без единого предупреждения — это и делает затенение коварным.
Исправление простое: чтобы изменить пакетную переменную, используйте присваивание =, а не :=:
func reset() {
counter = 0 // присваивание, не объявление
fmt.Println("внутри reset:", counter) // 0
}
Теперь reset действительно меняет counter, и main напечатает 0.
Точнее о поведении :=. В примере выше := создаёт новую переменную, потому что counter из другой (более широкой) области. Есть, однако, случай, когда := повторно использует уже объявленную переменную из того же блока: групповое присваивание вида value, err := .... Если err уже объявлена в том же блоке, а value — новая, компилятор переиспользует существующую err и объявит только value. Это не затенение — имя остаётся тем же объектом. Затенение возникает именно тогда, когда := применяется к имени из внешней области.
Общее правило: на уровне пакета объявление всегда через var; внутри функции := удобен, но требует внимания, когда имя совпадает с чем-то из внешней области — тогда вместо изменения существующей переменной вы получите её локальную копию.
Попробуйте решить
Что выведет следующая программа?
package main
import "fmt"
func double(n int) int {
return n * 2
}
func main() {
x := double(5)
fmt.Println(x)
}
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
