Go: SQL

Теория: Зачем в приложении появляется SQL

SQL появляется в Go-приложениях в тот момент, когда данных становится много, и ими нужно управлять не вручную, а через предсказуемый язык запросов. Авторизация, создание заказа, отчёты, фоновые задачи — всё это опирается на выборку строк, обновления и связи между сущностями. SQL даёт приложению общий способ описывать данные, искать их и менять. Без него данные быстро превращаются в набор разрозненных фрагментов, с которыми невозможно работать.

-- проверяем, есть ли пользователь с указанным email
SELECT id, email, password_hash
FROM users
WHERE email = $1;

Как движется запрос внутри сервиса

Когда клиент отправляет запрос, приложение проходит один и тот же путь. Браузер передаёт параметры, сервер принимает их и формирует SQL-запрос. База выбирает строки и отдаёт результат, а программа превращает его в структуры Go и собирает ответ. Такая цепочка повторяется в любом сервисе — от простого ручного эндпоинта до распределённой системы.

// обработчик читает параметр, выполняет SQL и возвращает JSON
email := r.URL.Query().Get("email")

var u User
	err := db.QueryRowContext(
		ctx,
		`SELECT id, email, name FROM users WHERE email = $1`,
	email,
).Scan(&u.ID, &u.Email, &u.Name)
if err != nil {
	http.Error(w, "not found", 404)
	return
}

json.NewEncoder(w).Encode(u)

SQL как общий принцип работы разных сервисов

Go используют для HTTP-API, фоновых задач, микросервисов и cron-воркеров, но способ общения с данными везде один и тот же. Сервис принимает параметры, выполняет SQL-операцию и продолжает работу: возвращает JSON, обновляет запись или передаёт данные далее по цепочке. Даже если один компонент работает на PostgreSQL, второй на MySQL, а третий пишет вспомогательные данные в SQLite, принцип остаётся одинаковым — SQL соединяет код и хранилище единым интерфейсом.

Фоновая задача, которая архивирует старые заказы:

// cron-воркер раз в сутки архивирует устаревшие заказы
_, err := db.ExecContext(ctx, `
    UPDATE orders
       SET status = 'archived'
     WHERE updated_at < NOW() - INTERVAL '30 days'
`)
if err != nil {
	log.Println("cron update failed:", err)
}

Отдельные хранилища и временные таблицы

В реальном приложении SQL появляется не только рядом с основной базой. Часто отдельные задачи удобнее вынести в независимые хранилища — например, хранить метрики в локальной SQLite или складывать логи в лёгкую файловую базу. Такой подход снижает нагрузку на основную СУБД и позволяет выполнять вспомогательные операции автономно. SQL остаётся тем же самым: запросы, параметры, чтение результатов.

Иногда сервису нужно собрать данные из разных источников или выполнить расчёт, который проще выразить SQL-агрегацией. Для таких задач база создаёт временные таблицы, обрабатывает данные локально рядом с индексами и удаляет временные структуры после вычисления. Это быстрее и надёжнее, чем переносить большие объёмы данных в приложение и обрабатывать их в памяти.

SQLite как отдельное хранилище метрик

// локальная SQLite-база для хранения метрик
dbMetrics, _ := sql.Open("sqlite", "metrics.db")

_, _ = dbMetrics.Exec(`
    CREATE TABLE IF NOT EXISTS metrics(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        event TEXT,
        value INTEGER,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
`)

// запись метрики
_, _ = dbMetrics.Exec(
    `INSERT INTO metrics(event, value) VALUES(?, ?)`,
    "cache.hit", 1,
)

Временная таблица для агрегаций

// временная таблица для дневной статистики
_, _ = db.ExecContext(ctx, `
    CREATE TEMPORARY TABLE daily AS
    SELECT user_id, SUM(amount) AS total
    FROM payments
    WHERE created_at::DATE = CURRENT_DATE
    GROUP BY user_id
`)
// чтение результата
rows, _ := db.QueryContext(ctx, `SELECT user_id, total FROM daily`)
defer rows.Close()

SQL как общий язык для любых СУБД

Когда приложение работает с разными хранилищами — PostgreSQL, MySQL, SQLite — SQL остаётся единым способом описывать операции с данными. Отличается только драйвер и формат плейсхолдеров, но сама схема работы не меняется: приложение формирует запрос, передаёт параметры, получает результат и использует его в логике. Такой единый подход делает код предсказуемым, а перенос между базами — почти бесшовным.

Даже если сервис хранит логи в SQLite, данные — в PostgreSQL, а временные таблицы создаёт в памяти, интерфейс остаётся одним и тем же. QueryRowContext() возвращает одну строку, QueryContext() отдаёт поток строк, ExecContext() выполняет команды без результата. Это позволяет строить слой данных так, чтобы он одинаково работал на любой СУБД.

// тот же код, но для SQLite
dbLogs, _ := sql.Open("sqlite", "logs.db")

var count int
_ = dbLogs.QueryRowContext(
    ctx,
    `SELECT COUNT(*) FROM logs WHERE level = ?`,
    "error",
).Scan(&count)

log.Println("errors today:", count)

database/sql как общий интерфейс над драйверами

Внутри Go есть слой, который делает работу с разными СУБД единообразной. Это пакет database/sql. Он не умеет подключаться к PostgreSQL или MySQL сам по себе — он задаёт правила. Драйверы реализуют эти правила под конкретную базу, а приложение использует один и тот же набор функций: Open(), Query(), Exec(), Prepare(), BeginTx(). Благодаря этому логика не зависит от выбора хранилища: SQL остаётся SQL, а детали подключения берёт на себя драйвер. Такой подход делает слой данных переносимым и понятным, даже если под капотом используется несколько разных СУБД.

import (
    "database/sql"
    _ "github.com/lib/pq"            // драйвер PostgreSQL
    _ "github.com/go-sql-driver/mysql" // драйвер MySQL
)

func probe(db *sql.DB) {
    var n int
    _ = db.QueryRow(`SELECT 1`).Scan(&n)
}
dbPG, _ := sql.Open("postgres", pgDSN)
dbMy, _ := sql.Open("mysql", myDSN)
probe(dbPG) // тот же код
probe(dbMy) // тот же код

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845