Go — язык, созданный для простоты

Go (Golang) — компилируемый язык, созданный в Google для надёжных и быстрых сервисов. Он сочетает простоту и производительность: горутины делают параллельность лёгкой, а сборщик мусора — безопасной. Идеальный выбор для серверов, кли и DevOps-инструментов.

Уровень: начальный 18 разделов 3–4 часа

1. Что такое Go

Go разработали Роберт Гризмер, Роб Пайк и Кен Томпсон в 2009 году. Цель — убрать боль разработки на C++ и Java: быстрая компиляция, простой синтаксис, встроенная параллельность и жёсткая статическая типизация.

Ключевые особенности Go:

Где применяетсяПримеры продуктов
Инфраструктура и DevOpsDocker, Kubernetes, Terraform
Серверы и APIсервисы Google, Cloudflare
CLI-инструментыGitHub CLI, Hugo
Сбор данных и сетипрокси, анализаторы трафика
Go не подходит там, где нужны сложные дженерики, виртуальная машина или огромная экосистема библиотек. Но для «сервис на 99.99% аптайма» — это один из лучших вариантов.

2. Установка и первая программа

Установи Go с официального сайта go.dev — там есть установщики для Windows, macOS и Linux. После установки проверь версию.

go version        # go version go1.24.0 windows/amd64

# на Windows можно также поставить через scoop или winget
winget install GoLang.Go

Создай файл main.go с классической программой:

package main

import "fmt"

func main() {
    fmt.Println("Привет, Go!")
}
go run main.go     # запуск
go build main.go   # сборка бинарника main.exe

Команда go run компилирует и сразу выполняет код, а go build создаёт исполняемый файл, который можно переносить на другие компьютеры.

Структура Go-программы

ЭлементНазначение
package mainисполняемая программа (не библиотека)
import "fmt"подключение пакета ввода-вывода
func main()точка входа программы
go run / go buildзапуск и сборка
В Go неиспользуемые импорты и переменные — ошибка компиляции. Этим язык приучает к чистоте: написал import "fmt", но не использовал — код не соберётся, пока не уберёшь.

3. Типы и переменные

Go строго типизирован: у каждой переменной есть тип, и неявные преобразования запрещены.

package main

import "fmt"

func main() {
    var name string = "Аня"      // явный тип
    var age int = 25
    var height float64 = 1.72
    var active bool = true

    city := "Москва"              // короткая запись (тип выводится сам)
    year := 2026

    fmt.Println(name, age, height, active, city, year)

    // несколько переменных сразу
    var x, y int = 10, 20
    a, b := "привет", 3.14

    // константы
    const pi = 3.14159
}

Основные типы

ТипРазмерПример
intмашинное слово42
int8 / int16 / int32 / int641/2/4/8 байт-5
float32 / float644 / 8 байт3.14
string—"текст"
bool—true / false
byte (=uint8)1 байт255
rune (=int32)4 байта'Ж'
// явное преобразование типов
var i int = 7
var f float64 = float64(i)
var s string = strconv.Itoa(i)  // "7" — нужен импорт strconv
Строки в Go — это последовательности байт UTF-8, поэтому русский текст работает без лишних настроек. Каждый символ — rune (4 байта), что важно учитывать при переборе строк.

4. Условия и циклы

В Go всего один цикл — for. Он заменяет while и do-while, а выбор строится на if и switch.

package main

import "fmt"

func main() {
    score := 85

    if score >= 90 {
        fmt.Println("Отлично")
    } else if score >= 60 {
        fmt.Println("Хорошо")
    } else {
        fmt.Println("Подтянись")
    }

    // switch — альтернатива цепочке if
    day := "saturday"
    switch day {
    case "saturday", "sunday":
        fmt.Println("Выходной")
    default:
        fmt.Println("Рабочий день")
    }
}

Варианты цикла for

// классический for
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// while-стиль
n := 10
for n > 0 {
    n--
}

// бесконечный цикл
for {
    // выход — через break
    break
}

