JavaRush /Курсы /Go SELF /Что учить дальше: SQL/DB, Docker, CI, профилирование, без...

Что учить дальше: SQL/DB, Docker, CI, профилирование, безопасность

Go SELF
72 уровень , 2 лекция
Открыта

1. Что учить дальше: инженерные области вокруг Go

Когда курс заканчивается, возникает типичная мысль: «Ну всё, пора учить следующий if!» (спойлер: следующего if нет). На практике рост Go‑разработчика почти всегда идёт не через ещё одну конструкцию языка, а через расширение контекста: где живут данные, как доставляется приложение, как оно проверяется автоматически, как измеряется производительность и как оно защищается от атак и ошибок эксплуатации.

Полезная ментальная модель такая: Go — это «двигатель», а остальная инженерия — это «дорога, правила движения и техосмотр». Можно иметь шикарный двигатель, но если выезжать на трассу без тормозов, фар и страховки, поездка будет короткой, но насыщенной эмоциями.

В качестве примера будем продолжать развивать наш учебный проект как Tasker: у нас есть доменная модель задач, CLI и/или HTTP‑API, и слой хранения (сначала файл/память), который мы хотим сделать «по‑настоящему взрослым».

Ниже — пять направлений, которые дают максимальную отдачу после базового Go: SQL/DB, Docker, CI, профилирование, безопасность.

Как эти направления складываются в одну картину

Чтобы мозг не воспринимал SQL/Docker/CI/pprof/безопасность как пять разрозненных вселенных, полезно увидеть их как один жизненный цикл приложения. Тот же Tasker проходит этот путь почти в любом реальном проекте:

flowchart TD
    A[Код на Go: домен + app + adapters] --> B[Хранилище: SQL/DB]
    A --> C[Docker: воспроизводимый запуск]
    A --> D[CI: автоматические проверки]
    A --> E[Профилирование: измеряем и объясняем]
    A --> F[Безопасность: лимиты, секреты, контракты ошибок]
    D --> C
    C -->|deploy/run| G[Прод окружение]
    G --> E
    G --> F
    B --> G

Смысл схемы простой: каждая область усиливает остальные. CI защищает Docker‑сборку от регрессий, Docker делает запуск воспроизводимым, DB даёт устойчивые данные, профилирование помогает не гадать, безопасность не даёт вашему успеху превратиться в публичную историю на Хабре.

2. SQL и базы данных: как думать о данных

SQL и базы данных часто пугают новичков не синтаксисом, а ощущением «там же целая вселенная». Хорошая новость: чтобы стать продуктивным, не нужно помнить все виды JOIN наизусть. Нужно научиться двум вещам: во‑первых, моделировать данные (как они живут во времени, что является ключом, какие поля обязательны), а во‑вторых, держать границу между доменом и хранением, чтобы БД не протекала во все слои приложения.

Модель данных: задача — это не только title

Когда вы переносите Tasker из файла в БД, у задачи появляется «официальная биография». Например, вам внезапно становится важно, когда задача создана, когда завершена, может ли done_at быть NULL, и что делать с удалением (удалять физически или помечать как удалённую).

Тут очень помогает дисциплина из курса про инварианты: доменная модель должна быть честной. Если у задачи есть ID, то не делайте вид, что его нет. Если Title обязателен — валидируйте это на границе (CLI/HTTP), а не «пусть в БД упадёт».

Мини‑скелет доменного типа (без углубления в миграции и ORM) может быть таким:

package domain

import "time"

type Task struct {
	ID        int
	Title     string
	Done      bool
	CreatedAt time.Time
	DoneAt    *time.Time
}

Обратите внимание на *time.Time: это честный способ сказать «может отсутствовать».

Граница слоёв: домен не должен знать про database/sql

Самый частый архитектурный перекос новичка: «раз уж я подключил БД, пусть доменная логика возвращает sql.ErrNoRows». Это кажется удобным, но на самом деле это делает конкретный драйвер/пакет частью вашего публичного контракта. Go‑сообщество отдельно подчёркивает, что оборачивание и “проброс” низкоуровневых ошибок наружу может сломать абстракции: если вы даёте вызывающему коду возможность делать errors.Is(err, sql.ErrNoRows), вы как бы обещаете, что ваша реализация хранения всегда будет на database/sql и будет возвращать именно эту причину.

В Tasker правильнее держать “свои” доменные ошибки:

package domain

import "errors"

