Перейти к содержимому

Переменные

Всё, что нужно для старта, — готово. В предыдущем блоке мы настроили окружение, написали первый «Hello World», разобрали структуру программы и даже заглянули под капот компилятора, узнав, как исходный код превращается в машинный. Дальше начинается настоящее программирование.

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

Помните, в уроке про структуру программы мы видели конструкцию x := 5? Или var version = "dev" в уроке про компиляцию, когда обсуждали флаги сборки? Тогда мы использовали их интуитивно. Вы уже знаете, что переменная — это способ сохранить данные. Но в Go работа с переменными имеет свои уникальные, строгие правила, которые были заложены Робом Пайком, Кеном Томпсоном и Робертом Гризмером для того, чтобы сделать код максимально читаемым и безопасным.

В этой статье мы формально разберём, чем отличаются var и :=, почему в Go нет «неопределённого поведения» при инициализации, что будет, если объявить переменную и забыть про неё, и почему компилятор так строго бьёт по рукам за неиспользуемый код.


Что такое переменная в философии Go

Прежде чем перейдём к синтаксису, давайте договоримся о терминах. Представьте себе склад. Переменная — это подписанная коробка на этом складе, в которую вы кладёте значение определённого типа. У коробки есть имя (идентификатор), форма и размер (тип данных — целое число, строка и т.д.) и, собственно, само содержимое (значение).

В динамических языках, таких как Python или JavaScript, вы можете взять коробку с надписью x, положить туда число 5, а через минуту выкинуть число и положить строку "hello". Языку всё равно — коробка резиновая.

Go — язык статически типизированный. Если вы создали коробку для целого числа (int), вы никогда не сможете положить туда строку. Компилятор на этапе проверки типов (Type Checker, о котором мы говорили в уроке 1.6) просто остановит сборку программы. Это сделано намеренно: строгая типизация исключает целый класс ошибок, когда программа в рантайме внезапно падает из-за того, что ожидала число, а получила текст.

Более того, синтаксис объявления переменных в Go отличается от классических С-подобных языков. В C или Java сначала пишется тип, а потом имя: int x = 5;. В Go создатели пошли по пути естественного чтения слева направо: var x int = 5. Вы буквально читаете: «переменная (var) с именем x типа int равна 5». Небольшое, но важное изменение, которое делает код более выразительным.


Полный синтаксис: объявление через var

Ключевое слово var — самый полный и явный способ объявить переменную в Go. Этот способ доступен как внутри функций (локальные переменные), так и за их пределами (переменные уровня пакета).

Давайте посмотрим на полный синтаксис, который описывает спецификация Go:

package main
import "fmt"
func main() {
// 1. Полное объявление с указанием типа и значения
var age int = 30
// 2. Объявление с выводом типа (type inference)
var name = "Gopher"
// 3. Объявление без инициализации (нулевое значение)
var isActive bool
fmt.Printf("Имя: %s, Возраст: %d, Активен: %t\n", name, age, isActive)
}
Имя: Gopher, Возраст: 30, Активен: false

Давайте разберём каждый вариант.

1. Явное указание типа и значения (var name type = value)

Вы указываете всё: и то, что это переменная, и её имя, и её тип, и начальное значение. Это самый многословный вариант. В идиоматичном Go* так пишут редко — только тогда, когда тип значения справа не совпадает с тем типом, который вы хотите задать переменной.

Например, число 42 по умолчанию станет типом int. Но если вы пишете системную утилиту и вам нужно ровно 8 бит памяти, вы напишете:

var smallNumber int8 = 42

Без явного указания типа int8 компилятор не догадается о ваших намерениях.

* Идиоматичный — «так принято в сообществе». Вы уже встречали это слово в уроке про компиляцию.

2. Вывод типа (var name = value)

Если вы сразу присваиваете значение, Go достаточно умён, чтобы самому догадаться, какой это тип. Вы пишете var name = "Gopher", и компилятор видит двойные кавычки — он понимает: это строка (string). Вам не нужно писать var name string = "Gopher". Вывод типа — отличный способ сократить визуальный шум в коде.