// итерация по коллекции (range)
names := []string{"Аня", "Боря", "Вера"}
for idx, name := range names {
    fmt.Println(idx, name)
}
Условия в if обязаны быть вокруг скобок? Нет — в Go скобки не нужны: if x > 5, а не if (x > 5). Но фигурная скобка открывается на той же строке — иначе код не соберётся.
В if можно инициализировать переменную, видимую только внутри блока: if v := compute(); v > 0 { ... }. Это удобно и безопасно.

5. Функции и замыкания

Функции объявляются ключевым словом func и могут возвращать несколько значений — это удобно для пар «результат + ошибка».

package main

import "fmt"

// функция с одним результатом
func square(x int) int {
    return x * x
}

// несколько возвращаемых значений
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("деление на ноль")
    }
    return a / b, nil
}

// именованные результаты
func swap(x, y int) (a, b int) {
    a, b = y, x
    return  // возвращает a и b
}

func main() {
    fmt.Println(square(5))           // 25
    res, err := divide(10, 2)
    if err != nil {
        fmt.Println("Ошибка:", err)
    } else {
        fmt.Println("Результат:", res)
    }
    a, b := swap(1, 2)
    fmt.Println(a, b)                // 2 1
}

Функции — значения: их можно передавать в другие функции и возвращать. Замыкание «запоминает» переменные внешней функции.

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    next := counter()
    fmt.Println(next()) // 1
    fmt.Println(next()) // 2
    fmt.Println(next()) // 3
    fmt.Println(counter()()) // 1 — отдельный счётчик
}
Анонимные функции (без имени) удобны для сортировки, колбэков и обработчиков. Например, http.HandleFunc("/", func(w, r) { ... }).

6. Указатели

Указатель хранит адрес переменной в памяти. Оператор & получает адрес, * разыменовывает — обращается к значению.

package main

import "fmt"

func addPrice(p *int, amount int) {
    *p += amount  // меняем значение по адресу
}

func main() {
    price := 100
    addPrice(&price, 50)  // передаём адрес переменной
    fmt.Println(price)    // 150

    // nil-указатели
    var p *int
    if p == nil {
        fmt.Println("Указатель пустой")
    }
}

Значение против указателя

Передача по значениюПередача по указателю
копия переменнойработа с оригиналом
изменения не сохраняютсяизменения сохраняются
быстро для малых данныхне копирует большие структуры
При использовании указателя всегда проверяй, что он не nil, иначе программа упадёт с паникой dereference of nil pointer. И не возвращай указатель на локальную переменную без причины — в Go это безопасно, но запутывает.
Срезы и map в Go уже содержат ссылку на данные внутри, поэтому передавать их по указателю обычно не нужно — изменение внутри функции «видит» вызывающий код.

7. Структуры и методы

Структура (struct) объединяет поля в один тип. Методы — функции с «получателем» (receiver), они определяют поведение.

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

// метод со значением получателя — не меняет оригинал
func (u User) Greeting() string {
    return "Привет, " + u.Name
}

// метод с указателем — может менять поля
func (u *User) Birthday() {
    u.Age++
}

func main() {
    u := User{Name: "Аня", Age: 25}
    fmt.Println(u.Greeting()) // Привет, Аня
    u.Birthday()
    u.Birthday()
    fmt.Println(u.Age)        // 27
}
// конструктор-функция (по соглашению NewXxx)
func NewUser(name string) *User {
    return &User{Name: name}
}

type Product struct {
    Name  string
    Price int
}

// встраивание структуры (embedding) — композиция
type CartItem struct {
    Product
    Quantity int
}

func main() {
    item := CartItem{
        Product:  Product{Name: "Кроссовки", Price: 4990},
        Quantity: 2,
    }
    fmt.Println(item.Name)  // поле из встроенной структуры
    fmt.Println(item.Price)
}
В Go нет классов и наследования. Вместо этого — структуры и композиция: встраивание одной структуры в другую передаёт её поля и методы. Это проще и предсказуемее классического ООП.

8. Интерфейсы

Интерфейс описывает набор методов. Тип удовлетворяет интерфейсу автоматически, если у него есть все эти методы — явных объявлений не нужно.

package main

import "fmt"

type Shape interface {
    Area() float64
}

type Rectangle struct{ W, H float64 }
type Circle struct{ R float64 }

