Циклы и функции

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

Содержание курса

Пакетная и локальная область видимости: затенение переменных

Посмотрите на такой код:

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; внутри функции := удобен, но требует внимания, когда имя совпадает с чем-то из внешней области — тогда вместо изменения существующей переменной вы получите её локальную копию.