Go спроектирован так, чтобы код читался одинаково у всех. В языке мало конструкций, почти нет «синтаксического сахара», а компилятор ругается на лишнее — например, на неиспользуемую переменную или импорт. Эта статья — первое знакомство с тем, как выглядит программа на Go: из чего она собрана, как объявляются переменные и типы, и как работают базовые управляющие конструкции.
Простота как принцип
Главная идея Go — читаемость важнее краткости. Авторы языка сознательно отказались от многих привычных по другим языкам возможностей: нет наследования классов, нет исключений в привычном виде, нет тернарного оператора, есть только один цикл — for. Меньше способов написать одно и то же — меньше споров и проще читать чужой код.
Из этого следует строгость компилятора. Объявил переменную и не использовал — ошибка компиляции, а не предупреждение. Импортировал пакет «на будущее» — тоже ошибка. Поначалу это раздражает, но быстро приучает не оставлять мусора.
Пакеты и func main
Любой файл Go начинается с объявления пакета. Пакет — это единица организации кода: набор файлов в одной директории, которые видят внутренние имена друг друга.
Программа, которую можно запустить, живёт в пакете main и содержит функцию main — точку входа:
живой пример
package main
import "fmt" // пакет стандартной библиотеки для ввода-вывода
func main() {
fmt.Println("Привет, Go") // печать строки в консоль
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Несколько важных деталей сразу:
package main+func main()— это то, что делает программу исполняемой. Библиотеки используют другое имя пакета и функцииmainне имеют.importподключает другие пакеты.fmt— часть стандартной библиотеки.- Видимость определяется регистром буквы. Имя с заглавной буквы (
Println) экспортируется — доступно из других пакетов. Имя со строчной буквы видно только внутри своего пакета. Отдельных ключевых слов вродеpublic/privateнет.
Объявление переменных: var и :=
Переменную можно объявить двумя способами. Полная форма — через var:
var age int = 30 // тип указан явно
var name = "Анна" // тип выведен компилятором из значения (string)
var active bool // без значения — переменная получит нулевое значение
Короткая форма — оператор :=. Он одновременно объявляет переменную и присваивает значение, тип выводится автоматически:
age := 30 // int
name := "Анна" // string
active := true // bool
Короткая формула: := работает только внутри функций; var можно использовать и на уровне пакета. На практике внутри функций почти всегда пишут :=, а var оставляют для случаев, когда нужно явно задать тип или объявить переменную без начального значения.
Тип в Go указывается после имени — age int, а не int age. Это непривычно, но читается слева направо: «переменная age типа int».
Базовые типы
Набор встроенных типов небольшой и предсказуемый:
var i int // целое, размер зависит от платформы (обычно 64 бита)
var i32 int32 // целое фиксированного размера; есть int8/int16/int64
var u uint // беззнаковое целое
var f float64 // число с плавающей точкой (по умолчанию для дробей)
var b bool // true / false
var s string // строка, неизменяемая последовательность байт; литералы записаны в UTF-8
var r rune // один символ Unicode (псевдоним int32)
Несколько правил, которые экономят время:
- По умолчанию для целых берут
int, для дробных —float64. Конкретные размеры (int32,int64) — когда это важно для формата данных или экономии памяти. - Go не делает неявных преобразований между числовыми типами. Сложить
intиint64напрямую нельзя — нужно явно преобразовать:int64(x) + y. Это убирает целый класс скрытых ошибок. stringнеизменяема: нельзя поменять символ по индексу. Чтобы собрать строку из частей, используют конкатенацию или специальные средства из стандартной библиотеки.
Нулевые значения (zero values)
В Go нет понятия «неинициализированная переменная». Если объявить переменную без значения, она получит нулевое значение своего типа:
var n int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (пустая строка)
var p *int // nil (для указателей, срезов, map, каналов, функций, интерфейсов)
Это сознательное решение: переменная всегда в осмысленном состоянии, «мусора» из памяти не бывает. Нулевые значения часто делают типы готовыми к работе сразу — например, нулевое значение многих структур из стандартной библиотеки уже можно использовать без дополнительной настройки.
Короткая формула: объявил — уже инициализировано. Думай о нулевом значении как о части дизайна типа, а не как о «пустышке».
Константы
Константы объявляются через const и вычисляются на этапе компиляции. Менять их нельзя:
const Pi = 3.14159
const greeting = "привет"
const (
StatusOK = 200
StatusError = 500
)
Числовые константы в Go нетипизированы, пока их не присвоят переменной. Благодаря этому const Pi = 3.14159 спокойно используется и там, где ждут float64, и там, где ждут float32 — без явного преобразования. Для перечислений в Go используют конструкцию iota — о ней в разделе «Глубже» ниже.
Управляющие конструкции
if
Скобки вокруг условия не нужны, фигурные скобки обязательны:
if age >= 18 {
fmt.Println("совершеннолетний")
} else {
fmt.Println("нет")
}
Удобная особенность — короткий оператор перед условием. Можно объявить переменную прямо в if, и она будет видна только внутри ветвей:
if n := len(name); n > 0 {
fmt.Println("длина имени:", n)
}
// здесь n уже недоступна
for — единственный цикл
В Go только один цикл — for, но он покрывает все случаи. Классическая форма со счётчиком:
for i := 0; i < 3; i++ {
fmt.Println(i)
}
Форма «пока условие истинно» (аналог while из других языков):
i := 0
for i < 3 {
i++
}
Бесконечный цикл — просто for { }. А для перебора коллекций есть for ... range:
names := []string{"Анна", "Борис"}
for index, value := range names {
fmt.Println(index, value)
}
С Go 1.22 range умеет считать сам: for i := range 3 даёт 0, 1, 2, а если счётчик не нужен — просто for range 3.
switch
switch в Go мощнее, чем привычный по другим языкам. По умолчанию нет «проваливания» в следующую ветку — break писать не нужно:
switch day {
case "сб", "вс":
fmt.Println("выходной")
case "пт":
fmt.Println("почти выходной")
default:
fmt.Println("рабочий день")
}
switch можно писать вообще без выражения — тогда ветки case содержат условия. Это чистая замена длинной цепочки if/else:
switch {
case age < 18:
fmt.Println("несовершеннолетний")
case age < 65:
fmt.Println("взрослый")
default:
fmt.Println("пенсионер")
}
Почему в Go нет тернарного оператора
В большинстве языков есть запись вида condition ? a : b. В Go её намеренно нет. Авторы посчитали, что вложенные тернарники быстро становятся нечитаемыми, а одна явная конструкция лучше двух способов делать выбор.
Вместо тернарника пишут обычный if:
label := "нет"
if age >= 18 {
label = "да"
}
Чуть длиннее — зато читается однозначно. Это хорошая иллюстрация всей философии языка: меньше способов, больше предсказуемости.
Глубже: перечисления через iotaрасширенное
В Go нет enum. Вместо него — свой тип на основе int и блок констант, где iota выдаёт 0, 1, 2… по порядку строк:
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
)
Тип нужен не для красоты: функция, которая принимает Weekday, не примет случайное число 7 без явного преобразования, а switch по такому типу читается как по перечислению. Строкового имени у константы нет — его печатают методом String() на этом типе; сгенерировать такой метод умеет утилита stringer.
Глубже: строки, байты и рунырасширенное
Строка в Go — это байты, а не символы. Для латиницы это незаметно, для кириллицы всплывает сразу: len считает байты, а буква в UTF-8 занимает два.
живой пример
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "привет"
fmt.Println(len(s)) // 12: байтов
fmt.Println(utf8.RuneCountInString(s)) // 6: символов
for i, r := range s {
fmt.Println(i, string(r)) // индекс байта и символ
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Символ здесь называется руной (rune, тот же int32), а range по строке идёт именно по рунам и отдаёт индекс первого байта каждой. Индексация s[0] возвращает байт (byte, он же uint8), а не букву; резать строку по байтовым индексам посреди символа нельзя. Собирать длинную строку в цикле через + дорого — каждое сложение создаёт новую строку; для этого есть strings.Builder.
Глубже: вариативные функции, init и затенениерасширенное
Функция может принимать сколько угодно аргументов одного типа, если последний параметр объявлен с многоточием: func sum(nums ...int) int. Внутри nums это обычный срез, пустой при вызове без аргументов, а готовый срез передают с многоточием после имени: sum(s...). Так устроены fmt.Println и append.
Порядок запуска пакета фиксирован: сначала инициализируются пакеты, которые он импортирует, потом его переменные уровня пакета в порядке зависимостей, потом функции init, и только после всех пакетов начинается main. Функций init в пакете может быть несколько, звать их руками нельзя. На этом держится анонимный импорт import _ "net/http/pprof": пакет подключают ради его init, который регистрирует обработчики, а имён из него не используют.
Затенение (shadowing) это объявление переменной с тем же именем во вложенном блоке. Чаще всего ловят err - переменную с ошибкой (что это за тип, подробно в статье Ошибки; здесь важно само затенение):
живой пример
package main
import (
"errors"
"fmt"
)
func load() (int, error) { return 0, errors.New("нет данных") }
func main() {
var err error
if v, err := load(); err == nil {
fmt.Println("значение", v)
}
fmt.Println(err) // <nil>: внутри if была другая err
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Оператор := в заголовке if создал новую err, живущую только внутри блока, и внешняя осталась nil. Компилятор это разрешает, go vet по умолчанию молчит, ловит отдельный анализатор shadow и внимательность на ревью: во вложенном блоке с := стоит проверять, не объявлена ли переменная с таким именем снаружи.
Коротко
- Программа на Go начинается с
package; исполняемая программа — этоpackage mainи функцияfunc main(). - Видимость имени определяется регистром первой буквы: заглавная — экспортируется, строчная — видна только в пакете.
- Переменные объявляют через
var(с явным типом) или:=(короткая форма, только внутри функций); тип пишется после имени. - Каждый тип имеет нулевое значение — переменная без явного значения всегда инициализирована (
0,"",false,nil). - Неявных преобразований между числовыми типами нет — преобразовывай явно.
- Цикл один —
for, он же заменяетwhile;switchне «проваливается» и работает без выражения; тернарного оператора нет — используется обычныйif. iotaв блокеconstдаёт перечисления.- Строка — байты:
lenсчитает байты,rangeидёт по рунам,s[i]даёт байт; длинные строки собирают черезstrings.Builder. ...Tделает функцию вариативной,initзапускается после переменных пакета и доmain, анонимный импорт_подключает пакет ради егоinit;:=во вложенном блоке затеняет внешнюю переменную.
Что почитать дальше
- Функции в Go - параметры, несколько возвратов и замыкания.
- Структуры и методы — как из базовых типов собирать собственные.
- Срезы и map — основные коллекции языка.
- Строки в Go - подробный разбор строки, рун и пакета strings.
- Инструменты Go — go run, go build, форматирование и тесты.