func (r Rectangle) Area() float64 { return r.W * r.H }
func (c Circle) Area() float64    { return 3.14 * c.R * c.R }

func printArea(s Shape) {
    fmt.Printf("Площадь: %.2f\n", s.Area())
}

func main() {
    printArea(Rectangle{W: 3, H: 4}) // 12.00
    printArea(Circle{R: 2})          // 12.56

    var s Shape = Rectangle{W: 5, H: 2}
    fmt.Println(s.Area())
}

Пустой интерфейс interface{} (или any) принимает любой тип, но пользоваться полями нужно через утверждение типа.

func describe(v any) {
    switch t := v.(type) {
    case string:
        fmt.Println("строка:", t)
    case int:
        fmt.Println("число:", t)
    default:
        fmt.Println("неизвестный тип")
    }
}
Утверждение типа выглядит так: value, ok := x.(string). Второе значение ok — успешно ли прошло утверждение. Используй его, чтобы избежать паники.
Ошибки в Go — это интерфейс error. Его принято возвращать последним и проверять сразу: if err != nil { return ... }. Ранние return и короткие функции делают код читаемым.

9. Goroutines и каналы

Goroutine — это легко весомая единица параллельного выполнения, запускается словом go. Каналы (chan) позволяют goroutines обмениваться данными и синхронизироваться.

package main

import (
    "fmt"
    "time"
)

func work(name string) {
    for i := 1; i <= 3; i++ {
        fmt.Println(name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go work("горутина A")   // запуск в отдельной горутине
    go work("горутина B")
    time.Sleep(500 * time.Millisecond) // ждём завершения
    fmt.Println("Готово")
}

Каналы

func sum(nums []int, result chan int) {
    total := 0
    for _, n := range nums {
        total += n
    }
    result <- total // отправка в канал
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6}
    result := make(chan int)

    go sum(nums[:3], result)
    go sum(nums[3:], result)

    a, b := <-result, <-result  // ждём оба результата
    fmt.Println(a, b, a+b)
}
// буферизованный канал и закрытие
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)          // закрываем: больше отправлять нельзя

for v := range ch {
    fmt.Println(v) // читаем все значения
}
Отправка в незакрытый, но никем не читаемый канал — вечная блокировка (deadlock). Закрывать канал должен отправитель, а не получатель. Повторное закрытие и отправка в закрытый канал вызывают панику.
Для многих входящих работников используй select с несколькими каналами: select { case v := <-ch1: ...; case v := <-ch2: ... }. Это как switch, но для каналов.

10. Пакеты, модули и HTTP-сервер

Go-программа собирается из пакетов. Модуль (go.mod) описывает проект и его зависимости. Всю экосистему версионирует команда go mod.

go mod init myapp        # создаём модуль
go get github.com/gin-gonic/gin   # добавляем внешний пакет
go mod tidy              # подчистить зависимости
go run .
// mathx/mathx.go — свой пакет
package mathx

func Max(a, b int) int {
    if a > b {
        return a
    }
    return b
}
// main.go
package main

import (
    "fmt"
    "myapp/mathx"
)

func main() {
    fmt.Println(mathx.Max(10, 3)) // 10
}

Стандартная библиотека уже содержит HTTP-сервер. Напишем мини-API без сторонних фреймворков:

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type Product struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Price int    `json:"price"`
}

var products = []Product{
    {ID: 1, Name: "Кроссовки", Price: 4990},
    {ID: 2, Name: "Шапка", Price: 990},
}

func listProducts(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(products)
}