3. Объявление без инициализации (var name type)

Если вы создаёте переменную, но пока не знаете, что в ней будет лежать, вы просто указываете её тип: var result int. В этот момент в Go происходит магия, которая отличает его от многих других языков — переменная автоматически получает нулевое значение. Но об этом чуть позже в отдельном разделе — это киллер-фича языка.

Группировка переменных var (...)

Часто бывает нужно объявить сразу несколько переменных. Писать var на каждой строке утомительно. Go позволяет группировать объявления в блоки:

package main
import "fmt"
// Группировка на уровне пакета
var (
appName string = "MyApp"
appVersion string = "1.0.0"
maxRetries int = 3
)
func main() {
fmt.Printf("Запуск %s v%s (попыток: %d)\n", appName, appVersion, maxRetries)
}
Запуск MyApp v1.0.0 (попыток: 3)

:::tip Лайфхак В официальном стайлгайде Uber по Go прямо сказано: всегда группируйте схожие объявления. Если у вас есть несколько связанных переменных вне функций (их называют переменными уровня пакета — то есть объявленных прямо в файле, как appName в примере выше, а не внутри func), объединяйте их в один блок var (...). Это делает код чище и показывает логическую связь между переменными. :::


Короткое объявление: магия :=

В повседневной разработке на Go вы будете видеть var не так уж часто. Практически весь локальный код пишется с использованием короткого объявления переменных (short variable declaration) — оператора :=.

Синтаксис: name := value.

package main
import "fmt"
func main() {
// Короткое объявление. Тип выводится автоматически.
message := "Привет, Gopher!"
count := 42
price := 19.99
fmt.Printf("%s Количество: %d, Цена: %.2f\n", message, count, price)
}
Привет, Gopher! Количество: 42, Цена: 19.99

Оператор := делает три вещи одновременно:

  1. Объявляет новую переменную
  2. Автоматически выводит её тип на основе значения справа
  3. Присваивает ей это значение

Невероятно удобно и лаконично. Но у := есть строгие правила и ограничения.

Ограничение 1: Только внутри функций

Короткое объявление запрещено использовать на уровне пакета (вне функций):

package main
// ОШИБКА КОМПИЛЯЦИИ: syntax error: non-declaration statement outside function body
version := "1.0.0"
func main() {
}

Почему так? В уроке 1.5 мы обсуждали структуру программы. Спецификация Go требует, чтобы любое выражение на уровне пакета начиналось с ключевого слова: package, import, func, var, const или type. Оператор := является statement (исполняемой инструкцией), а на уровне пакета допускаются только declarations (объявления). Поэтому вне функций обязаны писать var version = "1.0.0".

Ограничение 2: Минимум одна новая переменная

Оператор := — это именно оператор объявления, а не простого присваивания. Слева от него должна быть хотя бы одна новая переменная, которая ещё не существует в текущей области видимости (scope — то есть внутри тех фигурных скобок {}, где вы сейчас находитесь; подробно разберём в уроке 2.1.6):

package main
import "fmt"
func main() {
x := 10
fmt.Println(x)
// ОШИБКА КОМПИЛЯЦИИ: no new variables on left side of :=
// x := 20
// Правильно (простое присваивание):
x = 20
fmt.Println(x)
}

Но здесь кроется гениальное исключение, которое делает обработку ошибок в Go такой элегантной.

:::tip Ловушка переобъявления (Redeclaration) Если вы объявляете сразу несколько переменных через :=, компилятор пропустит код, даже если некоторые из этих переменных уже существуют. Главное правило — слева должна быть хотя бы одна новая переменная. Старым переменным просто присваивается новое значение. :::

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

