Кладовочка

О разном, чтобы было под рукой онлайн

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

Ресурсы

Что дальше


Связанные шпаргалки: C++, Rust, Си, Алгоритмы, Docker