func main() {
    http.HandleFunc("/products", listProducts)
    log.Println("Сервер: http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
# проверка
curl http://localhost:8080/products

Популярные фреймворки и пакеты

ПакетНазначение
net/httpHTTP-сервер из стандартной библиотеки
gin / chi / echoweb-фреймворки (маршруты, middleware)
database/sqlработа с базами данных (драйверы отдельно)
gormORM для баз данных
testingвстроенное тестирование
Имя файла часто неважно для сборки, важно имя пакета. Экспортируемые (публичные) имена начинаются с большой буквы, внутренние — с маленькой: mathx.Max доступен снаружи, mathx.min — нет.
Компиляция в один бинарник упрощает деплой: скопировал файл на сервер — программа работает. Для контейнеризации подойдёт Docker со scratch-образом размером десятки мегабайт.

11. Обработка ошибок: error, panic, recover

В Go ошибки — обычные значения типа error. Их возвращают, проверяют и оборачивают, а не бросают исключения. Типовой паттерн:

func parseBudget(text string) (int, error) {
    n, err := strconv.Atoi(text)
    if err != nil {
        return 0, fmt.Errorf("parse budget %q: %w", text, err)
    }
    return n, nil
}
// %w позволяет распаковать цепочку errors.Is / errors.As
if errors.Is(err, strconv.ErrSyntax) {
    log.Println("некорректный формат числа")
}

Для поистине критических ситуаций существует panic, а defer + recover ловит его в горутине:

func safe() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    panic("недоступная БД")
}
Не используй panic для бизнес-ошибок — только для truly непереживаемых состояний (нарушенный инвариант). Обычные ошибки возвращают через error.
fmt.Errorf("...: %w") — главный инструмент оборачивания: он сохраняет исходную ошибку для errors.Is и errors.As.

12. Работа с файлами и контекстом

Чтение файлов лежит в стандартной библиотеке os и io. Эффективный паттерн — построчная обработка:

f, err := os.Open("data/orders.txt")
if err != nil {
    return err
}
defer f.Close()

sc := bufio.NewScanner(f)
for sc.Scan() {
    process(sc.Text())
}
if err := sc.Err(); err != nil {
    log.Println("чтение прервано:", err)
}

defer f.Close() — идиома: закрытие файла обязательно выполнится при выходе из функции.

Для длительных операций используют context.Context — он передаёт дедлайн, отмену и значения между сервисами:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

data, err := db.Query(ctx, "SELECT * FROM events")
Контекст обязателен почти во всех современных Go API (http, sql, redis). Правило: первый параметр функции, который тянет внешний ресурс, — это ctx.
Не храни контекст в структуре — он принадлежит запросу и живёт внутри функции. Исключение — наследование родительского сервиса.

13. Логирование и переменные окружения

Начни с log/slog — структурированного логгера из стандартной библиотеки (Go 1.21+):

import (
    "log/slog"
    "os"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    slog.SetDefault(logger)

    slog.Info("сервис запущен",
        "port", 8080,
        "env", os.Getenv("APP_ENV"),
    )
    slog.Error("ошибка подключения", "db", "postgres")
}

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

func appConfig() (addr string, debug bool) {
    addr = os.Getenv("ADDR")
    if addr == "" {
        addr = ":8080"
    }
    debug = os.Getenv("DEBUG") == "1"
    return addr, debug
}
Разделяй уровни: Debug — трассировка, Info — важные события, Error — с контекстом kernel. Не логируй пароли и токены.
В продакшене собирай структурированные логи в Сборщик: целиком лог-строка — это событие, а поля — измерения для метрик и алертов.

14. Тесты и бенчмарки

Тесты в Go — обычные функции TestXxx в файлах _test.go:

// parser_test.go
func TestParseBudget(t *testing.T) {
    cases := []struct {
        in   string
        want int
        err  bool
    }{
        {"100", 100, false},
        {"ab", 0, true},
    }
    for _, c := range cases {
        got, err := parseBudget(c.in)
        if (err != nil) != c.err {
            t.Fatalf("%q: err = %v, want %v", c.in, err, c.err)
        }
        if !c.err && got != c.want {
            t.Errorf("%q: got %d, want %d", c.in, got, c.want)
        }
    }
}
func BenchmarkParseBudget(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _, _ = parseBudget("12345")
    }
}

Запуск: go test ./..., для бенчмарков — go test -bench=. -benchmem. Команда сама находит все _test.go в пакете.

// табличные тесты — главный стиль Go
// покрытие: go test -cover
Табличные тесты (slice of cases) заменяют тысячи строк: один цикл тестирует все кейсы, включая ошибки входных данных.
Bенчмарки сравнивают только на одинаковом окружении: прогоняй их на CI или в одном контейнере, чтобы результаты не «плыли» из-за нагрузки машины.

15. Продвинутая конкурентность