var ErrTaskNotFound = errors.New("task not found")

А адаптер БД уже сам решает, как маппить sql.ErrNoRows в domain.ErrTaskNotFound.

Практический шаг: интерфейс хранилища и SQL‑адаптер

Если у вас уже есть интерфейс Storage, то SQL‑версия — это просто ещё одна реализация. Важно не превращать её в «бог‑объект», а держать маленькие методы.

package app

import "context"

type Storage interface {
	Create(ctx context.Context, title string) (int, error)
	Get(ctx context.Context, id int) (Task, error)
	List(ctx context.Context) ([]Task, error)
	MarkDone(ctx context.Context, id int) error
}

И вот тут SQL ложится очень естественно: каждый метод — это 1–2 запроса.

Мини‑пример database/sql: соединение и таймаут

У новичков есть две крайности: либо не ставить таймаут вообще (“авось”), либо ставить 1 миллисекунду (“чтобы было быстро”). Практичнее начать с аккуратного context.WithTimeout на границе операции хранения.

package sqlstore

import (
	"context"
	"database/sql"
	"time"
)

func GetTask(ctx context.Context, db *sql.DB, id int) (Task, error) {
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()
	// дальше будет QueryRowContext + Scan
	return Task{}, nil
}

Смысл не в магическом числе 2, а в привычке: внешние ресурсы должны иметь ограничение по времени, иначе ваше приложение рано или поздно повиснет “навсегда”.

Что учить в SQL/DB после курса

Чтобы не утонуть, держите фокус на практическом минимуме для прикладного Go:

  • Таблицы и ключи (primary key, unique).
  • Индексы (зачем они и как влияют на поиск).
  • Транзакции (что они дают, хотя бы на уровне «всё или ничего»).
  • Миграции как дисциплина изменения схемы.
  • Базовая диагностика: умение прочитать медленный запрос и понять “почему” (индекс не используется, фильтр по неиндексируемому полю и т.п.).

Этого достаточно, чтобы Tasker на БД был реальным, а не учебным.

3. Docker: воспроизводимая упаковка и запуск

Docker часто продают как «магическую коробку», но полезнее думать о нём как о стандартизированном способе описать окружение, чтобы ваш Tasker запускался одинаково у вас, у коллеги и на сервере. Это не про “модно”, а про “воспроизводимо”: одна и та же версия Go, те же зависимости, те же команды сборки, те же переменные окружения.

Есть мягкая ирония: многие начинают изучать Docker, чтобы «не разбираться в окружении». А заканчивают тем, что разбираются в окружении лучше всех — просто потому, что Docker заставляет описывать его явно.

Минимальная цель: статичный бинарь в контейнере

Tasker — идеальный кандидат: один бинарник, конфиг через env, никакого Node.js‑зоопарка.

Простейший Dockerfile (в “двух стадиях”, чтобы финальный образ был маленьким) может выглядеть так:

FROM golang:1.25 AS build
WORKDIR /src
COPY . .
RUN go test ./... && go build -o /bin/tasker ./cmd/tasker

FROM gcr.io/distroless/base-debian12
COPY --from=build /bin/tasker /tasker
ENTRYPOINT ["/tasker"]

Здесь есть важная привычка: тесты в сборке. Не потому что “так принято”, а потому что иначе вы однажды задеплоите нерабочий бинарь и будете уверять коллег, что «у меня работало».

Конфигурация через env: один бинарь, разные окружения

Docker особенно хорошо раскрывается, когда конфиг не зашит в код. Вы уже делали env‑конфиг в курсе, поэтому просто закрепим минимальный паттерн.

package config

import "os"

type Config struct {
	Addr string
	DSN  string
}

func Load() Config {
	return Config{Addr: env("TASKER_ADDR", ":8080"), DSN: env("TASKER_DSN", "")}
}

func env(key, def string) string {
	if v, ok := os.LookupEnv(key); ok && v != "" { return v }
	return def
}

Если DSN пустой — это может быть fail‑fast ошибкой запуска, потому что без него SQL‑хранилище не поднимется. И это хорошо: “сломаться сразу” обычно дешевле, чем “сломаться на третьем запросе”.

Что изучать дальше в Docker

После первого успешного запуска обычно всплывают взрослые вопросы:

  • как подключать БД рядом (docker compose);
  • как прокидывать порты;
  • как хранить данные (volumes);
  • как задавать healthcheck;
  • как собирать multi‑arch образы;
  • как не запускать процесс от root.