package main
import (
"fmt"
"os"
)
func main() {
// os.Open возвращает два значения: файл и ошибку
file1, err := os.Open("file1.txt")
if err != nil {
fmt.Println("Ошибка 1:", err)
}
// ВНИМАНИЕ:
// Мы снова используем :=, хотя переменная err УЖЕ существует!
// Это работает, потому что file2 — НОВАЯ переменная.
file2, err := os.Open("file2.txt")
if err != nil {
fmt.Println("Ошибка 2:", err)
}
// Чистим за собой (об этом в будущих уроках)
if file1 != nil { file1.Close() }
if file2 != nil { file2.Close() }
}

Если бы Go заставлял придумывать новые имена для ошибок (err1, err2, err3), код превратился бы в ад. Разрешение переиспользовать уже существующую переменную err при условии появления новой переменной (например, file2) — это прагматичный компромисс от создателей языка. Effective Go называет это «pure pragmatism, making it easy to use a single err value».


Нулевые значения (Zero Values): гарантия безопасности

Вот мы и добрались до концепции, которая кардинально отличает Go от таких языков, как C или C++.

Когда сам начинал писать на C, одной из самых страшных ошибок была неинициализированная переменная. Если в C вы пишете int x; и забываете присвоить значение, в памяти переменной будет лежать «мусор» — случайные биты, которые остались от предыдущих программ. Это приводило к катастрофическим, плавающим багам, которые невозможно отловить.

В Java пошли чуть дальше: объекты по умолчанию равны null, что привело к миллиардам ошибок NullPointerException.

Роб Пайк и команда создателей Go решили эту проблему радикально: в Go не бывает неинициализированных переменных. Как только вы создаёте переменную через var x type, Go автоматически очищает память и записывает туда дефолтное, «нулевое» значение для данного типа.

Таблица нулевых значений

Тип данныхНулевое значениеСмысл
int, int64, float64 и др. числа0 (или 0.0)Обычный математический ноль
boolfalseЛожь по умолчанию
string""Пустая строка (без пробелов)
pointer (*int), func, interfacenilОтсутствие адреса или реализации
slice, map, channelnilПустая неинициализированная коллекция
array (например, [3]int)[0, 0, 0]Массив, заполненный нулевыми значениями
structВсе поля — нулиКаждое поле сброшено в свой ноль

Проверим на практике:

package main
import "fmt"
func main() {
var defaultInt int
var defaultBool bool
var defaultString string
var defaultPointer *int
// %q используем для строк, чтобы увидеть кавычки пустой строки
fmt.Printf("int: %d\n", defaultInt)
fmt.Printf("bool: %t\n", defaultBool)
fmt.Printf("string: %q\n", defaultString)
fmt.Printf("pointer: %v\n", defaultPointer)
}
int: 0
bool: false
string: ""
pointer: <nil>

Философия «Make the zero value useful»

В идиоматичном Go есть золотое правило: «Сделай нулевое значение полезным». Это один из знаменитых Go Proverbs Роба Пайка. Поскольку мы точно знаем, что структура будет инициализирована нулями, стандартная библиотека Go спроектирована так, чтобы эти нули уже имели смысл.

Например, в Go есть примитив синхронизации sync.Mutex. Вам не нужно вызывать конструкторы или инициализаторы. Вы просто пишете var mu sync.Mutex, и переменная уже готова к использованию — её нулевое внутреннее состояние означает «мьютекс разблокирован». Аналогично bytes.Buffer — можно сразу вызвать buf.WriteString("hello") без какой-либо инициализации. Это делает код невероятно надёжным и предсказуемым.

nil — это валидный срез

Отдельно стоит сказать про срезы (slices — динамические массивы, которые мы подробно разберём позже). Переменная, объявленная как var items []string, получает значение nil. Но этот nil-срез уже готов к работе — к нему можно добавлять элементы через append, не вызывая make():

package main
import "fmt"
func main() {
var names []string // nil-срез, но уже рабочий
names = append(names, "Alice")
names = append(names, "Bob")
fmt.Println(names) // [Alice Bob]
fmt.Println(len(names)) // 2
}
[Alice Bob]
2