После знакомства с горутинами и каналами переходим к надёжным паттернам. Три классических инструмента синхронизации:

func main() {
    var wg sync.WaitGroup
    for _, job := range jobs {
        wg.Add(1)
        go func(j string) {
            defer wg.Done()
            process(j)
        }(job)
    }
    wg.Wait()
}

Гонки данных (data race) — главный источник хаоса в конкурентном коде. Запускай тесты с флагом -race: встроенный детектор ловит большинство гонок на этапе разработки.

Популярная модель — worker pool: фиксированное число горутин берёт задачи из канала, а результаты кладёт в другой:

jobs := make(chan int, 10)
for w := 0; w < 4; w++ {
    go worker(jobs)
}
for i := 0; i < 10; i++ { jobs <- i }
close(jobs)
Захватывай переменную цикла через параметр функции — go func(j string){...}(job). Иначе все горутины прочитают последнее значение итерации.
Девиз Go — «Не общайтесь через память; делитесь памятью через общение». Если данные читают несколько горутин — передавай их через канал, а не мутируй общей переменной.

16. JSON и практика REST

Сериализация в Go — стандартная encoding/json с тегами структур. Теги задают имена полей в JSON и правила пропуска:

type Product struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Price     int    `json:"price"`
    Discount  *int   `json:"discount,omitempty"`
    CreatedAt string `json:"created_at"`
}

p := Product{ID: 1, Name: "Кроссовки", Price: 4990}
data, _ := json.Marshal(p)   // компактный JSON
var q Product
json.Unmarshal(data, &q)     // обратно в структуру
nil-слайс и пустой слайс сериализуются по-разному: null против []. Если API обещает пустой массив, инициализируй слайс явно: data := []Product{}.
Обработка ошибок парсинга: для JSON от пользователя проверяй UnmarshalTypeError и отправляй клиенту понятный текст, а не «400 Bad Request» без деталей.

17. Профилирование и оптимизация

Go включает профилировщик pprof — получить картину «где время и память» можно за несколько минут:

import _ "net/http/pprof"

// в main добавить отдельный локальный порт:
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()
# снять CPU-профиль 30с и открыть интерактив
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
# или текст-топ для консоли
go tool pprof -top http://localhost:6060/debug/pprof/allocations

Типичные находки: избыточные аллокации в циклах, fmt.Sprintf вместо strconv, копии больших строк, неэффективные map. Порядок работы один: профиль → горячие точки → простые фиксы → повторный замер.

go test -bench=. -benchmem показывает аллокации на операцию — бенчмарки дополняют профиль и защищают от регрессий.
Микрооптимизации (ручные пулы, небезопасные преобразования bytes) нужны примерно в 1% кода. Без профиля «ускорение» — это угадывание, а порою ещё и усложнение.

18. Сборка и кросс-компиляция

Одна команда собирает бинарник под любую платформу — суперсила Go в деплое и CLI-инструментах:

GOOS=linux   GOARCH=amd64 go build -o app-linux .
GOOS=windows GOARCH=amd64 go build -o app.exe .
GOOS=darwin  GOARCH=arm64 go build -o app-mac .
# меньше размер + версия в бинарнике:
go build -ldflags="-s -w -X main.version=1.2.3" .

Флаги -s -w убирают таблицу символов и уменьшают файл, а -X подставляет значение в переменную — стандарт версионирования CLI-утилит.

Контейнеризация — двухэтапный Dockerfile: стадия сборки и минимальный финальный образ:

FROM golang:1.24 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .

FROM scratch
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]
CGO_ENABLED=0 отключает C-зависимости — бинарник статический и работает в любом образе. Для драйверов с C-кодом (SQLite) собирай на целевом окружении.
Чек-лист релиза: тесты и go vet чистые, платформа целевая, версия через ldflags, образ минимален, секреты только в переменных окружения.

Что дальше

Ты прошёл весь базовый курс Go: типы, функции, указатели, структуры, интерфейсы, горутины и HTTP. Дальше стоит углубиться в работу с базами через SQL, понять сеть в HTTP и DNS и познакомиться с деплоем в Хостинг и деплой. А для алгоритмического фундамента загляни в Алгоритмы и структуры данных.

← Вернуться к курсам