Но это уже «второй круг». На первом круге вам достаточно научиться упаковывать и запускать.

4. CI: делаем качество повторяемым

CI (Continuous Integration) новичкам кажется бюрократией: «зачем роботу запускать то, что я и так могу запустить локально?» На практике CI — это ваш способ сделать качество повторяемым. Локально вы можете забыть прогнать тесты или случайно не заметить предупреждение. CI не забывает. Он, конечно, тоже ломается, но хотя бы делает это одинаково для всех.

Подход из курса “quality gate” здесь превращается в привычку команды: форматирование, тесты, vet, линтеры — в одном конвейере. И вы перестаёте спорить “а надо ли запускать gofmt”, потому что спорить уже не с кем: робот молча не принимает PR.

Минимальный GitHub Actions workflow для Go

Ниже — компактный пример. Он не идеален “на все случаи”, но как стартовый шаблон хорош: формат, тесты, vet.

name: ci
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: "1.25.x" }
      - run: gofmt -w .
      - run: go test ./...
      - run: go vet ./...

Да, gofmt -w . в CI — спорная штука (обычно форматирование проверяют, а не правят). Но для учебного проекта это наглядно показывает идею: стиль кода — часть языка.

Контракт CI для Tasker: что именно должно быть зелёным

Суть CI не в количестве шагов, а в том, чтобы он отражал вашу “Definition of Done”. Для Tasker разумный минимум: проект собирается, тесты проходят, линтер не ругается на критичное, контракт CLI/HTTP не разваливается. И вот это уже “коммуникация качества”: вы не просто говорите “всё работает”, вы показываете, как вы это доказываете.

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

Оптимизация “на глаз” — это как лечить по фотографии: иногда угадаешь, но лучше бы не надо. Профилирование — это дисциплина, которая заставляет вас сначала измерить, потом изменить, потом снова измерить. В Go этот мир очень дружелюбный: у вас есть бенчмарки, pprof, трассировка. Главное — не воспринимать это как «магический шаманский бубен», а как нормальный инструмент наблюдения.

Очень важная психологическая победа новичка: понять, что медленно — это не “плохо написано”, а “есть узкое место”. Узкие места бывают даже у хорошего кода, потому что реальный мир — коварный.

Бенчмарки: измеряем конкретную функцию

Если в Tasker у вас есть, например, фильтрация задач по подстроке, вы можете измерить её на данных. Даже если потом вы никогда не будете это оптимизировать — вы хотя бы тренируете мышцу “измерять”.

package tasker

import "testing"

func BenchmarkContains(b *testing.B) {
	tasks := makeTestTasks(10_000)
	for i := 0; i < b.N; i++ {
		_ = filterByTitle(tasks, "read")
	}
}

Тут важно, что вы измеряете один кусок, а не “всё приложение целиком”.

pprof в сервисе: где время и память

Когда Tasker становится HTTP‑сервисом, вы хотите видеть: где CPU, где аллокации, где блокировки. Обычно в Go для этого подключают net/http/pprof на отдельный адрес/порт (или под защитой). В рамках “что учить дальше” достаточно знать, что так можно, и что это надо закрывать от внешнего мира.

Трассировка: почему запрос иногда долгий

Иногда проблема не в среднем времени, а в “иногда зависает”. Тут полезны инструменты трассировки. В Go есть встроенная экосистема для анализа трасс, и в документации подчёркивается, что трассировка помогает диагностировать таймауты и «редкие» задержки, если у вас есть способ поймать нужный интервал.

Даже если вы не будете сразу внедрять сложные схемы, важно усвоить практическую мысль: профилирование — это не только “ускорить”, это ещё и “объяснить поведение”.

6. Безопасность: базовый минимум для прикладной разработки

Безопасность для начинающего звучит как тема для спецслужб, но в прикладной разработке она начинается с простых вещей: ограничения ввода, корректные сообщения об ошибках, отсутствие утечек секретов, защита файловых путей, таймауты, принцип минимальных прав. Это не “дополнительные фичи”, это часть качества, потому что взлом и потеря данных — тоже баг, просто с особенно неприятным отчётом.

Здесь полезно помнить: большинство инцидентов происходит не из‑за голливудских хакеров, а из‑за «неожиданного ввода» и «мы забыли закрыть наружу отладочный эндпоинт».

Валидация и лимиты: защищаемся от большого запроса