Uber Go Style Guide рекомендует: проверяйте пустоту среза через len(s) == 0, а не через s == nil. Пустой срез и nil-срез ведут себя одинаково (длина 0, append работает), но технически это разные вещи — и s == nil может дать неожиданный результат, если срез был инициализирован как []string{}.


Присваивание и множественные операции

Мы научились объявлять переменные. Теперь нужно их изменять. Присваивание в Go выполняется через знак =. Зафиксируйте в голове разницу:

  • := — это создание (объявление) + присваивание
  • = — это изменение (присваивание) уже существующей переменной

Так как Go — статически типизированный язык, вы не можете положить в переменную значение другого типа:

var count int = 5
count = "ten" // ОШИБКА: cannot use "ten" (type untyped string) as type int

Множественное присваивание (Tuple Assignment)

Go поддерживает изящную механику множественного присваивания в одной строке. Представьте классическую задачу: поменять местами значения двух переменных a и b. В C или Java пришлось бы заводить временную переменную temp. В Go это делается одной строкой:

package main
import "fmt"
func main() {
a, b := 10, 20
fmt.Printf("До: a=%d, b=%d\n", a, b)
// Значения вычисляются одновременно, а затем присваиваются
a, b = b, a
fmt.Printf("После: a=%d, b=%d\n", a, b)
}
До: a=10, b=20
После: a=20, b=10

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

Этот же паттерн повсеместно используется при чтении из map (словарей) или получении нескольких значений от функции:

value, ok := myMap["key"] // значение + флаг «ключ найден?»
result, err := someFunc() // результат + ошибка

Строгий надзиратель: неиспользуемые переменные и _

Помните, в уроке 1.5 мы упоминали, что Go не компилирует код с неиспользуемыми переменными? Пришло время разобраться, почему и как с этим жить.

В большинстве языков (Java, Python, C++, TypeScript) неиспользуемая переменная — это предупреждение (warning) от линтера. Программа всё равно соберётся. В Go создатели возвели чистоту кода в абсолют. Согласно спецификации, неиспользуемая локальная переменная — это ошибка компиляции:

package main
import "fmt"
func main() {
x := 5
y := 10 // ОШИБКА КОМПИЛЯЦИИ: y declared and not used
fmt.Println(x)
}

Go FAQ объясняет это так: «The presence of an unused variable may indicate a bug […] Go refuses to compile programs with unused variables or imports, trading short-term convenience for long-term build speed and program clarity.» Неиспользуемая переменная почти всегда означает баг — либо вы забыли дописать логику, либо случайно оставили мусор после рефакторинга.

:::caution Внимание Это правило строго работает только для локальных переменных внутри функций. Переменная на уровне пакета (var unused int вне функции) не вызовет ошибки — она потенциально может быть использована в других файлах того же пакета или экспортирована. Компилятор не может однозначно доказать её «бесполезность». :::

Blank Identifier (Пустой идентификатор _)

Что делать, если функция возвращает несколько значений, а нам нужно только одно? Для таких ситуаций в Go есть пустой идентификатор — подчёркивание _:

package main
import "fmt"
func divide(a, b int) (int, int) {
return a / b, a % b
}
func main() {
// Нам нужен только результат деления. Остаток игнорируем через _
result, _ := divide(10, 3)
fmt.Printf("Результат: %d\n", result)
}
Результат: 3

Подчёркивание _ — это не переменная. Это специальный синтаксический маркер для компилятора: «Я знаю, что функция возвращает значение, но я намеренно его отбрасываю. Не выделяй под него память и не ругайся». Effective Go описывает его как аналог записи в /dev/null — write-only значение.

Иногда во время отладки нужно временно оставить переменную, но закомментировать код, который её использует. Чтобы компилятор не мешал, разработчики используют хак:

x := 42
_ = x // Теперь компилятор считает, что x «использована»

:::danger Важно Не оставляйте _ = x в готовом (production) коде. Это костыль исключительно для локальной отладки! :::

Пустой идентификатор также используется при импорте пакетов ради побочных эффектов (side effects), например для инициализации драйверов баз данных: import _ "github.com/lib/pq".


