Go: SQL

Теория: Работа с базой данных

Работа с базой в Go начинается с подготовки подключения. Программа вызывает sql.Open(), чтобы создать объект, который управляет пулом соединений. На этом этапе никакого реального соединения с базой ещё нет. Приложение как будто знакомится с администратором базы и получает доступ к инструментам, через которые позже установит связь. Такой подход позволяет заранее описать правила работы пула и подготовить приложение к нагрузке, прежде чем оно попробует выполнить первый запрос.

package main

import (
	"database/sql"
	"log"

	_ " github.com/jackc/pgx/v5" // регистрируем драйвер PostgreSQL
)

func main() {
	// DSN: параметры подключения. Здесь пока только описание, реального соединения нет.
	dsn := "host=localhost port=5432 user=app password=secret dbname=shop sslmode=disable"

	// sql.Open создаёт объект *sql.DB и настраивает работу пула.
	// Соединение НЕ устанавливается прямо сейчас.
	db, err := sql.Open("pgx", dsn)
	if err != nil {
		log.Fatal(err)
	}

	defer db.Close() // закрываем пул при завершении программы

	// На этом этапе база ещё не пингуется и не проверяется.
}

Ping и когда соединение становится реальным

После создания пула программе нужно убедиться, что база отвечает. Это делает метод Ping() или его контекстная версия PingContext(). В этот момент устанавливается первое реальное соединение: драйвер отправляет короткий запрос, и база подтверждает, что доступна. Такой шаг помогает обнаружить ошибку раньше, чем приложение начнёт выполнять рабочие операции. Если база недоступна, сервис остановится сразу, а не в середине запроса.

package main

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

	_ "github.com/jackc/pgx/v5/stdlib"
)

func main() {
	dsn := "host=localhost port=5432 user=app password=secret dbname=shop sslmode=disable"
	db, err := sql.Open("pgx", dsn)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Создаём контекст с таймаутом. Если база "зависла", приложение не будет ждать бесконечно.
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	// Именно здесь устанавливается реальное соединение с базой.
	if err := db.PingContext(ctx); err != nil {
		log.Fatal("database unreachable:", err)
	}

	// Если программа дошла до этой точки — подключение работает.
}

После проверки подключения приложение получает гарантию, что база отвечает, и теперь может настраивать работу пула под свои задачи.

Пул соединений работает как очередь из заранее подготовленных подключений. Приложение берёт из этой очереди свободное соединение, выполняет запрос и возвращает его обратно. Такой подход снижает задержки, потому что нет необходимости заново устанавливать соединение каждый раз. Когда в системе много запросов, настройки пула определяют, справится ли приложение с нагрузкой без задержек и ошибок.

// Настраиваем пул соединений.
// Эти параметры влияют на то, как приложение поведёт себя под нагрузкой.

// Максимальное число одновременных соединений.
// Если запросов больше, лишние будут ждать свободный слот.
db.SetMaxOpenConns(10)

// Сколько соединений можно держать "в простое".
// Полезно, когда нагрузка скачет: свободные соединения не закрываются сразу.
db.SetMaxIdleConns(5)

// Максимальное время жизни одного соединения.
// Старые соединения будут обновляться — это защищает от зависших сессий.
db.SetConnMaxLifetime(30 * time.Minute)

Exec для изменения данных

Когда программе нужно изменить данные в таблице, она вызывает метод Exec(). Этот метод подходит для операций, которые не возвращают строк результата: вставки, обновления и удаления. Приложение передаёт SQL-запрос с плейсхолдерами, значения для них и получает объект, из которого можно узнать количество изменённых строк.

ctx := context.Background()

// Добавляем запись. Значения подставляются через плейсхолдеры.
res, err := db.ExecContext(ctx,
    `INSERT INTO products(name, price) VALUES($1, $2)`,
    "Ноутбук", 120000,
)
if err != nil {
	log.Fatal(err)
}

// Узнаём, сколько строк изменилось.
rows, _ := res.RowsAffected()
// При insert это почти всегда 1.

// Обновляем цену товара.
_, err = db.ExecContext(ctx,
    `UPDATE products SET price = $1 WHERE name = $2`,
    110000, "Ноутбук",
)
if err != nil {
	log.Fatal(err)
}

// Удаляем товар.
_, err = db.ExecContext(ctx,
    `DELETE FROM products WHERE name = $1`,
    "Ноутбук",
)
if err != nil {
	log.Fatal(err)
}

QueryRow и Scan при выборе одной строки

Когда запрос должен вернуть только одну строку, программа использует QueryRow() или QueryRowContext(). Этот метод сразу готов к считыванию результата: приложение вызывает Scan() и получает значения в переменные. Ошибка появляется именно на этапе Scan(), потому что до этого драйвер ещё не читает строку.

ctx := context.Background()

// Получаем количество записей в таблице.
var count int
err := db.QueryRowContext(ctx,
    `SELECT COUNT(*) FROM products`,
).Scan(&count)
if err != nil {
	log.Fatal(err)
}

// Получаем конкретный товар по id.
var name string
var price int

err = db.QueryRowContext(ctx,
    `SELECT name, price FROM products WHERE id = $1`,
    1,
).Scan(&name, &price)

// sql.ErrNoRows появляется, если строки нет.
if err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        // строки нет — обрабатываем «пустой результат»
        return nil, fmt.Errorf("не найдено: %w", err)
    }
    // любая другая ошибка
    return nil, fmt.Errorf("ошибка запроса: %w", err)
}

Query при получении нескольких строк

Когда запрос возвращает несколько строк, приложение использует Query() или QueryContext(). Эти методы открывают поток результатов: программа читает строки по одной через Next(), считывает данные вызовом Scan() и возвращает соединение в пул, когда вызывает rows.Close().

ctx := context.Background()

type Product struct {
	ID    int
	Name  string
	Price int
}

// Делаем выборку всех товаров.
rows, err := db.QueryContext(ctx,
    `SELECT id, name, price FROM products ORDER BY id`,
)
if err != nil {
	log.Fatal(err)
}
defer rows.Close() // соединение освобождается после закрытия

var products []Product

for rows.Next() {
	var p Product

	// Считываем очередную строку в структуру.
	if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
		log.Fatal(err)
	}
	products = append(products, p)
}
// Проверяем ошибку чтения.
if err := rows.Err(); err != nil {
	log.Fatal(err)
}

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

+7 800 100 22 47

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

+7 495 085 21 62

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

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