Go (Golang) с нуля: большая шпаргалка по синтаксису, горутинам и бэкенду
Go (Golang) — компилируемый статически типизированный язык golang от Google с намеренно простым синтаксисом, быстрой сборкой и первоклассной поддержкой конкурентности. Этот материал — большая шпаргалка go и одновременно практическое введение в язык программирования golang: от установки и типов до горутин, каналов и HTTP-серверов. Если вы ищете «go для начинающих», начните с раздела Быстрый старт — он проведёт от hello.go до работающего API за десять минут. Остальные разделы — это golang шпаргалка: открывайте нужную тему и копируйте идиоматичные примеры. Всё написано под Go 1.22+, примеры запускаются через go run.
Быстрый старт
Go поставляется одним набором инструментов: компилятор, форматтер, линтер, менеджер зависимостей и тестовая инфраструктура уже внутри. Никаких виртуальных окружений — только go.mod, где фиксируются версии модулей.
# Установка (Linux, официальный тарбол)
wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin # добавь в ~/.bashrc
# macOS: brew install go | Ubuntu/Debian: sudo apt install golang-go
go version # go version go1.22.5 linux/amd64
go env # GOPATH, GOMODCACHE, GOCACHE, GOOS, GOARCH
# Модуль и запуск
mkdir myapp && cd myapp
go mod init github.com/user/myapp # создаёт go.mod — паспорт модуля
go run . # собрать во временный файл и запустить
go build -o app . # один самодостаточный бинарник ./app
go install . # установить в $GOBIN ($GOPATH/bin)
# Зависимости
go get github.com/google/uuid@v1.6.0
go mod tidy # добавить недостающие, убрать лишние
go mod download # скачать модули в GOMODCACHE
go list -m all # всё дерево модулей
go list -m -u all # доступные обновления
go mod why github.com/google/uuid
go mod vendor # скопировать зависимости в vendor/
# Workspace: несколько модулей рядом без replace
go work init ./service ./shared
go work use ./service
go work sync
// main.go
package main
import "fmt"
func main() {
fmt.Println("Привет, Go!")
}
// go.mod — источник истины о версиях
// module github.com/user/myapp
// go 1.22 // минимальная версия языка для модуля
// toolchain go1.22.5 // какую версию брать, если локальная старше (Go 1.21+)
// go.work — только для локальной разработки, в git обычно не коммитится
// go 1.22
// use (
// ./service
// ./shared
// )
Ключевые термины
| Термин | Значение |
|---|---|
| Модуль (module) | Единица версионирования и зависимостей, описывается go.mod |
| Пакет (package) | Единица компиляции внутри модуля; файлы одной папки делят пакет |
| Горутина (goroutine) | Лёгкий поток, управляемый планировщиком Go, а не ОС |
| Канал (channel) | Типизированная очередь для обмена данными между горутинами |
| Интерфейс | Набор методов; реализуется неявно, без implements |
| Срез (slice) | Дескриптор {указатель, len, cap} над массивом — основная коллекция |
| Мапа (map) | Хеш-таблица ключ → значение, порядок обхода не определён |
| Embedding | Встраивание типа анонимным полем — композиция вместо наследования |
defer |
Отложенный вызов, срабатывает при выходе из функции (LIFO) |
context |
Перенос отмены, дедлайна и request-scoped данных по стеку |
| Нулевое значение | Значение по умолчанию (0, "", false, nil) — неинициализированных нет |
go.sum |
Контрольные суммы зависимостей для воспроизводимых сборок |
Структура проекта
Go не диктует структуру папок, но сообщество выработало устойчивый стандарт. Главное правило: точка входа тонкая, бизнес-логика — в internal, где компилятор сам запретит импорт снаружи.
myapp/
├── cmd/
│ └── api/
│ └── main.go # точка входа: только сборка зависимостей
├── internal/
│ ├── user/
│ │ ├── service.go # бизнес-логика (недоступна другим модулям)
│ │ └── service_test.go
│ └── storage/
│ └── postgres.go
├── pkg/
│ └── httpx/ # код, пригодный для импорта извне
│ └── middleware.go
├── go.mod
└── go.sum
cmd/<binary>/— по папке на каждый бинарник;main.goтолько читает конфиг и связывает пакеты.internal/— механизм видимости на уровне компилятора: импортировать может лишь поддерево родителя.pkg/— код, который действительно готовы отдавать наружу. Не создавай, пока не понадобилось.- Имена пакетов короткие, в нижнем регистре, без подчёркиваний и без «stutter» (
user.Service, а неuser.UserServiceManager).
// cmd/api/main.go — тонкая точка входа
package main
import (
"log/slog"
"net/http"
"github.com/user/myapp/internal/user"
)
func main() {
svc := user.NewService()
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", svc.Handler)
slog.Info("server started", "addr", ":8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
slog.Error("server failed", "err", err)
}
}
Переменные, константы и типы
Четыре способа объявить переменную, и каждый уместен в своём контексте. Короткая форма := работает только внутри функций. Константы вычисляются на этапе компиляции, а iota избавляет от ручной нумерации. Приведение типов всегда явное — неявных преобразований нет даже между int и int64.
var x int // объявление без значения -> нулевое значение 0
var s string = "go" // тип и значение
y := 42 // короткая форма — только внутри функции
a, b := 1, "two" // множественное присваивание
var ( // групповое объявление читается лучше
host string = "localhost"
port int = 8080
)
_, _ = a, b // blank identifier _ — намеренно игнорируем
const Pi = 3.14159 // нетипизированная константа: подставится где нужно
const (
StatusNew = iota // 0
StatusActive // 1
StatusBlocked // 2
StatusDeleted // 3
)
// iota в выражениях и с пропусками
type ByteSize float64
const (
_ = iota // пропускаем 0
KB ByteSize = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20
GB // 1 << 30
)
// Частая идиома: строковое представление enum
func (s Status) String() string {
return [...]string{"new", "active", "blocked", "deleted"}[s]
}
// Приведение типов явное; усечение при переполнении происходит молча
var i int = 42
f := float64(i)
u := uint8(i)
r := string(rune(65)) // "A": int -> rune -> string
bs := []byte("go") // строка в байты
var big int = 300
_ = uint8(big) // 44 — компилятор не предупредит!
В Go нет «неинициализированных» значений: каждая переменная получает нулевое значение своего типа. Это устраняет целый класс ошибок, но важно понимать, что именно значит «пусто» для срезов, мап и интерфейсов.
| Тип | Пример | Нулевое значение | Особенность |
|---|---|---|---|
int / int8..64 |
var i int |
0 |
Размер int зависит от платформы |
uint / uint8..64 |
var u uint |
0 |
Беззнаковые, усечение молчит |
float32/64 |
var f float64 |
0 |
float64 — по умолчанию для математики |
bool |
var b bool |
false |
— |
string |
var s string |
"" |
Неизменяемая, UTF-8 |
byte / rune |
byte(65) |
0 |
Псевдонимы uint8 и int32 |
[N]T |
var a [3]int |
[0 0 0] |
Value-тип, копируется целиком |
[]T |
var s []int |
nil |
len=0, но nil != пустой |
map[K]V |
var m map[string]int |
nil |
Запись в nil — паника |
chan T |
var c chan int |
nil |
Операции блокируют навсегда |
*T / func |
var p *int |
nil |
Разыменование/вызов nil — паника |
interface |
var e error |
nil |
См. ловушку nil-интерфейса |
struct |
var u User |
Все поля обнулены | == работает, если поля сравнимы |
var (
i int // 0
f float64 // 0
s string // "" — это НЕ nil!
b bool // false
p *int // nil
sl []int // nil: len == 0, но это не пустой срез
m map[string]int // nil: запись паникует
c chan int // nil: операции блокируют навсегда
err error // nil
)
// Строки — байтовые срезы UTF-8, а не последовательность символов.
// Для кириллицы len считает байты, а не буквы:
s = "Привет"
len(s) // 12 байт, а не 6 символов
utf8.RuneCountInString(s) // 6 — правильный подсчёт
for i, r := range s { // range даёт руны и байтовые индексы
_ = i
_ = r
}
rs := []rune(s) // 6 элементов-кодпоинтов
Управляющие конструкции
Go минималистичен: один цикл for, нет тернарного оператора, switch не проваливается без явного fallthrough. Зато if умеет инициализировать переменную прямо в условии, а с Go 1.22 у каждой итерации цикла своя переменная — классический баг с замыканиями исправлен на уровне языка.
// if с инициализацией: v видна только внутри веток
if v, ok := m["key"]; ok {
fmt.Println("нашли:", v)
}
// Тернарного оператора нет — только if/else
var sign string
if x := 10; x > 0 {
sign = "positive"
} else {
sign = "non-positive"
}
_ = sign
// for покрывает все формы цикла
for i := 0; i < 5; i++ { } // классический
for i := range 5 { _ = i } // Go 1.22: range по целому -> 0..4
for cond { } // аналог while
for { break } // бесконечный
items := []string{"a", "b"}
for i, v := range items { _, _ = i, v } // индекс и значение
for _, v := range items { _ = v } // только значение
for i := range items { _ = i } // только индекс
for k, v := range m { _, _ = k, v } // порядок обхода мапы случаен
// С Go 1.22 у каждой итерации своя переменная — замыкание безопасно
for _, v := range []int{1, 2, 3} {
go func() { fmt.Println(v) }()
}
// switch без break; можно объединять значения и работать как цепочка условий
switch day := time.Now().Weekday(); day {
case time.Saturday, time.Sunday:
fmt.Println("выходной")
default:
fmt.Println("будни")
}
switch { // switch-true заменяет длинный if/else
case score >= 90:
fmt.Println("отлично")
case score >= 70:
fmt.Println("хорошо")
default:
fmt.Println("надо подтянуть")
}
switch n {
case 1:
fmt.Println("one")
fallthrough // явный проброс в следующий case
case 2:
fmt.Println("two") // выполнится и при n == 1
}
// Метки — способ выйти из вложенного цикла
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer
}
}
}
Функции
Функции первого класса: их передают, возвращают и хранят в переменных. Несколько возвращаемых значений — идиоматичный способ отдавать результат вместе с ошибкой. defer откладывает вызов до выхода из функции; порядок LIFO, аргументы вычисляются в момент defer.
// Несколько возвращаемых значений — так в Go возвращают ошибки
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Именованные результаты документируют смысл и допускают naked return
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
// Вариативные функции
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3)
sum([]int{1, 2, 3}...) // распаковка среза
// Функции как значения и замыкания
double := func(x int) int { return x * 2 }
func apply(f func(int) int, v int) int { return f(v) }
apply(double, 21) // 42
func counter() func() int {
n := 0
return func() int { n++; return n } // захват переменной извне
}
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // выполнится при любом выходе, даже при ошибке
return nil
}
func order() {
defer fmt.Println("1") // третий
defer fmt.Println("2") // второй
defer fmt.Println("3") // первый
}
// Вывод: 3, 2, 1
for i := 0; i < 3; i++ {
defer fmt.Println(i) // выведет 2, 1, 0 — аргумент зафиксирован
}
// recover превращает панику в ошибку на границах (хендлеры, воркеры)
func safe() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
panic("boom")
return nil
}
Указатели и семантика значений
Go всегда передаёт аргументы по значению — то есть копирует. Указатель даёт доступ к оригиналу. Никакой арифметики указателей, как в C, и никакого -> — только * и .. new выделяет память под нулевое значение, make инициализирует срез, мапу или канал. Go безопасно возвращает адрес локальной переменной: компилятор сам решает через escape analysis, вынести её в кучу или оставить на стеке.
x := 42
p := &x // p имеет тип *int — это адрес x
fmt.Println(*p) // 42 — разыменование
*p = 100 // меняем значение по адресу
type User struct{ Name string }
func modifyValue(u User) { u.Name = "changed" } // меняет копию
func modifyPtr(u *User) { u.Name = "changed" } // меняет оригинал
u := User{Name: "Аня"}
modifyValue(u) // u.Name остался "Аня"
modifyPtr(&u) // u.Name стал "changed"
// new vs make
pi := new(int) // *int, указывает на 0
sl := make([]int, 0, 10) // len=0, cap=10 — избегаем реаллокаций
mp := make(map[string]int)
ch := make(chan int, 10)
_, _, _ = pi, sl, mp
// Возврат адреса локальной переменной безопасен:
func newInt() *int {
v := 42
return &v // v «убежит» в кучу
}
// Посмотреть решения: go build -gcflags="-m" ./...
Массивы и срезы
Массив — фиксированная длина, value-тип: присваивание копирует все элементы, а [3]int и [4]int — разные типы. На практике используют срезы: лёгкий дескриптор из указателя, длины и ёмкости, который ссылается на общий массив — отсюда большинство сюрпризов.
// Массивы
var a [3]int // [0 0 0]
b := [3]int{1, 2, 3}
c := [...]int{1, 2, 3} // длина выводится компилятором
_, _, _ = a, b, c
// a == b работает, если элементы сравнимы
// Срезы: создание и append
s := []int{1, 2, 3}
s = make([]int, 3) // len=3, cap=3, нули
s = make([]int, 0, 10) // len=0, cap=10 — преаллокация
s = append(s, 4) // при переполнении cap растёт (~вдвое)
s = append(s, 1, 2, 3)
s = append(s, []int{7, 8}...) // склеивание
len(s) // длина
cap(s) // ёмкость до реаллокации
s[1:3] // срез среза: новая len, тот же массив
s[:2] // с начала
s[2:] // до конца
s[:] // целиком
dst := make([]int, len(s))
n := copy(dst, s) // копирует min(len), возвращает число
_ = n
// Ловушка: срез смотрит в тот же массив
base := []int{1, 2, 3, 4}
sub := base[1:3] // [2 3], cap=3
sub[0] = 99 // base = [1 99 3 4]
// Трёхиндексный срез ограничивает cap — append не затронет base
sub2 := base[1:3:3]
// Удаление
i := 1
base = append(base[:i], base[i+1:]...) // с сохранением порядка
// Быстрое удаление без порядка
base[i] = base[len(base)-1]
base = base[:len(base)-1]
// Вставка x на позицию i
x := 5
base = append(base[:i], append([]int{x}, base[i:]...)...)
// nil-срез безопасен для len/append/range, но не для записи по индексу
var ns []int
ns = append(ns, 1) // ok
// ns[0] = 1 // panic: index out of range
// Маленький срез от большого массива удерживает весь массив.
// Копия освобождает память: copy(first, bigSlice)
_, _, _ = sub2, ns, n
Мапы
Мапа — хеш-таблица с амортизированным доступом за O(1). Порядок обхода намеренно случаен. Мапы — ссылочный тип, их нельзя сравнивать между собой (только с nil). Запись в nil-мапу паникует, поэтому её обязательно инициализируют.
m := map[string]int{"a": 1}
m = make(map[string]int) // правильная инициализация
m["b"] = 2 // запись
v := m["a"] // чтение
v, ok := m["missing"] // comma-ok: 0, false
_, _ = v, ok
delete(m, "a") // удаление
len(m) // число ключей
clear(m) // Go 1.21: удалить всё
// nil-мапа: чтение безопасно, запись — паника
var nm map[string]int
_ = nm["x"] // 0
// nm["x"] = 1 // panic: assignment to entry in nil map
// Сортированный обход — единственный способ получить порядок
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
// Множество через мапу с пустой структурой — 0 байт на значение
seen := map[string]struct{}{}
seen["x"] = struct{}{}
if _, ok := seen["x"]; ok {
fmt.Println("уже видели")
}
// Ключи должны быть сравнимыми: срезы, мапы и функции — нельзя
Структуры, методы и embedding
Структура — набор именованных полей; инициализируй именованно, иначе добавление поля сломает позиционные литералы. Метод — функция с получателем: value копирует, pointer меняет оригинал. Embedding — это композиция, а не наследование: встроенный тип продвигает свои поля и методы наружу. Теги управляют сериализацией.
type Point struct {
X, Y int
}
p1 := Point{1, 2} // позиционно — избегай
p2 := Point{X: 1, Y: 2} // именованно — надёжно
p3 := &Point{X: 1, Y: 2} // указатель
p3.X = 10 // авто-разыменование: (*p3).X не нужен
_, _ = p1, p2
// Методы: value vs pointer получатель
func (p Point) Distance() int { return p.X + p.Y } // читает
func (p *Point) Move(dx, dy int) { p.X += dx; p.Y += dy } // меняет
// Правило: если хоть один метод с pointer-получателем — делай все такими.
// Embedding: анонимное поле продвигает поля и методы
type Animal struct{ Name string }
func (a Animal) Speak() string { return a.Name + " издаёт звук" }
type Dog struct {
Animal
Breed string
}
d := Dog{Animal: Animal{Name: "Рекс"}, Breed: "лабрадор"}
d.Name // продвинутое поле
d.Speak() // продвинутый метод
d.Animal.Speak() // явный доступ
// Теги управляют JSON-сериализацией
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // пропустить, если пусто
Pass string `json:"-"` // никогда не сериализовать
}
Интерфейсы
Интерфейс — контракт из методов, который реализуется неявно: достаточно иметь нужные методы, слово implements не нужно. Определяй интерфейсы на стороне потребителя и делай их маленькими. any (синоним interface{}) хранит значение любого типа; извлечь его можно type assertion или type switch. Ловушка nil-интерфейса — источник самых коварных багов.
type Stringer interface {
String() string
}
type User struct{ Name string }
func (u User) String() string { return u.Name }
var s Stringer = User{Name: "Аня"} // реализация неявная
fmt.Println(s) // "Аня"
// Композиция интерфейсов вместо «швейцарских ножей»
type Reader interface{ Read(p []byte) (int, error) }
type Writer interface{ Write(p []byte) (int, error) }
type ReadWriter interface {
Reader
Writer
}
// any и type assertion
var v any = "hello"
str, ok := v.(string) // безопасная форма: ok == false при несоответствии
str = v.(string) // паникует, если тип другой
_, _ = str, ok
// type switch
var x any = 42
switch t := x.(type) {
case int:
fmt.Println("int:", t+1)
case string:
fmt.Println("string:", t)
default:
fmt.Printf("unknown %T\n", t)
}
// Ловушка nil-интерфейса: интерфейс = (тип, значение).
// Если тип не nil, то и интерфейс не nil, даже когда значение nil.
type MyErr struct{}
func (e *MyErr) Error() string { return "my" }
func bad() error {
var p *MyErr = nil
return p // интерфейс = (*MyErr, nil) — это НЕ nil!
}
func good() error { return nil } // честный nil-интерфейс
if err := bad(); err != nil {
fmt.Println("поймали:", err) // выполнится — ловушка сработала
}
Ошибки
Ошибки в Go — обычные значения: их возвращают, проверяют и оборачивают. Паника предназначена только для программных багов, а не для ожидаемых сбоев. %w сохраняет цепочку для errors.Is/errors.As, %v её теряет. Sentinel-ошибка — конкретное значение для сравнения; кастомный тип даёт структурированные данные; errors.Join (Go 1.20) объединяет несколько ошибок.
// Sentinel-ошибка для сравнения
var ErrNotFound = errors.New("not found")
// Оборачивание через %w сохраняет цепочку
func queryUser(id int) error {
if err := db.Query(id); err != nil {
return fmt.Errorf("query user %d: %w", id, err)
}
return nil
}
// errors.Is проверяет всю цепочку на совпадение значения
if errors.Is(err, ErrNotFound) {
// сработает, даже если ErrNotFound обёрнута в несколько слоёв
}
// errors.As извлекает конкретный тип по цепочке
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("проблемный путь:", pathErr.Path)
}
// Кастомная ошибка со структурой
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("поле с ошибкой:", ve.Field)
}
// Объединение ошибок (Go 1.20): errors.Is проверяет обе ветки
err = errors.Join(validateName(), validateEmail())
err = fmt.Errorf("validate user: %w", err)
// %w сохраняет цепочку, %v превращает ошибку в строку:
// fmt.Errorf("wrap: %w", err) -> errors.Is/As работают
// fmt.Errorf("flat: %v", err) -> цепочка потеряна
Generics
Generics (Go 1.18+) позволяют писать функции и типы, работающие с несколькими типами, с проверкой на компиляции. Не злоупотребляй: интерфейсы — для поведения, generics — для контейнеров и алгоритмов. Constraint ограничивает допустимые типы; тильда ~ разрешает любой тип с данным базовым типом. Большинство задач уже закрыто пакетами slices и maps (Go 1.21+) и встроенными min/max.
// Type parameters: [T any] означает «любой тип»
func Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, 0, len(s))
for _, v := range s {
out = append(out, f(v))
}
return out
}
strs := Map([]int{1, 2, 3}, strconv.Itoa) // []string{"1", "2", "3"}
_ = strs
// Constraint: union допустимых типов, ~ разрешает производные типы
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](vals []T) T {
var total T
for _, v := range vals {
total += v
}
return total
}
Sum([]int{1, 2, 3}) // 6
Sum([]float64{1.5, 2.5}) // 4
// Готовые constraints в cmp
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Стандартные пакеты slices/maps закрывают типовые задачи
import "slices"
nums := []int{3, 1, 2}
slices.Sort(nums)
slices.Contains(nums, 2) // true
slices.Index(nums, 3) // индекс или -1
slices.Clone(nums) // поверхностная копия
slices.Max(nums)
slices.Reverse(nums)
Горутины и WaitGroup
Горутина — лёгкий поток под управлением планировщика Go: старт стоит пару килобайт стека, поэтому можно запускать десятки тысяч. go f() запускает функцию конкурентно. WaitGroup ждёт завершения группы. Утечка горутины — блокировка навсегда, о которой рантайм не сообщит: типовой случай — отправка в канал, который никто не читает.
go fmt.Println("в горутине")
// WaitGroup ждёт завершения группы горутин
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // счётчик увеличиваем ДО запуска
go func(n int) {
defer wg.Done() // уменьшить счётчик, даже при панике
fmt.Println("worker", n)
}(i)
}
wg.Wait() // блокирует, пока счётчик не станет нулём
// ПЛОХО: горутина навсегда висит на небуферизованном канале
func leak() {
ch := make(chan int)
go func() { ch <- 42 }() // получателя нет — блокировка навсегда
}
// ХОРОШО: буфер даёт отправителю завершиться
func noLeak() {
ch := make(chan int, 1)
go func() { ch <- 42 }()
<-ch
}
runtime.NumGoroutine() // число живых горутин — помогает ловить утечки
runtime.Gosched() // уступить процессор другим горутинам
Каналы
Канал — типизированная очередь между горутинами. Небуферизованный синхронизирует отправителя и получателя (рандеву), буферизованный развязывает их, пока есть место. range читает до закрытия, поэтому забыть close означает вечное ожидание. select ждёт готовности любого из нескольких каналов, а default делает проверку неблокирующей.
ch := make(chan int) // небуферизованный
ch = make(chan int, 10) // буферизованный
ch <- 42 // отправка
v := <-ch // получение
v, ok := <-ch // ok=false, если канал закрыт и пуст
_, _ = v, ok
close(ch) // закрывает ТОЛЬКО отправитель
// Небуферизованный: отправка блокируется до получения
ping := make(chan string)
go func() { ping <- "ping" }()
fmt.Println(<-ping)
// Буферизованный: отправка не блокирует, пока буфер не полон
done := make(chan struct{}, 1)
done <- struct{}{}
// range завершается по close — забыть close означает зависнуть
nums := make(chan int, 3)
go func() {
for i := 0; i < 3; i++ {
nums <- i
}
close(nums)
}()
for n := range nums {
fmt.Println(n)
}
// select ждёт любое из событий; default делает проверку неблокирующей
select {
case v := <-ch1:
fmt.Println("из ch1:", v)
case ch2 <- 42:
fmt.Println("отправили в ch2")
case <-time.After(time.Second):
fmt.Println("таймаут")
default:
fmt.Println("никто не готов")
}
// Направленные каналы документируют роль функции
func producer(out chan<- int) { out <- 1 } // только отправка
func consumer(in <-chan int) { <-in } // только получение
// nil-канал блокирует навсегда — иногда это нужно намеренно
var nilCh chan int
_ = nilCh
// <-nilCh // блокировка навсегда
Синхронизация: Mutex, RWMutex, Once, atomic
Гонка данных возникает при одновременном доступе к памяти без синхронизации; рантайм может её не заметить, поэтому всегда гоняй тесты с -race. Mutex — взаимное исключение, RWMutex пускает много читателей параллельно, но писателя эксклюзивно. sync.Once гарантирует однократное выполнение, atomic даёт быстрые операции без блокировок для одного значения.
// ГОНКА: 1000 горутин инкрементируют одну переменную
var counter int
for i := 0; i < 1000; i++ {
go func() { counter++ }() // потерянные обновления
}
// Mutex — взаимное исключение
var (
mu sync.Mutex
guarded int
)
mu.Lock()
guarded++
mu.Unlock()
// Идиома: defer разблокировки защищает от ранних return
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.counter++
}
// RWMutex: читатели параллельно, писатель эксклюзивно
var rw sync.RWMutex
rw.RLock()
_ = data
rw.RUnlock()
rw.Lock()
data = newValue
rw.Unlock()
// sync.Once — ленивая инициализация ровно один раз, даже при гонке
var (
once sync.Once
config *Config
)
func GetConfig() *Config {
once.Do(func() { config = loadConfig() })
return config
}
// atomic: быстрые операции без блокировок для одного значения
import "sync/atomic"
var n atomic.Int64
n.Add(1)
n.Load()
n.Store(42)
n.CompareAndSwap(42, 43)
var ptr atomic.Pointer[Config] // типизированный указатель (Go 1.19+)
ptr.Store(config)
c := ptr.Load()
_ = c
// Правило: Mutex — для составных инвариантов, atomic — для одного значения.
// Мьютекс нельзя копировать — передавай по указателю.
context
context.Context переносит отмену, дедлайн и request-scoped данные по стеку вызовов. Он всегда первый аргумент и никогда не хранится в структуре. cancel нужно вызывать всегда, иначе утекут таймеры и горутины. Значения — только request-scoped данные (ID запроса, авторизация), а не конфигурация.
func fetch(ctx context.Context, url string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
_, err = http.DefaultClient.Do(req)
return err
}
// Отмена по событию
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // ВСЕГДА, иначе утечка
go func() {
<-done
cancel() // сигнал всем, кто слушает ctx.Done()
}()
if err := fetch(ctx, url); errors.Is(err, context.Canceled) {
fmt.Println("работа отменена")
}
// Таймаут и дедлайн
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ctx, cancel = context.WithDeadline(parent, time.Now().Add(5*time.Second))
defer cancel()
select {
case <-ctx.Done():
fmt.Println(ctx.Err()) // DeadlineExceeded или Canceled
case res := <-ch:
fmt.Println(res)
}
// Значения: ключ — собственный неэкспортируемый тип
type ctxKey string
const userIDKey ctxKey = "userID"
ctx = context.WithValue(ctx, userIDKey, 42)
id, ok := ctx.Value(userIDKey).(int) // type assertion обязателен
_, _ = id, ok
// С Go 1.20 можно передать причину отмены
ctx, cancel = context.WithCancelCause(parent)
cancel(errors.New("graceful shutdown"))
cause := context.Cause(ctx)
_ = cause
Пакеты, видимость, init
Видимость определяется регистром первой буквы: заглавная — экспорт, строчная — только внутри пакета. Никаких public/private. init выполняется автоматически до main, один раз на пакет, после инициализации переменных. Циклические импорты запрещены компилятором.
package mathutil
func Add(a, b int) int { return a + b } // Add видна снаружи
func sub(a, b int) int { return a - b } // sub — приватная
const MaxSize = 100 // видна
type Client struct{ host string } // Client виден, host — нет
// init выполняется до main, один раз на пакет
var pool *Pool
func init() {
pool = newPool()
}
// Группировка импортов: стандартная библиотека, свои, сторонние
import (
"fmt"
"os"
"github.com/user/myapp/internal/db"
"github.com/google/uuid"
)
// Особые случаи
import (
_ "net/http/pprof" // blank import: нужен только побочный эффект init
m "github.com/x/math" // алиас
)
_, _ = m.Add, uuid.New
_ = db.Open
// internal/ ограничивает импорт поддеревом родителя:
// github.com/me/app/internal/db -> только из github.com/me/app/...
Тестирование и инструменты
Тесты живут рядом с кодом в файлах *_test.go и пишутся тем же языком, без внешних фреймворков. Table-driven тесты — идиома Go: один цикл покрывает десяток сценариев. Инструменты качества встроены: gofmt каноничен для всего сообщества, go vet и staticcheck находят ошибки до ревью, pprof объясняет, куда уходит CPU и память.
// add_test.go
package mathutil
import "testing"
func TestAdd(t *testing.T) {
if got := Add(2, 3); got != 5 {
t.Errorf("Add(2,3) = %d, want 5", got)
}
}
// Table-driven тест с подтестами
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"zero", 0, 0, 0},
{"negative", -1, 1, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
// Бенчмарк: b.N подбирает фреймворк
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Пример-тест проверяется по выводу и служит документацией
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}
// Помощники: t.Helper(), t.Cleanup(), t.Parallel(), go test -fuzz
go test ./... # все пакеты
go test -v ./mathutil # подробный вывод
go test -run TestAdd # тесты по regexp
go test -race ./... # детектор гонок — обязателен в CI
go test -cover ./... # покрытие
go test -count=1 ./... # отключить кэш результатов
go test -short ./... # пропустить долгие тесты
go test -bench=. -benchmem ./... # бенчмарки с аллокациями
# Инструменты качества
gofmt -w . # форматирование в каноническом стиле
go fmt ./... # то же через go
goimports -w . # gofmt + сортировка и чистка импортов
go vet ./... # встроенный статический анализ
staticcheck ./... # глубже vet: go install honnef.co/go/tools/cmd/staticcheck@latest
golangci-lint run # агрегатор линтеров (.golangci.yml)
# Профилирование
go test -bench=. -cpuprofile=cpu.prof
go test -bench=. -memprofile=mem.prof
go tool pprof -http=:8081 cpu.prof
// Профилирование работающего сервиса
import _ "net/http/pprof" // регистрирует /debug/pprof/* в DefaultServeMux
go http.ListenAndServe("localhost:6060", nil) // отдельный порт только для профилей
// Прочие команды: go doc strings.Builder, go generate ./...,
// go env -w GOFLAGS=-mod=mod, dlv debug (отладчик Delve)
HTTP-сервер и клиент
С Go 1.22 стандартный ServeMux понимает методы и path-параметры — для простых API внешние роутеры не нужны. JSON-хендлеры работают через encoding/json. Клиент всегда с таймаутом, тело ответа всегда закрывается. Graceful shutdown по сигналу — обязательный элемент production-сервиса.
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // извлечь path-параметр
fmt.Fprintf(w, "user %s", id)
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second, // защита от Slowloris
}
log.Fatal(srv.ListenAndServe())
// JSON-хендлер
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func createUser(w http.ResponseWriter, r *http.Request) {
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
u.ID = 1
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)
}
// Graceful shutdown по сигналу
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("listen", "err", err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx) // ждёт завершения активных запросов
// HTTP-клиент: всегда таймаут и всегда defer resp.Body.Close()
client := &http.Client{Timeout: 5 * time.Second}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close() // иначе утекут соединения
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
slog.Error("bad status", "code", resp.StatusCode, "body", body)
return
}
var out User
_ = json.NewDecoder(resp.Body).Decode(&out)
// Middleware — функция, оборачивающая http.Handler
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request", "method", r.Method, "path", r.URL.Path,
"dur", time.Since(start))
})
}
handler := logging(mux)
_ = handler
Работа с файлами и JSON
os.ReadFile и os.WriteFile покрывают большинство задач; для больших файлов — потоковое чтение через bufio.Scanner. JSON — часть стандартной библиотеки, сериализация управляется тегами структур.
data, err := os.ReadFile("config.json") // прочитать целиком
if err != nil {
return err
}
if err := os.WriteFile("out.txt", data, 0o644); err != nil {
return err
}
// Потоковое чтение построчно
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil { // Scanner копит ошибку
return err
}
// Директории и обход файловой системы
os.MkdirAll("data/cache", 0o755)
entries, err := os.ReadDir(".") // уже отсортирован по имени
if err != nil {
return err
}
for _, e := range entries {
if e.IsDir() {
continue
}
info, _ := e.Info()
fmt.Println(e.Name(), info.Size())
}
if _, err := os.Stat("x.txt"); errors.Is(err, os.ErrNotExist) {
fmt.Println("файла нет")
}
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
Tags []string `json:"tags,omitempty"`
}
cfg := Config{Host: "localhost", Port: 8080}
b, err := json.Marshal(cfg) // компактный JSON
b, err = json.MarshalIndent(cfg, "", " ") // с отступами для чтения
if err != nil {
return err
}
var parsed Config
if err := json.Unmarshal(b, &parsed); err != nil {
return err
}
// Потоково — для io.Reader/Writer и больших данных
dec := json.NewDecoder(resp.Body)
dec.DisallowUnknownFields() // строгий режим
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(cfg)
raw := json.RawMessage(`{"x":1}`) // отложенный разбор вложенного поля
_ = raw
Конкурентные паттерны
Worker pool ограничивает конкурентность фиксированным числом горутин — защита от перегрузки внешних сервисов. Fan-in собирает результаты из нескольких каналов в один, pipeline — цепочка стадий, каждая читает входной канал и пишет в выходной. Для отмены группы по первой ошибке используют errgroup.
// Worker pool: N воркеров разбирают очередь задач
func workerPool(jobs <-chan int, results chan<- int, workers int) {
var wg sync.WaitGroup
for w := 0; w < workers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := range jobs { // range завершится по close(jobs)
results <- j * j
}
}()
}
go func() {
wg.Wait()
close(results) // закрыть, когда все воркеры вышли
}()
}
jobs := make(chan int, 100)
results := make(chan int, 100)
go workerPool(jobs, results, 4)
for i := 0; i < 100; i++ {
jobs <- i
}
close(jobs)
for r := range results {
_ = r
}
// Fan-in: несколько каналов -> один
func fanIn(chans ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range chans {
wg.Add(1)
go func(c <-chan int) { // c передаём аргументом
defer wg.Done()
for v := range c {
out <- v
}
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}
// Pipeline: цепочка стадий
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
for v := range square(gen(1, 2, 3)) {
fmt.Println(v) // 1, 4, 9
}
// errgroup: отмена всей группы горутин по первой ошибке
import "golang.org/x/sync/errgroup"
g, gctx := errgroup.WithContext(ctx)
for _, url := range urls {
url := url // фиксируем для замыкания
g.Go(func() error {
return fetch(gctx, url)
})
}
if err := g.Wait(); err != nil {
return err
}
Частые ошибки
| Ошибка | Причина | Решение |
|---|---|---|
assignment to entry in nil map |
Запись в неинициализированную мапу | Инициализируй make(map[K]V) или литералом перед записью |
index out of range на nil-срезе |
nil-срез нельзя индексировать и записывать | Используй append или make([]T, n) заранее |
| Утечка горутины | Блокировка на канале, который никто не читает | Буферизуй канал, закрывай его, добавляй context для отмены |
| Data race | Конкурентный доступ к данным без синхронизации | Mutex/atomic, обязательно go test -race в CI |
| Захват переменной цикла (до Go 1.22) | Замыкание видело одну переменную на все итерации | Передавай значение параметром; в Go 1.22+ проблема исправлена |
err != nil при nil-значении |
В интерфейсе лежит (тип, nil), это не равно nil |
Возвращай явный nil, проверяй через errors.As |
defer внутри цикла |
Отложенные вызовы копятся до выхода из функции | Вынеси тело итерации в отдельную функцию/замыкание |
| Проигнорированная ошибка | _ = f() молча скрывает сбой |
Обрабатывай и оборачивай через %w; включи errcheck |
Неверное использование context |
Хранение в структуре, nil, подмена им параметров |
Передавай первым аргументом, бери из вызывающего кода |
| Deadlock на небуферизованном канале | Отправка и получение в одной горутине | Разведи по горутинам или используй буфер |
GOMAXPROCS «для ускорения» |
Ручное управление вредит планировщику; CPU-bound не ускоряется от горутин сверх числа ядер | Не трогай без профилирования; помни, что по умолчанию GOMAXPROCS = число ядер |
Ожидание порядка от range по мапе |
Порядок обхода псевдослучайный | Сортируй ключи через slices.Sort перед обходом |
Копирование sync.Mutex |
Мьютекс нельзя копировать после использования | Передавай по указателю, не возвращай по значению |
time.After в цикле |
Таймеры не собираются сборщиком до срабатывания | Используй time.NewTimer и timer.Stop() |
| Незакрытое тело HTTP-ответа | Утекают соединения и память | Всегда defer resp.Body.Close() |
os.Exit/log.Fatal с defer |
Отложенные вызовы не выполняются при выходе | Очистку делай до os.Exit, log.Fatal — только в main |
Ресурсы
- Официальная документация Go — спецификация, туториалы, гайды
- Effective Go — как писать идиоматичный код
- Go by Example — примеры на каждую тему с выводом
- Блог Go — разборы языка, конкурентности и рантайма
- pkg.go.dev — документация всех пакетов и модулей
- Go Code Review Comments — правила ревью и стиля
- Go 1.22 Release Notes — новая семантика цикла,
ServeMux, range по int - The Go Memory Model — формальные гарантии горутин и синхронизации
Что дальше
- Шпаргалка по Docker: контейнеры, образы, Dockerfile, compose
- Шпаргалка по Kubernetes: Pods, Deployments, Services, Helm
- Кэширование в бэкенде: стратегии, Redis, инвалидация
- Шпаргалка по Rust: владение, заимствование, трейты
- Шпаргалка по Си: указатели, память, компиляция