Как Go выводит типы (Type Inference)

Мы видели, что x := 42 автоматически делает x целым числом. Но как именно работает механизм вывода типов?

Если правая часть выражения — литерал (значение в коде без явного типа), Go использует таблицу типов по умолчанию:

ВыражениеВыведенный типПочему
42, -10intЦелые числа → int (64 бита на современных машинах)
3.14float64Числа с точкой → всегда float64, никогда float32
'G', '⌘'int32 (rune)Символ в одинарных кавычках → rune (Unicode-символ)
"Hello"stringТекст в двойных кавычках → string
true, falseboolЛогические литералы → bool

Проверим. В fmt.Printf есть спецификатор %T, который выводит тип переменной:

package main
import "fmt"
func main() {
a := 42
b := 3.14
c := 'G'
d := "Go"
e := true
fmt.Printf("a: %T (значение: %v)\n", a, a)
fmt.Printf("b: %T (значение: %v)\n", b, b)
fmt.Printf("c: %T (значение: %v)\n", c, c)
fmt.Printf("d: %T (значение: %v)\n", d, d)
fmt.Printf("e: %T (значение: %v)\n", e, e)
}
a: int (значение: 42)
b: float64 (значение: 3.14)
c: int32 (значение: 71)
d: string (значение: Go)
e: bool (значение: true)

Обратите внимание: c показывает 71 — это код символа 'G' в ASCII. Тип int32 (он же rune) хранит числовой код символа, а не сам символ. Подробнее о рунах и строках — в уроке 2.1.2 «Базовые типы».

Вывод типов делает код чище. Явно указывать тип нужно только тогда, когда дефолтный вариант не устраивает (например, нужен int8 или float32). Нюансы приведения типов разберём в уроке 2.1.5 «Определение типов».


Области видимости и инициализация

Где живёт переменная и кто имеет к ней доступ? Это зависит от того, где вы её объявили.

Пакетный уровень (Package-level): переменная, объявленная вне функций (только через var). Доступна из любого файла внутри того же пакета. Если имя начинается с заглавной буквы (var Version string), она становится экспортируемой — доступной другим пакетам. Подробнее об экспорте — в уроке 2.1.7 «Именование и экспорт».

Локальный уровень (Local): переменная внутри функции (через var или :=). Существует только в рамках блока {}, в котором создана.

Сужайте область видимости

Uber Go Style Guide рекомендует: объявляйте переменную как можно ближе к месту использования.

:::tip Для опытных: объявление переменной прямо в if Конструкцию if мы подробно разберём в уроке 2.3.1. Но стоит знать заранее: Go позволяет объявить переменную прямо в условии — через точку с запятой перед проверкой:

// Обычный вариант: err живёт до конца функции
err := os.WriteFile("data.txt", data, 0644)
if err != nil {
return err
}
// Идиоматичный вариант: err живёт только внутри блока if
if err := os.WriteFile("data.txt", data, 0644); err != nil {
return err
}

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

Порядок инициализации пакета

Локальные переменные создаются в тот момент, когда программа доходит до строки с их объявлением. А вот с пакетными переменными интереснее — они инициализируются до вызова main().

Компилятор строит граф зависимостей. Если переменная a инициализируется значением переменной b, компилятор сначала создаст b, даже если в коде она написана ниже:

package main
import "fmt"
// Go сам разберётся, что сначала нужно посчитать b и c
var a = b + c
var b = 10
var c = 20
func main() {
fmt.Printf("a = %d\n", a) // a = 30
}

Но если создать циклическую зависимость (var x = y и var y = x), программа просто не скомпилируется.


Затенение (Shadowing): главный кошмар новичка

Раз мы заговорили про области видимости и блоки {}, обязаны разобрать самую частую логическую ошибку начинающих Go-разработчиков — затенение переменных (variable shadowing).