Если у вас HTTP‑API, вы уже знаете идею MaxBytesReader и строгого decode. Даже в CLI есть похожая мысль: не доверять входу. Например, ID задачи должен быть положительным, а не “-9999999999”.

package app

import (
	"errors"
	"strconv"
)

func parseID(s string) (int, error) {
	id, err := strconv.Atoi(s)
	if err != nil || id <= 0 { return 0, errors.New("id must be positive int") }
	return id, nil
}

Это не “идеальная безопасность”, но это честная граница: плохой ввод не должен проваливаться внутрь.

Секреты: не логировать и не хранить в репозитории

Секреты — это API‑ключи, пароли, токены. Их базовое правило скучное, но спасительное: секреты приходят через env/секрет‑хранилища, и вы никогда не печатаете их в логах. Даже в debug. Особенно в debug.

package config

import (
	"errors"
	"os"
)

func MustAPIKey() (string, error) {
	key, ok := os.LookupEnv("TASKER_API_KEY")
	if !ok || key == "" { return "", errors.New("TASKER_API_KEY is not set") }
	return key, nil
}

Сообщение об ошибке должно помочь настроить систему, но не раскрывать сам ключ.

Ошибки: не отдавать внутренности наружу

Вы уже делали это в курсе и в CLI, и в HTTP: внутренние детали идут в логи, наружу — стабильное сообщение/контракт. Это не только про UX, но и про безопасность: текст ошибки иногда содержит путь к файлу, DSN, внутренние структуры, и это всё лишнее для клиента.

И здесь снова полезно помнить мысль про абстракции ошибок: если вы пробрасываете наружу низкоуровневые детали, вы случайно обещаете клиенту “слишком много”, а потом это трудно менять.

Что учить в безопасности дальше

После курса хорошая “дорожная карта” по безопасности выглядит так:

  • OWASP Top 10 на уровне понимания терминов.
  • Безопасная работа с конфигурацией и секретами.
  • Основы авторизации (API key/JWT как концепты).
  • Безопасная работа с файлами и путями.
  • Базовые практики логирования (не писать PII, request_id, минимум шума).

Это уже даст вам уровень «не стреляю себе в ногу каждый второй день».

7. Типичные ошибки после курса

Ошибка №1: “Сначала выучу Docker, потом базы, потом тесты” — и ничего не делается.
Так происходит, потому что направления кажутся большими, и мозг пытается выбрать “самое правильное”. Практичнее идти от потребности проекта: упёрлись в хранение — добавили SQL; устали от “у меня работает” — добавили CI; захотели одинаковый запуск — добавили Docker.

Ошибка №2: Протаскивать database/sql и sql.ErrNoRows в доменный слой.
Это выглядит удобно в моменте, но вы платите архитектурой: домен начинает зависеть от деталей хранения. В Go‑подходе часто подчёркивается, что оборачивание и проброс низкоуровневой ошибки делает её частью API, а значит усложняет смену реализации.

Ошибка №3: Считать, что профилирование нужно “когда будет медленно”.
На практике оно нужно, когда вы хотите не спорить, а знать. Причём “медленно” может быть не про CPU, а про блокировки, ожидания I/O, GC. Трассировка и профили — это не роскошь, а способ объяснить, что происходит, особенно в редких зависаниях.

Ошибка №4: Хранить секреты в репозитории “временно, для теста”.
Временные секреты обычно живут дольше всего, потому что “потом уберу” — это самая надёжная ложь в программировании. Привычка должна быть противоположной: секреты только через env/secret store, а приложение падает при старте, если секрет не задан.

Ошибка №5: Делать CI, который “всегда зелёный”, потому что он ничего не проверяет.
Это происходит, когда хочется быстро “поставить галочку”. Настоящий CI должен проверять то, чему вы хотите доверять: тесты, vet, минимальные линтеры, сборку. Иначе он превращается в декоративную гирлянду: красиво светится, но не греет.

1
Задача
Go SELF, 72 уровень, 2 лекция
Недоступна
Безопасная конфигурация
Безопасная конфигурация
1
Задача
Go SELF, 72 уровень, 2 лекция
Недоступна
Стабильная ошибка
Стабильная ошибка
1
Задача
Go SELF, 72 уровень, 2 лекция
Недоступна
Фильтр задач
Фильтр задач
1
Задача
Go SELF, 72 уровень, 2 лекция
Недоступна
Сборка проекта
Сборка проекта
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