Затенение происходит, когда вы используете := внутри вложенного блока (например, внутри if), чтобы «изменить» переменную, объявленную снаружи. Но вместо изменения := создаёт новую переменную с таким же именем, которая перекрывает (затеняет) внешнюю:

package main
import (
"fmt"
"os"
)
func main() {
var size int64 = 0
f, err := os.Open("test.txt")
if err == nil {
info, err := f.Stat()
if err == nil {
size := info.Size() // ЗАТЕНЕНИЕ! Создана НОВАЯ переменная size!
fmt.Println("Размер внутри блока:", size)
}
f.Close()
}
// Выведет 0! Внешняя переменная не изменилась.
fmt.Println("Размер файла снаружи:", size)
}

Что произошло? Компилятор увидел size := info.Size() внутри блока if. Он создал новую локальную переменную size для этого блока. Она жила ровно до закрывающей скобки }, после чего была уничтожена. А внешняя переменная size так и осталась нулём.

:::danger Ловушка Shadowing Внутри блоков if и for будьте предельно внимательны. Если нужно обновить значение внешней переменной, используйте оператор присваивания =. Оператор := создаст копию-тень, которая исчезнет за пределами блока. :::

Для борьбы с этим сообщество использует инструмент shadow из пакета golang.org/x/tools — он анализирует код и находит подозрительные перекрытия имён. Подробнее про области видимости — в уроке 2.1.6 «Области видимости и затенение».

Не называйте переменные именами встроенных функций

Ещё одна ловушка из той же серии. В Go есть набор встроенных идентификаторов — len, cap, append, copy, new, make, error, string и другие. Язык позволяет объявить переменную с таким именем — код скомпилируется. Но вы затените встроенную функцию, и в оставшейся части блока она перестанет работать:

// Плохо: затенили встроенную функцию len
len := 10
fmt.Println(len)
// Теперь len() как функция НЕДОСТУПНА в этом блоке!
// size := len(mySlice) — ошибка компиляции
// Хорошо: используйте описательное имя
length := 10

Uber Go Style Guide прямо запрещает это: «The Go language specification outlines several built-in, predeclared identifiers that should not be used as names within Go programs.» Особенно часто новички называют переменную string или error — не делайте так.


Устройство памяти: стек, куча и Escape Analysis

:::tip Для опытных: куда попадают переменные Влияет ли то, как мы объявляем переменную (var или :=), на производительность? Нет, вообще не влияет. Машинный код будет идентичным.

Но как Go решает, где в памяти будет лежать переменная? В C/C++ программист управляет этим вручную (malloc для кучи). В Go это делает компилятор с помощью Escape Analysis (анализ утечек).

У каждой горутины есть свой стек (Stack) — быстрая память. Когда функция завершает работу, всё на её стеке мгновенно стирается. Также есть куча (Heap) — общая память, выделение в которой дороже, а очисткой занимается сборщик мусора (GC).

По умолчанию компилятор старается держать локальные переменные на стеке. Но если вы возвращаете из функции указатель на локальную переменную, компилятор понимает: «Эта переменная нужна кому-то за пределами функции. Нужно перенести её в кучу». Это и есть «утечка» (escape).

func doNotEscape() int {
x := 42
return x // x копируется, оригинал умирает на стеке (быстро)
}
func escape() *int {
y := 100
return &y // y «сбегает» в кучу, потому что отдаём адрес наружу
}

Проверить, куда отправляются переменные: go build -gcflags="-m" . :::


Идиоматичный Go: как принято писать

Язык позволяет писать по-разному, но в профессиональной среде принято придерживаться стандартов. Крупнейшие компании — Google и Uber — опубликовали стилистические руководства по Go.

:::caution Внимание Стайлгайды Google и Uber — это рекомендации, а не закон. Они отражают практики конкретных компаний. В вашей команде или проекте правила могут отличаться. Но если вы только начинаете и собственного стиля ещё нет — это отличная отправная точка. :::

Выжимка главных правил работы с переменными:

1. := по умолчанию

Повсеместно используйте короткое объявление := для локальных переменных. Google Go Style Guide: «For consistency, prefer := over var when initializing a new variable with a non-zero value.»

2. var — только со смыслом

Ключевое слово var уместно в трёх случаях:

  • На уровне пакета (где := запрещено)
  • Когда нужно именно нулевое значение, и вы хотите это подчеркнуть: var mu sync.Mutex, var result []string. Uber запрещает писать var x int = 0, требуя просто var x int
  • Когда тип правой части не совпадает с тем, что вам нужно: var id int64 = 42

Когда мы дойдём до структур (struct), это правило распространится и на них: var user User предпочтительнее user := User{}, если все поля должны быть нулевыми. Uber Go Style Guide объясняет: var явно сигнализирует «мне нужно именно нулевое значение».

3. Длина имени зависит от контекста

Google Style Guide диктует: длина имени переменной пропорциональна размеру её области видимости.

  • Живёт 2–5 строк (индекс в цикле) → одна буква: i, v, k
  • Параметр метода → короткие имена: w для io.Writer, r для io.Reader
  • Живёт 30 строк сложной функции → описательное имя: dbConnection, userCount

При этом не добавляйте типы в названия. Вместо userSliceusers, вместо numUsersuserCount.

СитуацияИспользуйтеПример
Нулевое значение намеренноvarvar count int
Объявление + инициализация:=name := "Go"
Уровень пакетаvar (обязательно)var Version = "1.0"
Пустой срезvarvar items []string
Нужен конкретный типvar или приведениеvar r io.Reader = os.Stdin
Цикл:=for i := 0; i < n; i++
Возврат функции:=f, err := os.Open(name)

:::tip Для опытных: конвенции пакетных переменных Два дополнительных правила из Uber Go Style Guide, которые пригодятся, когда вы начнёте работать с пакетами:

Префикс _ для неэкспортируемых глобальных переменных. Если у вас есть пакетная переменная, которая не должна быть видна снаружи, Uber рекомендует начинать её имя с _: var _defaultPort = 8080 вместо var defaultPort = 8080. Это визуально выделяет глобальное состояние и снижает риск случайного затенения. Исключение — переменные ошибок с префиксом err (например, var errNotFound = errors.New("not found")). Подробнее об экспорте — в уроке 2.1.7.

Избегайте мутабельных глобальных переменных. Пакетные переменные, которые изменяются в рантайме, — это скрытое глобальное состояние. Они усложняют тестирование и создают проблемы при конкурентном доступе. Вместо var _db *sql.DB на уровне пакета лучше передавать зависимости явно — через параметры функций или поля структур (dependency injection). :::


Переменные в Go vs другие языки

Если вы пришли в Go с бэкграундом из других языков, полезно увидеть отличия:

ЯзыкСинтаксис объявленияМутабельностьПри неинициализацииВывод типа
Govar x int / x := 5МутабельнаНулевое значение (безопасно)Да
Pythonx = 5МутабельнаNameErrorДинамическая
C / C++int x = 5;МутабельнаМусор в памяти (опасно!)Да (auto)
Javaint x = 5;МутабельнаОбъекты — null, примитивы — 0Да (var, Java 10+)
Rustlet x = 5;Иммутабельна (нужно mut)Ошибка компиляцииДа
TypeScriptlet x: number = 5МутабельнаundefinedДа

Go занимает уникальную нишу: отказывается от мусора в памяти (как в C/C++), не заставляет писать типы везде (как старая Java), оставляет переменные мутабельными по умолчанию (в отличие от Rust), но не прощает неиспользуемые переменные (в отличие от Python и TypeScript).


Свежие изменения: исправление циклов в Go 1.22

Go — язык консервативный, но развивающийся. Между версиями 1.22 и 1.26 произошло важное изменение, связанное с переменными.

До Go 1.22 переменная, объявленная в цикле (for i, v := range items), была одной и той же переменной в памяти, которая перезаписывалась на каждой итерации. Это порождало баги при использовании горутин (многопоточности): если вы запускали горутины внутри цикла и передавали им указатель на v, все горутины получали ссылку на одну ячейку памяти и выводили последнее значение.

Начиная с Go 1.22, спецификация изменилась: для каждой итерации цикла создаётся новая переменная. Это тихое изменение спасло тысячи часов дебага по всему миру. Привычный хак i := i внутри цикла больше не нужен.


Итоги

КонцепцияЧто помнить
var x typeЯвное объявление. Получает нулевое значение. Работает везде.
var x = valueТип выводится. Для пакетного уровня, когда := запрещён.
x := valueКороткое объявление. Только внутри функций. По умолчанию.
:= vs =:= создаёт переменную, = изменяет существующую.
Zero valuesВ Go нет неинициализированных переменных. int0, string"", boolfalse.
_ (blank identifier)Явно отбрасывает значение. Не переменная — маркер для компилятора.
Shadowing:= во вложенном блоке создаёт новую переменную, а не меняет внешнюю.
НеиспользуемыеЛокальная переменная без использования → ошибка компиляции.

Задачи

Задача 1: Нулевые значения ⭐

Объявите переменные типов int, float64, bool, string без инициализации и выведите их значения. Что напечатает программа?

Решение
package main
import "fmt"
func main() {
var i int
var f float64
var b bool
var s string
fmt.Printf("int: %d, float64: %f, bool: %t, string: %q\n", i, f, b, s)
}
int: 0, float64: 0.000000, bool: false, string: ""

Все переменные получили нулевые значения своих типов — в Go не бывает «мусора в памяти».

Задача 2: Swap ⭐

Объявите две переменные a = 100 и b = 200. Поменяйте их значения местами без временной переменной. Выведите результат.

Решение
package main
import "fmt"
func main() {
a, b := 100, 200
fmt.Printf("До: a=%d, b=%d\n", a, b)
a, b = b, a
fmt.Printf("После: a=%d, b=%d\n", a, b)
}
До: a=100, b=200
После: a=200, b=100

Go вычисляет все выражения справа от = перед записью, поэтому a, b = b, a безопасно.

Задача 3: Найди баг ⭐⭐

Этот код должен напечатать result = 42, но выводит result = 0. Найдите ошибку и исправьте её.

package main
import "fmt"
func main() {
var result int
if true {
result := 42
fmt.Println("внутри:", result)
}
fmt.Println("result =", result)
}
Подсказка

Обратите внимание на оператор внутри блока if — это := или =?

Решение

Проблема — затенение (shadowing). Внутри if оператор := создаёт новую локальную переменную result, которая перекрывает внешнюю. Исправление — заменить := на =:

package main
import "fmt"
func main() {
var result int
if true {
result = 42 // = вместо :=
fmt.Println("внутри:", result)
}
fmt.Println("result =", result)
}
внутри: 42
result = 42

Задача 4: Детектив типов ⭐⭐

Не запуская код, определите типы переменных a, b, c, d, e. Затем проверьте себя с помощью fmt.Printf("%T").

a := 100
b := 3.0
c := 'Z'
d := "Go"
e := a > 50
Решение
package main
import "fmt"
func main() {
a := 100
b := 3.0
c := 'Z'
d := "Go"
e := a > 50
fmt.Printf("a: %T\n", a) // int
fmt.Printf("b: %T\n", b) // float64
fmt.Printf("c: %T\n", c) // int32 (rune)
fmt.Printf("d: %T\n", d) // string
fmt.Printf("e: %T\n", e) // bool
}
  • 100 — целое число → int
  • 3.0 — число с точкой → float64 (не float32!)
  • 'Z' — символ в одинарных кавычках → int32 (он же rune)
  • "Go" — строка → string
  • a > 50 — результат сравнения → bool

Что дальше?

Значения хранить умеем, но какие именно типы данных можно в эти переменные положить? В следующем уроке 2.1.2 «Базовые типы» мы разберём числа, строки, руны и булевы значения до последнего бита. Увидимся!


Источники