Описание пакета database языка программирования Go
Пакет для работы с базами данных
Пакет sql предоставляет общий интерфейс для баз данных SQL (или SQL-подобных).
Пакет sql должен использоваться вместе с драйвером базы данных. Список драйверов см. на сайте https://golang.org/s/sqldrivers.
Драйверы, не поддерживающие отмену контекста, вернут результат только после завершения запроса.
Примеры использования приведены на вики-странице https://golang.org/s/sqlwiki.
Пакет driver определяет интерфейсы, которые должны быть реализованы драйверами баз данных, используемыми пакетом sql.
Большая часть кода должна использовать пакет database/sql.
Подробная документация по драйверам: https://pkg.go.dev/database/sql/driver
1 - Работа с пакетом database/sql в Go: ошибки и их обработка
Переменные в пакете database/sql с примерами
Пакет database/sql
в Go предоставляет универсальный интерфейс для работы с SQL-базами данных. Рассмотрим основные ошибки, которые могут возникнуть при работе с этим пакетом, и как их правильно обрабатывать.
Основные ошибки пакета database/sql
1. ErrConnDone - соединение уже закрыто
var ErrConnDone = errors.New("sql: connection is already closed")
Эта ошибка возникает, когда вы пытаетесь выполнить операцию на соединении, которое уже было возвращено в пул соединений.
Пример:
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
// Возвращаем соединение в пул
conn.Close()
// Пытаемся использовать закрытое соединение
err = conn.PingContext(context.Background())
if errors.Is(err, sql.ErrConnDone) {
fmt.Println("Ошибка: соединение уже закрыто")
}
2. ErrNoRows - нет строк в результате
var ErrNoRows = errors.New("sql: no rows in result set")
Эта ошибка возвращается методом Scan
, когда QueryRow
не находит ни одной строки.
Правильная обработка:
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 123).Scan(&name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("Пользователь не найден")
} else {
log.Fatal(err)
}
} else {
fmt.Printf("Имя пользователя: %s\n", name)
}
3. ErrTxDone - транзакция уже завершена
var ErrTxDone = errors.New("sql: transaction has already been committed or rolled back")
Эта ошибка возникает при попытке выполнить операцию в уже завершенной транзакции.
Пример:
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// Фиксируем транзакцию
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
// Пытаемся выполнить запрос в завершенной транзакции
_, err = tx.Exec("INSERT INTO users(name) VALUES (?)", "Alice")
if errors.Is(err, sql.ErrTxDone) {
fmt.Println("Ошибка: транзакция уже завершена")
}
Советы по работе с database/sql
- Всегда проверяйте ошибки после операций с базой данных.
- Используйте
errors.Is
для проверки конкретных ошибок пакета database/sql.
- Закрывайте ресурсы (соединения, транзакции, результаты запросов) с помощью defer.
- Используйте контексты для управления таймаутами и отменой операций.
Полный пример работы с базой данных:
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
ID int
Name string
}
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверка соединения
err = db.Ping()
if err != nil {
log.Fatal(err)
}
// Пример запроса с обработкой ErrNoRows
user, err := getUser(db, 123)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("Пользователь не найден")
} else {
log.Fatal(err)
}
} else {
fmt.Printf("Найден пользователь: %+v\n", user)
}
// Пример транзакции
err = transferMoney(db, 1, 2, 100)
if err != nil {
log.Fatal(err)
}
}
func getUser(db *sql.DB, id int) (*User, error) {
var user User
err := db.QueryRow("SELECT id, name FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name)
if err != nil {
return nil, err
}
return &user, nil
}
func transferMoney(db *sql.DB, from, to, amount int) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
}()
// Списание денег
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
// Зачисление денег
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
if err != nil {
return err
}
return nil
}
2 - Подробное описание функций пакета database/sql в Go
Пакет database/sql предоставляет универсальный интерфейс для работы с SQL-базами данных. Рассмотрим основные функции этого пакета с примерами использования.
Основные функции управления драйверами
func Drivers() []string
Эта функция возвращает отсортированный список имен зарегистрированных драйверов баз данных.
Пример использования:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql" // регистрируем MySQL драйвер
_ "github.com/lib/pq" // регистрируем PostgreSQL драйвер
)
func main() {
// Получаем список зарегистрированных драйверов
drivers := sql.Drivers()
fmt.Println("Зарегистрированные драйверы:")
for _, driver := range drivers {
fmt.Println("-", driver)
}
// Выведет что-то вроде:
// Зарегистрированные драйверы:
// - mysql
// - postgres
}
func Register(name string, driver driver.Driver)
Эта функция регистрирует драйвер базы данных под указанным именем. Если попытаться зарегистрировать два драйвера с одинаковым именем или передать nil, функция вызовет panic.
Обычно драйверы регистрируют себя автоматически при импорте с пустым идентификатором _
, как в примере выше. Но можно регистрировать драйверы и вручную:
package main
import (
"database/sql"
"database/sql/driver"
"fmt"
)
// Простой mock-драйвер для примера
type mockDriver struct{}
func (d *mockDriver) Open(name string) (driver.Conn, error) {
fmt.Println("Mock driver открывает соединение с:", name)
return nil, nil
}
func main() {
// Регистрируем наш mock-драйвер
sql.Register("mock", &mockDriver{})
// Теперь можем его использовать
db, err := sql.Open("mock", "test-connection")
if err != nil {
fmt.Println("Ошибка:", err)
return
}
defer db.Close()
// Проверяем, что драйвер зарегистрирован
fmt.Println("Зарегистрированные драйверы:", sql.Drivers())
}
Основные функции для работы с БД
func Open(driverName, dataSourceName string) (*DB, error)
Открывает новое соединение с базой данных. На самом деле не устанавливает соединение сразу, а только готовит объект DB.
Пример:
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
func (*DB) Ping() error
и func (*DB) PingContext(ctx context.Context) error
Проверяет, что соединение с БД живо и доступно.
Пример:
err = db.Ping()
if err != nil {
log.Fatal("Не удалось подключиться к БД:", err)
}
fmt.Println("Успешное подключение к БД!")
Функции выполнения запросов
func (*DB) Exec(query string, args ...interface{}) (Result, error)
func (*DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error)
Выполняет запрос без возврата строк (INSERT, UPDATE, DELETE).
Пример:
result, err := db.Exec(
"INSERT INTO users(name, age) VALUES (?, ?)",
"Alice",
30,
)
if err != nil {
log.Fatal(err)
}
lastID, err := result.LastInsertId()
rowsAffected, err := result.RowsAffected()
func (*DB) Query(query string, args ...interface{}) (*Rows, error)
func (*DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)
Выполняет запрос, возвращающий строки (SELECT).
Пример:
rows, err := db.Query("SELECT id, name, age FROM users WHERE age > ?", 25)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
var age int
err = rows.Scan(&id, &name, &age)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s, %d\n", id, name, age)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
func (*DB) QueryRow(query string, args ...interface{}) *Row
func (*DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row
Выполняет запрос, который должен вернуть не более одной строки.
Пример:
var name string
var age int
err := db.QueryRow("SELECT name, age FROM users WHERE id = ?", 1).Scan(&name, &age)
if err != nil {
if err == sql.ErrNoRows {
fmt.Println("Пользователь не найден")
} else {
log.Fatal(err)
}
} else {
fmt.Printf("Имя: %s, Возраст: %d\n", name, age)
}
Функции для работы с транзакциями
func (*DB) Begin() (*Tx, error)
func (*DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)
Начинает новую транзакцию.
Пример:
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// Откатываем транзакцию в случае ошибки
defer func() {
if err != nil {
tx.Rollback()
}
}()
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
return err
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err != nil {
return err
}
// Если все успешно - коммитим
err = tx.Commit()
if err != nil {
return err
}
Работа с соединениями
func (*DB) Conn(ctx context.Context) (*Conn, error)
Получает одно соединение из пула для выполнения нескольких операций в одном контексте.
Пример:
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Выполняем несколько операций в одном соединении
var count int
err = conn.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
log.Fatal(err)
}
_, err = conn.ExecContext(context.Background(), "UPDATE stats SET user_count = ?", count)
if err != nil {
log.Fatal(err)
}
Заключение
Пакет database/sql
предоставляет все необходимые функции для работы с SQL-базами данных в Go. Основные принципы:
- Всегда проверяйте ошибки
- Закрывайте ресурсы (Rows, Tx, Conn) с помощью defer
- Используйте контексты для управления таймаутами
- Для разных типов запросов используйте соответствующие методы (Exec, Query, QueryRow)
Правильное использование этих функций позволит вам создавать надежные и эффективные приложения, работающие с базами данных.
3 - Контекст (context) в транзакциях database/sql
Контекст (context.Context) - это механизм в Go для управления временем жизни операций, отмены и передачи значений между вызовами функций. В работе с базой данных через database/sql контекст играет ключевую роль.
Основные цели использования контекста в транзакциях
- Отмена операций - можно прервать долгий запрос
- Таймауты - установка максимального времени выполнения
- Распространение значений - передача метаданных через цепочку вызовов
Методы с поддержкой контекста
В database/sql
большинство операций имеют две версии:
- Обычная (
Begin
, Exec
, Query
и т.д.)
- С контекстом (
BeginTx
, ExecContext
, QueryContext
и т.д.)
Пример использования контекста с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Начинаем транзакцию с таймаутом
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
defer tx.Rollback() // Безопасный откат при ошибках
// Выполняем запрос в транзакции
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
// Если контекст истек, получим context.DeadlineExceeded
log.Printf("Update failed: %v", err)
return
}
// Коммитим транзакцию
err = tx.Commit()
if err != nil {
log.Printf("Commit failed: %v", err)
}
Особенности работы контекста в транзакциях
- Каскадная отмена - отмена контекста прерывает все операции в транзакции
- Изоляция транзакций - контекст не влияет на другие транзакции
- Ресурсы - отмена не освобождает соединение автоматически
Практические сценарии использования
- HTTP-обработчики - привязка к времени жизни запроса:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tx, err := db.BeginTx(ctx, nil)
// ...
}
- Долгие отчеты - возможность отмены:
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(10*time.Second)
cancel() // Принудительная отмена через 10 сек
}()
rows, err := db.QueryContext(ctx, "SELECT * FROM big_table")
- Распределенные транзакции - передача идентификаторов:
ctx := context.WithValue(context.Background(), "txID", generateID())
tx, _ := db.BeginTx(ctx, nil)
Важные нюансы
- Всегда проверяйте ошибки на
context.Canceled
и context.DeadlineExceeded
- Освобождайте ресурсы с помощью
defer
даже при отмене контекста
- Не передавайте один контекст в несколько независимых транзакций
Использование контекста делает ваши транзакции более управляемыми и устойчивыми к долгим операциям.
4 - Описание типа database/sql DB
DB - это хэндл базы данных, представляющий пул из нуля или более базовых соединений. Он безопасен для одновременного использования несколькими горутинами.
type DB struct {
// содержит отфильтрованные или неэкспонированные поля
}
Пакет sql создает и освобождает соединения автоматически; он также поддерживает свободный пул незадействованных соединений. Если в базе данных есть понятие состояния каждого соединения, то такое состояние можно надежно наблюдать в рамках транзакции (Tx) или соединения (Conn). После вызова DB.Begin возвращаемая Tx привязывается к одному соединению. После вызова Tx.Commit или Tx.Rollback для транзакции, соединение этой транзакции возвращается в пул незанятых соединений DB. Размер пула можно контролировать с помощью DB.SetMaxIdleConns.
func Open
func Open(driverName, dataSourceName string) (*DB, error)
Open открывает базу данных, указанную именем драйвера базы данных и именем источника данных, обычно состоящим как минимум из имени базы данных и информации о подключении.
Большинство пользователей открывают базу данных с помощью специфической для драйвера вспомогательной функции подключения, которая возвращает *DB. Драйверы баз данных не включены в стандартную библиотеку Go. Список драйверов сторонних разработчиков см. на https://golang.org/s/sqldrivers.
Open может просто проверить свои аргументы, не создавая соединения с базой данных. Чтобы убедиться, что имя источника данных действительно, вызовите DB.Ping.
Возвращаемая БД безопасна для одновременного использования несколькими горутинами и поддерживает собственный пул незанятых соединений. Таким образом, функцию Open следует вызывать только один раз. Редко возникает необходимость закрывать БД.
func OpenDB
func OpenDB(c driver.Connector) *DB
OpenDB открывает базу данных с помощью driver.Connector, позволяя драйверам обходить строковое имя источника данных.
Большинство пользователей открывают базу данных с помощью специфической для драйвера функции-помощника подключения, которая возвращает *DB. Драйверы баз данных не включены в стандартную библиотеку Go.
OpenDB может просто проверить свои аргументы, не создавая соединения с базой данных. Чтобы убедиться, что имя источника данных действительно, вызовите DB.Ping.
Возвращаемая БД безопасна для одновременного использования несколькими горутинами и поддерживает свой собственный пул незанятых соединений. Таким образом, функция OpenDB должна быть вызвана только один раз. Редко возникает необходимость закрывать БД.
func (*DB) Close
func (db *DB) Close() error
Close закрывает базу данных и предотвращает запуск новых запросов. Затем Close ожидает завершения всех запросов, которые начали обрабатываться на сервере.
Закрывать БД приходится редко, так как хэндл БД должен быть долгоживущим и использоваться совместно многими горутинами.
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close() // Всегда закрывайте соединение
func (*DB) Ping
func (db *DB) Ping() error
Ping проверяет, что соединение с базой данных еще живо, и при необходимости устанавливает соединение.
Внутри Ping использует context.Background; чтобы указать контекст, используйте DB.PingContext.
err = db.Ping()
if err != nil {
log.Fatal("Connection failed:", err)
}
func (*DB) PingContext
func (db *DB) PingContext(ctx context.Context) error
PingContext проверяет, что соединение с базой данных еще живо, и при необходимости устанавливает соединение.
Пример
package main
import (
"context"
"database/sql"
"log"
"time"
)
var (
ctx context.Context
db *sql.DB
)
func main() {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
status := "up"
if err := db.PingContext(ctx); err != nil {
status = "down"
}
log.Println(status)
}
func (*DB) Begin
func (db *DB) Begin() (*Tx, error)
Begin начинает транзакцию. Уровень изоляции по умолчанию зависит от драйвера.
Begin внутренне использует context.Background; чтобы указать контекст, используйте DB.BeginTx.
func (*DB) BeginTx
func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)
BeginTx начинает транзакцию.
Предоставленный контекст используется до тех пор, пока транзакция не будет зафиксирована или откачена. Если контекст будет отменен, пакет sql откатит транзакцию. Tx.Commit вернет ошибку, если контекст, предоставленный BeginTx, будет отменен.
Предоставленный параметр TxOptions является необязательным и может быть равен nil, если следует использовать значения по умолчанию. Если используется уровень изоляции не по умолчанию, который драйвер не поддерживает, будет возвращена ошибка.
Пример
import (
"context"
"database/sql"
"log"
)
var (
ctx context.Context
db *sql.DB
)
func main() {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal(err)
}
id := 37
_, execErr := tx.Exec(`UPDATE users SET status = ? WHERE id = ?`, "paid", id)
if execErr != nil {
_ = tx.Rollback()
log.Fatal(execErr)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
}
func (*DB) Conn
func (db *DB) Conn(ctx context.Context) (*Conn, error)
Conn возвращает одно соединение, либо открывая новое соединение, либо возвращая существующее соединение из пула соединений. Conn будет блокироваться до тех пор, пока не будет возвращено соединение или не будет отменен ctx. Запросы, выполняемые по одному и тому же Conn, будут выполняться в одной и той же сессии базы данных.
Каждый Conn должен быть возвращен в пул баз данных после использования вызовом Conn.Close.
Подробнее о Conn
Работа с соединениями (Conn
) в database/sql
Соединение (Conn
) представляет собой одиночное подключение к базе данных, которое можно использовать как в уже открытом пуле соединений (DB
), так и для прямого управления подключением.
Основные концепции
-
Отличие от DB
:
DB
- это пул соединений (управляет множеством подключений)
Conn
- одно конкретное соединение
-
Когда использовать:
- Когда нужно выполнить несколько операций в рамках одного соединения
- Для работы с сессионными переменными
- Для транзакций, требующих изоляции от других операций
Примеры использования
1. Получение соединения из пула
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Получаем отдельное соединение из пула
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close() // Важно возвращать соединение в пул
// Используем соединение
var result int
err = conn.QueryRowContext(context.Background(), "SELECT 1 + 1").Scan(&result)
if err != nil {
log.Fatal(err)
}
fmt.Println("Result:", result)
}
2. Использование сессионных переменных
// Устанавливаем переменную сессии и используем её в запросе
err = conn.ExecContext(context.Background(), "SET @user_id = 123").Scan(&result)
if err != nil {
log.Fatal(err)
}
var userID int
err = conn.QueryRowContext(context.Background(), "SELECT @user_id").Scan(&userID)
fmt.Println("User ID:", userID) // Выведет: User ID: 123
3. Транзакции в отдельном соединении
// Начинаем транзакцию в этом соединении
tx, err := conn.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal(err)
}
// Выполняем операции в транзакции
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err != nil {
tx.Rollback()
log.Fatal(err)
} else {
tx.Commit()
}
4. Прямое подключение (без пула)
Для прямого подключения без пула можно использовать драйвер напрямую, но это не рекомендуется:
package main
import (
"database/sql"
"database/sql/driver"
"fmt"
"log"
)
func main() {
// Получаем драйвер по имени
driver := sql.Driver(new(mysql.MySQLDriver))
// Создаем прямое соединение (не рекомендуется для production)
conn, err := driver.Open("user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer conn.Close() // Закрываем соединение
// Преобразуем в тип Conn (если нужно)
if sqlConn, ok := conn.(driver.Conn); ok {
// Работаем с соединением...
}
}
Когда использовать Conn
вместо DB
?
- Сессионные переменные: Когда нужно сохранить состояние между запросами
- Временные таблицы: Которые видны только в текущем соединении
- LOCK TABLES: Когда нужно заблокировать таблицы на время сессии
- Точный контроль: Когда важно, чтобы несколько операций использовали одно соединение
Важные замечания
- Всегда закрывайте
Conn
с помощью defer conn.Close()
- Не держите соединения долго - возвращайте их в пул
- Для большинства случаев достаточно стандартного
DB
Conn
дороже в создании, чем операции через DB
Пример комплексного использования:
func complexOperation(db *sql.DB) error {
// Получаем соединение
conn, err := db.Conn(context.Background())
if err != nil {
return err
}
defer conn.Close()
// Настраиваем сессию
if _, err := conn.ExecContext(context.Background(), "SET @start_time = NOW()"); err != nil {
return err
}
// Выполняем транзакцию
tx, err := conn.BeginTx(context.Background(), nil)
if err != nil {
return err
}
// Бизнес-логика...
if _, err := tx.Exec("INSERT INTO orders (...) VALUES (...)"); err != nil {
tx.Rollback()
return err
}
// Получаем время выполнения
var startTime string
conn.QueryRowContext(context.Background(), "SELECT @start_time").Scan(&startTime)
fmt.Println("Operation started at:", startTime)
return tx.Commit()
}
Используйте Conn
осознанно, только когда действительно нужно контролировать конкретное соединение. В большинстве случаев достаточно стандартного DB
.
func (*DB) Driver
func (db *DB) Driver() driver.Driver
Driver возвращает базовый драйвер базы данных.
func (*DB) Exec
func (db *DB) Exec(query string, args ...any) (Result, error)
Exec выполняет запрос, не возвращая ни одной строки. В качестве args используются любые параметры-заполнители в запросе.
Exec использует внутренний контекст context.Background; чтобы указать контекст, используйте DB.ExecContext.
context.Background
func Background() Context
Background возвращает ненулевой, пустой Context. Он никогда не отменяется, не имеет значений и сроков. Обычно используется главной функцией, инициализацией и тестами, а также в качестве контекста верхнего уровня для входящих запросов.
func (*DB) ExecContext
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
ExecContext выполняет запрос, не возвращая ни одной строки. В качестве args используются любые параметры-заполнители в запросе.
func (*DB) Prepare
func (db *DB) Prepare(query string) (*Stmt, error)
Prepare создает подготовленный отчет для последующих запросов или выполнения. Из полученного отчета можно одновременно выполнять несколько запросов или операций. Вызывающая сторона должна вызвать метод *Stmt.Close
оператора, когда он больше не нужен.
Prepare внутренне использует context.Background; чтобы указать контекст, используйте DB.PrepareContext.
Пример
import (
"context"
"database/sql"
"log"
)
var db *sql.DB
func main() {
projects := []struct {
mascot string
release int
}{
{"tux", 1991},
{"duke", 1996},
{"gopher", 2009},
{"moby dock", 2013},
}
stmt, err := db.Prepare("INSERT INTO projects(id, mascot, release, category) VALUES( ?, ?, ?, ? )")
if err != nil {
log.Fatal(err)
}
defer stmt.Close() // Prepared statements take up server resources and should be closed after use.
for id, project := range projects {
if _, err := stmt.Exec(id+1, project.mascot, project.release, "open source"); err != nil {
log.Fatal(err)
}
}
}
func (*DB) PrepareContext
func (db *DB) PrepareContext(ctx context.Context, query string) (*Stmt, error)
PrepareContext создает подготовленный запрос для последующих запросов или выполнений. Из полученного отчета можно одновременно выполнять несколько запросов или операций. Вызывающая сторона должна вызвать метод *Stmt.Close
оператора, когда он больше не нужен.
Предоставленный контекст используется для подготовки утверждения, а не для его выполнения.
func (*DB) Query
func (db *DB) Query(query string, args ...any) (*Rows, error)
Query выполняет запрос, возвращающий строки, обычно SELECT. В качестве args используются любые параметры-заполнители в запросе.
Query внутренне использует context.Background; чтобы указать контекст, используйте DB.QueryContext.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/lib/pq" // или другой драйвер БД
)
var db *sql.DB
func main() {
// Инициализация подключения к БД
var err error
db, err = sql.Open("postgres", "user=postgres dbname=test sslmode=disable") // замените на свои параметры
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Проверка соединения
if err := db.Ping(); err != nil {
log.Fatal("Failed to ping database:", err)
}
age := 27
q := `
-- Создаем временную таблицу
CREATE TEMP TABLE uid (id bigint) ON COMMIT DROP;
-- Заполняем временную таблицу
INSERT INTO uid
SELECT id FROM users WHERE age < $1;
-- Первый набор результатов
SELECT
users.id, name
FROM
users
JOIN uid ON users.id = uid.id;
-- Второй набор результатов
SELECT
ur.user_id, ur.role_id
FROM
user_roles AS ur
JOIN uid ON uid.id = ur.user_id;
`
// Используем контекст для запроса
ctx := context.Background()
rows, err := db.QueryContext(ctx, q, age)
if err != nil {
log.Fatal("Query failed:", err)
}
defer rows.Close()
// Обрабатываем первый набор результатов
log.Println("Processing users:")
for rows.Next() {
var (
id int64
name string
)
if err := rows.Scan(&id, &name); err != nil {
log.Fatal("Failed to scan user row:", err)
}
log.Printf("id %d name is %s\n", id, name)
}
// Переходим ко второму набору результатов
if !rows.NextResultSet() {
if err := rows.Err(); err != nil {
log.Fatal("Failed to get next result set:", err)
}
log.Fatal("Expected more result sets, but none available")
}
// Обрабатываем второй набор результатов
roleMap := map[int64]string{
1: "user",
2: "admin",
3: "gopher",
}
log.Println("\nProcessing roles:")
for rows.Next() {
var (
userID int64
roleID int64
)
if err := rows.Scan(&userID, &roleID); err != nil {
log.Fatal("Failed to scan role row:", err)
}
roleName, ok := roleMap[roleID]
if !ok {
roleName = "unknown"
}
log.Printf("user %d has role %s\n", userID, roleName)
}
if err := rows.Err(); err != nil {
log.Fatal("Rows error:", err)
}
}
func (*DB) QueryContext
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
QueryContext выполняет запрос, возвращающий строки, обычно SELECT. В качестве args используются любые параметры-заполнители запроса.
Пример
package main
import (
"context"
"database/sql"
"fmt"
"log"
"strings"
_ "github.com/go-sql-driver/mysql" // или другой драйвер БД
)
func main() {
// 1. Инициализация подключения к БД
var err error
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// 2. Проверка соединения
ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
log.Fatal("Failed to ping database:", err)
}
// 3. Выполнение запроса
age := 27
rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)
if err != nil {
log.Fatal("Query failed:", err)
}
defer rows.Close()
// 4. Обработка результатов
names := make([]string, 0)
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
log.Printf("Scan error (skipping row): %v", err) // Не прерываем выполнение
continue
}
names = append(names, name)
}
// 5. Проверка ошибок после итерации
if err := rows.Err(); err != nil {
log.Fatal("Rows error:", err)
}
// 6. Проверка ошибок закрытия (более аккуратная обработка)
if err := rows.Close(); err != nil {
log.Printf("Warning: error closing rows: %v", err) // Не фатальная ошибка
}
// 7. Вывод результатов
if len(names) > 0 {
fmt.Printf("%s are %d years old\n", strings.Join(names, ", "), age)
} else {
fmt.Printf("No users found at age %d\n", age)
}
}
func (*DB) QueryRow
func (db *DB) QueryRow(query string, args ...any) *Row
QueryRow выполняет запрос, который, как ожидается, вернет не более одной строки. QueryRow всегда возвращает значение, не являющееся нулем. Ошибки откладываются до вызова метода Row’s Scan. Если запрос не выбрал ни одной строки, то *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
QueryRow внутренне использует context.Background;
func (*DB) QueryRowContext
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
QueryRowContext выполняет запрос, который, как ожидается, вернет не более одной строки. QueryRowContext всегда возвращает значение non-nil. Ошибки откладываются до вызова метода Row’s Scan. Если запрос не выбрал ни одной строки, то *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
Пример
package main
import (
"context"
"database/sql"
"log"
"time"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)
func main() {
// Инициализация подключения к БД
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
// Проверка соединения
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
// Выполнение запроса
id := 123
var username string
var created time.Time
// Используем контекст с таймаутом для запроса
queryCtx, queryCancel := context.WithTimeout(context.Background(), 3*time.Second)
defer queryCancel()
err = db.QueryRowContext(queryCtx,
"SELECT username, created_at FROM users WHERE id=?",
id,
).Scan(&username, &created)
switch {
case err == sql.ErrNoRows:
log.Printf("No user found with id %d", id)
case err != nil:
log.Printf("Query error: %v", err) // Не используем Fatalf чтобы не прерывать программу
default:
log.Printf(
"Username: %q, Account created on: %s",
username,
created.Format("2006-01-02 15:04:05"),
)
}
}
func (*DB) SetConnMaxIdleTime
func (db *DB) SetConnMaxIdleTime(d time.Duration)
SetConnMaxIdleTime устанавливает максимальное время, в течение которого соединение может простаивать.
Просроченные соединения могут быть лениво закрыты перед повторным использованием.
Если d <= 0, соединения не закрываются из-за времени простоя соединения.
func (*DB) SetConnMaxLifetime
func (db *DB) SetConnMaxLifetime(d time.Duration)
SetConnMaxLifetime устанавливает максимальное количество времени, в течение которого соединение может быть использовано повторно.
Просроченные соединения могут быть лениво закрыты перед повторным использованием.
Если d <= 0, соединения не закрываются из-за возраста соединения.
func (*DB) SetMaxIdleConns
func (db *DB) SetMaxIdleConns(n int)
SetMaxIdleConns устанавливает максимальное количество соединений в пуле незадействованных соединений.
Если MaxOpenConns больше 0, но меньше нового MaxIdleConns, то новый MaxIdleConns будет уменьшен, чтобы соответствовать лимиту MaxOpenConns.
Если n <= 0, простаивающие соединения не сохраняются.
По умолчанию максимальное количество незадействованных соединений в настоящее время равно 2. Это может измениться в будущем выпуске.
func (*DB) SetMaxOpenConns
func (db *DB) SetMaxOpenConns(n int)
SetMaxOpenConns устанавливает максимальное количество открытых соединений с базой данных.
Если MaxIdleConns больше 0, а новое значение MaxOpenConns меньше MaxIdleConns, то MaxIdleConns будет уменьшено, чтобы соответствовать новому ограничению MaxOpenConns.
Если n <= 0, то количество открытых соединений не ограничено. По умолчанию 0 (неограниченно).
func (*DB) Stats
func (db *DB) Stats() DBStats
Stats возвращает статистику базы данных.
5 - Описание типа database/sql ColumnType
ColumnType тип для управлени структурой базы данных
type ColumnType struct {
// содержит отфильтрованные или неэкспортированные поля
}
ColumnType содержит имя и тип колонки.
func (*ColumnType) DatabaseTypeName
func (ci *ColumnType) DatabaseTypeName() string
DatabaseTypeName возвращает системное имя базы данных типа столбца. Если возвращается пустая строка, значит, имя типа драйвером не поддерживается. Список типов данных драйвера см. в документации к драйверу.
Спецификаторы ColumnType.Length не учитываются. Общие имена типов включают “VARCHAR”, “TEXT”, “NVARCHAR”, “DECIMAL”, “BOOL”, “INT” и “BIGINT”.
func (*ColumnType) DecimalSize
func (ci *ColumnType) DecimalSize() (precision, scale int64, ok bool)
DecimalSize возвращает масштаб и точность десятичного типа. Если они не применяются или не поддерживаются, ok равно false.
func (*ColumnType) Length
func (ci *ColumnType) Length() (length int64, ok bool)
Length возвращает длину типа столбца для типов столбцов переменной длины, таких как текстовые и бинарные типы полей. Если длина типа не ограничена, то значение будет math.MaxInt64 (все ограничения базы данных будут действовать). Если тип столбца не переменной длины, например int, или если он не поддерживается драйвером, ok будет false.
func (*ColumnType) Name
func (ci *ColumnType) Name() string
Name возвращает имя или псевдоним колонки.
func (*ColumnType) Nullable
func (ci *ColumnType) Nullable() (nullable, ok bool)
Nullable сообщает, может ли столбец быть нулевым. Если драйвер не поддерживает это свойство, ok будет равно false.
func (*ColumnType) ScanType
func (ci *ColumnType) ScanType() reflect.Type
ScanType возвращает тип Go, подходящий для сканирования с помощью Rows.Scan. Если драйвер не поддерживает это свойство, ScanType вернет тип пустого интерфейса.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// Подключение к базе данных
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/testdb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверка соединения
if err := db.Ping(); err != nil {
log.Fatal(err)
}
// Выполнение запроса
rows, err := db.QueryContext(context.Background(),
"SELECT id, username, created_at, balance, is_active FROM users LIMIT 1")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Получение информации о столбцах
columnTypes, err := rows.ColumnTypes()
if err != nil {
log.Fatal(err)
}
// Анализ метаданных каждого столбца
for i, ct := range columnTypes {
fmt.Printf("\n--- Столбец %d: %s ---\n", i+1, ct.Name())
// Тип данных в БД
databaseType := ct.DatabaseTypeName()
fmt.Printf("Тип в БД: %s\n", databaseType)
// Nullable
nullable, ok := ct.Nullable()
if ok {
fmt.Printf("Может быть NULL: %v\n", nullable)
} else {
fmt.Println("Информация о NULL недоступна")
}
// Длина/размер
length, ok := ct.Length()
if ok {
fmt.Printf("Длина: %d\n", length)
}
// Точность и масштаб (для чисел)
precision, scale, ok := ct.DecimalSize()
if ok {
fmt.Printf("Точность: %d, Масштаб: %d\n", precision, scale)
}
// Тип сканирования в Go
scanType := ct.ScanType()
fmt.Printf("Тип в Go: %v\n", scanType)
}
// Пример использования информации о типах для динамического сканирования
var (
id int64
username string
createdAt time.Time
balance float64
isActive bool
)
if rows.Next() {
err = rows.Scan(&id, &username, &createdAt, &balance, &isActive)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nРеальные значения: %d, %s, %v, %.2f, %t\n",
id, username, createdAt, balance, isActive)
}
}
6 - Описание типа Conn database/sql
Conn представляет собой одно соединение с базой данных, а не пул соединений с базой данных.
type Conn struct {
// содержит отфильтрованные или неэкспонированные поля
}
Conn представляет собой одно соединение с базой данных, а не пул соединений с базой данных. Предпочтительнее запускать запросы из БД, если нет особой необходимости в постоянном соединении с одной базой данных.
Conn должен вызвать Conn.Close, чтобы вернуть соединение в пул баз данных, и может делать это одновременно с выполняющимся запросом.
После вызова Conn.Close все операции над соединением завершаются с ошибкой ErrConnDone.
func (*Conn) BeginTx
func (c *Conn) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)
BeginTx начинает транзакцию.
Предоставленный контекст используется до тех пор, пока транзакция не будет зафиксирована или откачена. Если контекст будет отменен, пакет sql откатит транзакцию. Tx.Commit вернет ошибку, если контекст, предоставленный BeginTx, будет отменен.
Предоставленный параметр TxOptions является необязательным и может быть равен nil, если следует использовать значения по умолчанию. Если используется уровень изоляции не по умолчанию, который драйвер не поддерживает, будет возвращена ошибка.
func (*Conn) Close
func (c *Conn) Close() error
Close возвращает соединение в пул соединений. Все операции после Close возвращаются с ошибкой ErrConnDone. Close безопасно вызывать одновременно с другими операциями, и он будет блокироваться до тех пор, пока все остальные операции не завершатся. Может оказаться полезным сначала отменить любой используемый контекст, а затем вызвать Close непосредственно после него.
func (*Conn) ExecContext
func (c *Conn) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
ExecContext выполняет запрос, не возвращая никаких строк. В качестве args используются любые параметры-заполнители в запросе.
Пример
package main
import (
"context"
"database/sql"
"log"
"os"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)
func main() {
// 1. Инициализация подключения к БД
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// 2. Проверка соединения
ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
log.Fatal("Failed to ping database:", err)
}
// 3. Получение выделенного соединения
conn, err := db.Conn(ctx)
if err != nil {
log.Fatal("Failed to get connection:", err)
}
defer func() {
if err := conn.Close(); err != nil {
log.Printf("Warning: error closing connection: %v", err)
}
}()
// 4. Выполнение транзакции
id := 41
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// 5. Выполнение UPDATE в транзакции
result, err := tx.ExecContext(ctx,
`UPDATE balances SET balance = balance + 10 WHERE user_id = ?`,
id,
)
if err != nil {
tx.Rollback()
log.Fatal("Failed to update balance:", err)
}
// 6. Проверка количества измененных строк
rows, err := result.RowsAffected()
if err != nil {
tx.Rollback()
log.Fatal("Failed to get rows affected:", err)
}
if rows != 1 {
tx.Rollback()
log.Printf("Expected single row affected, got %d rows affected", rows)
os.Exit(1) // Более мягкое завершение чем log.Fatal
}
// 7. Фиксация транзакции
if err := tx.Commit(); err != nil {
log.Fatal("Failed to commit transaction:", err)
}
log.Println("Balance updated successfully")
}
func (*Conn) PingContext
func (c *Conn) PingContext(ctx context.Context) error
PingContext проверяет, что соединение с базой данных все еще живо.
func (*Conn) PrepareContext
func (c *Conn) PrepareContext(ctx context.Context, query string) (*Stmt, error)
PrepareContext создает подготовленный запрос для последующих запросов или выполнений. Несколько запросов или выполнений могут быть запущены одновременно из полученного утверждения. Вызывающая сторона должна вызвать метод *Stmt.Close оператора, когда он больше не нужен.
Предоставленный контекст используется для подготовки утверждения, а не для его выполнения.
func (*Conn) QueryContext
func (c *Conn) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
QueryContext выполняет запрос, возвращающий строки, обычно SELECT. В args указываются любые параметры-заполнители запроса.
func (*Conn) QueryRowContext
func (c *Conn) QueryRowContext(ctx context.Context, query string, args ...any) *Row
QueryRowContext выполняет запрос, который, как ожидается, вернет не более одной строки. QueryRowContext всегда возвращает значение non-nil. Ошибки откладываются до вызова метода *Row.Scan. Если запрос не выбрал ни одной строки, то *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
func (*Conn) Raw
func (c *Conn) Raw(f func(driverConn any) error) (err error)
Raw выполняет f, раскрывая базовое соединение драйвера на время выполнения f. DriverConn не должен использоваться вне f.
После того как f вернется и err не будет driver.ErrBadConn, Conn будет продолжать использоваться до вызова Conn.Close.
Разбираем метод Raw()
для работы с низкоуровневыми соединениями
Метод Raw()
в database/sql
позволяет получить прямое доступ к драйвер-специфичному соединению, минуя абстракции пакета database/sql
. Это нужно для использования специфичных функций драйвера, которые не доступны через стандартный API.
Что такое f
в этом контексте?
f
- это ваша функция-колбэк, которая получает доступ к нативному соединению драйвера. Она должна иметь сигнатуру:
func(driverConn any) error
Зачем это нужно?
- Использование специфичных функций драйвера (например, PostgreSQL-специфичные команды)
- Оптимизация производительности для критичных участков кода
- Работа с нестандартными возможностями БД
Простой пример (PostgreSQL)
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "user=postgres dbname=test sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Используем Raw для доступа к нативному соединению PostgreSQL
err = conn.Raw(func(driverConn interface{}) error {
// Приводим к типу, который ожидаем (для PostgreSQL)
pgConn, ok := driverConn.(driver.Conn) // Здесь нужен ваш тип соединения
if !ok {
return fmt.Errorf("unexpected driver connection type")
}
// Пример: выполняем LISTEN для PostgreSQL-нотификаций
// Это специфичная функция PostgreSQL, недоступная через стандартный API
_, err := pgConn.(*pq.Conn).Exec("LISTEN channel_name")
return err
})
if err != nil {
log.Fatal(err)
}
}
Более практичный пример (MySQL)
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
conn, err := db.Conn(context.Background())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Получаем низкоуровневое соединение MySQL
err = conn.Raw(func(driverConn interface{}) error {
// Приводим к ожидаемому типу соединения MySQL
mysqlConn, ok := driverConn.(*mysql.Conn) // Требуется import драйвера
if !ok {
return fmt.Errorf("expected mysql.Conn, got %T", driverConn)
}
// Используем специфичные методы MySQL
fmt.Println("Server version:", mysqlConn.GetServerVersion())
fmt.Println("Connection ID:", mysqlConn.GetConnectionID())
// Можно выполнить специфичные команды
return mysqlConn.Ping()
})
if err != nil {
log.Fatal(err)
}
}
Важные предупреждения:
-
Тип соединения зависит от драйвера:
- PostgreSQL:
*pq.Conn
- MySQL:
*mysql.Conn
- SQLite: зависит от используемого драйвера
-
Безопасность:
- Не сохраняйте соединение вне функции
f
- Все ошибки должны быть обработаны
-
Совместимость:
- Код становится зависимым от конкретного драйвера
- Может сломаться при смене драйвера
Когда стоит использовать Raw()
?
- Когда вам нужен доступ к специфичным функциям СУБД
- Для оптимизации критических участков
- При работе с нестандартными возможностями (нотификации, специфичные команды)
В большинстве случаев стандартного API database/sql
достаточно, и Raw()
не понадобится.
7 - Описание типа DBStats database/sql
DBStats DBStats содержит статистику базы данных. В этом разделе представлены другие типы: IsolationLevel, NamedArg, Null, NullBool
type DBStats
type DBStats struct {
MaxOpenConnections int // Максимальное количество открытых соединений с базой данных.
// Состояние пула
OpenConnections int // Количество установленных соединений, как используемых, так и простаивающих.
InUse int // Количество соединений, используемых в данный момент.
Idle int // Количество простаивающих соединений.
// Счетчики
WaitCount int64 // Общее количество ожидающих соединений.
WaitDuration time.Duration // Общее время, заблокированное в ожидании нового соединения.
MaxIdleClosed int64 // Общее количество соединений, закрытых из-за SetMaxIdleConns.
MaxIdleTimeClosed int64 // Общее количество соединений, закрытых из-за SetConnMaxIdleTime.
MaxLifetimeClosed int64 // Общее количество соединений, закрытых из-за SetConnMaxLifetime.
}
DBStats содержит статистику базы данных.
type IsolationLevel
IsolationLevel - это уровень изоляции транзакции, используемый в TxOptions.
const (
LevelDefault IsolationLevel = iota
LevelReadUncommitted
LevelReadCommitted
LevelWriteCommitted
LevelRepeatableRead
LevelSnapshot
LevelSerializable
LevelLinearizable
)
Различные уровни изоляции, которые драйверы могут поддерживать в DB.BeginTx. Если драйвер не поддерживает заданный уровень изоляции, может быть возвращена ошибка.
См. https://en.wikipedia.org/wiki/Isolation_(database_systems)#Isolation_levels.
func (IsolationLevel) String
func (i IsolationLevel) String() string
String возвращает имя уровня изоляции транзакции.
type NamedArg
type NamedArg struct {
// Name - имя параметра-заместителя.
//
// Если пусто, то будет использоваться
// порядковая позиция в списке аргументов.
//
// Имя не должно содержать префикса символа.
Name string
// Value - это значение параметра.
// Ему могут быть присвоены те же типы значений, что и аргументам запроса
//.
Value any
// содержит отфильтрованные или неэкспонированные поля
}
NamedArg - это именованный аргумент. Значения NamedArg могут использоваться в качестве аргументов DB.Query или DB.Exec и связываться с соответствующим именованным параметром в операторе SQL.
Для более краткого способа создания значений NamedArg см. функцию Named.
func Named
func Named(name string, value any) NamedArg
Named предоставляет более лаконичный способ создания значений NamedArg.
Пример
db.ExecContext(ctx, `
delete from Invoice
where
TimeCreated < @end
and TimeCreated >= @start;`,
sql.Named("start", startTime),
sql.Named("end", endTime),
)
type Null
type Null[T any] struct {
V T
Valid bool
}
Null представляет значение, которое может быть нулевым. Null реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования:
var s Null[string]
err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s)
...
if s.Valid {
// используем s.V
} else {
// NULL значение
}
T должен быть одним из типов, принимаемых driver.Value.
func (*Null[T]) Scan
func (n *Null[T]) Scan(value any) error
func (Null[T]) Value
func (n Null[T]) Value() (driver.Value, error)
type NullBool ¶
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}
NullBool представляет bool, который может быть нулевым. NullBool реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullBool) Scan
func (n *NullBool) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullBool) Value
func (n NullBool) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullByte
type NullByte struct {
Byte byte
Valid bool // Valid is true if Byte is not NULL
}
NullByte представляет байт, который может быть нулевым. NullByte реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullByte) Scan
func (n *NullByte) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullByte) Value
func (n NullByte) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullFloat64 ¶
type NullFloat64 struct {
Float64 float64
Valid bool // Valid is true if Float64 is not NULL
}
NullFloat64 представляет float64, который может быть нулевым. NullFloat64 реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullFloat64) Scan ¶
func (n *NullFloat64) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullFloat64) Value ¶
func (n NullFloat64) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullInt16
type NullInt16 struct {
Int16 int16
Valid bool // Valid is true if Int16 is not NULL
}
NullInt16 представляет int16, который может быть нулевым. NullInt16 реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullInt16) Scan
func (n *NullInt16) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullInt16) Value
func (n NullInt16) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullInt32
type NullInt32 struct {
Int32 int32
Valid bool // Valid is true if Int32 is not NULL
}
NullInt32 представляет int32, который может быть нулевым. NullInt32 реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullInt32) Scan
func (n *NullInt32) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullInt32) Value
func (n NullInt32) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullInt64
type NullInt64 struct {
Int64 int64
Valid bool // Valid is true if Int64 is not NULL
}
NullInt64 представляет int64, который может быть нулевым. NullInt64 реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullInt64) Scan
func (n *NullInt64) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullInt64) Value
func (n NullInt64) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullString
type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}
NullString представляет строку, которая может быть нулевой. NullString реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования:
var s NullString
err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s)
...
if s.Valid {
// использовать s.String
} else {
// NULL значение
}
func (*NullString) Scan
func (ns *NullString) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullString) Value
func (ns NullString) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
type NullTime
type NullTime struct {
Time.Time
Valid bool // Valid is true if Time is not NULL
}
NullTime представляет время time.Time, которое может быть нулевым. NullTime реализует интерфейс Scanner, поэтому его можно использовать в качестве места сканирования, аналогично NullString.
func (*NullTime) Scan
func (n *NullTime) Scan(value any) error
Scan реализует интерфейс Scanner.
func (NullTime) Value
func (n NullTime) Value() (driver.Value, error)
Value реализует интерфейс driver.Valuer.
Практическое использование Null-типов в Go
Null-типы (NullString
, NullInt64
, NullTime
и др.) нужны для работы с NULL-значениями из базы данных. Разберём на реальных примерах.
Интерфейсы
- Scanner - позволяет сканировать (читать) значение из БД
- driver.Valuer - позволяет преобразовывать значение для записи в БД
Полный пример с NullString и NullTime
Пример
package main
import (
"database/sql"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
type UserProfile struct {
ID int64
Name sql.NullString
Bio sql.NullString
DeletedAt sql.NullTime
}
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 1. Запись данных с NULL-значениями
profile := UserProfile{
Name: sql.NullString{String: "Иван Иванов", Valid: true},
Bio: sql.NullString{}, // NULL
DeletedAt: sql.NullTime{Time: time.Now(), Valid: false}, // Будет записан как NULL
}
_, err = db.Exec(
"INSERT INTO users (name, bio, deleted_at) VALUES (?, ?, ?)",
profile.Name,
profile.Bio,
profile.DeletedAt,
)
if err != nil {
log.Fatal(err)
}
// 2. Чтение данных с возможными NULL-значениями
var user UserProfile
err = db.QueryRow(`
SELECT id, name, bio, deleted_at
FROM users
WHERE id = ?
`, 1).Scan(
&user.ID,
&user.Name,
&user.Bio,
&user.DeletedAt,
)
if err != nil {
log.Fatal(err)
}
// 3. Обработка NULL-значений
log.Println("ID:", user.ID)
if user.Name.Valid {
log.Println("Name:", user.Name.String)
} else {
log.Println("Name not set")
}
if user.Bio.Valid {
log.Println("Bio:", user.Bio.String)
} else {
log.Println("Bio not set")
}
if user.DeletedAt.Valid {
log.Println("Deleted at:", user.DeletedAt.Time)
} else {
log.Println("Not deleted")
}
}
Как работают Null-типы?
1. Scan
(чтение из БД)
Пример
func (ns *NullString) Scan(value interface{}) error {
if value == nil {
ns.String, ns.Valid = "", false
return nil
}
ns.Valid = true
return convertAssign(&ns.String, value)
}
2. Value
(запись в БД)
Пример
func (ns NullString) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.String, nil
}
Пример с пользовательским Null-типом
Создадим свой NullStatus
для enum-поля:
Пример
type NullStatus struct {
Status string
Valid bool
}
func (ns *NullStatus) Scan(value interface{}) error {
if value == nil {
ns.Status, ns.Valid = "", false
return nil
}
ns.Valid = true
switch v := value.(type) {
case []byte:
ns.Status = string(v)
case string:
ns.Status = v
default:
return fmt.Errorf("unsupported type: %T", value)
}
return nil
}
func (ns NullStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.Status, nil
}
Когда использовать Null-типы?
- Когда поле в БД может быть NULL
- Когда нужно отличать “нулевое значение” от “неустановленного”
- При работе с опциональными полями
Альтернативы
В новых версиях Go можно использовать указатели:
Пример
var name *string
err := db.QueryRow("SELECT name FROM users WHERE id = 1").Scan(&name)
if name != nil {
// есть значение
}
Но Null-типы предоставляют более удобный интерфейс и явный флаг Valid
.
8 - Описание типа Out database/sql
Out может использоваться для извлечения параметров OUTPUT из хранимых процедур. В этом разделе представлены другие типы: RawBytes, Result
type Out
type Out struct {
// Dest — указатель на значение, которое будет установлено в качестве результата
// параметра OUTPUT хранящейся процедуры.
Dest any
// In — является ли параметр параметром INOUT. Если да, то входное значение для хранящейся
// процедуры — это разыменованное значение указателя Dest, которое затем заменяется
// выходным значением.
In bool
// содержит отфильтрованные или неэкспортированные поля
}
Out может использоваться для извлечения параметров OUTPUT из хранимых процедур.
Не все драйверы и базы данных поддерживают параметры OUTPUT.
Пример использования:
var outArg string
_, err := db.ExecContext(ctx, «ProcName», sql.Named(«Arg1», sql.Out{Dest: &outArg}))
type RawBytes
RawBytes — это байтовый срез, который содержит ссылку на память, принадлежащую самой базе данных. После выполнения Rows.Scan в RawBytes срез остается действительным только до следующего вызова Rows.Next, Rows.Scan или Rows.Close.
RawBytes
- это специальный тип в пакете database/sql
, определённый как []byte
, который используется для сканирования данных из базы данных без копирования памяти. Это может быть полезно для оптимизации производительности при работе с большими бинарными данными.
Основные принципы работы с RawBytes
- Временное владение памятью: RawBytes содержит ссылку на память, управляемую драйвером БД, а не на копию данных.
- Ограниченное время жизни: Данные в RawBytes действительны только до следующего вызова методов:
- Небезопасность: Если сохранить RawBytes и попытаться использовать после вышеуказанных вызовов, это приведёт к неопределённому поведению.
Пример 1: Базовое сканирование
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query("SELECT blob_column FROM my_table WHERE id = ?", 1)
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var raw sql.RawBytes
if err := rows.Scan(&raw); err != nil {
panic(err)
}
// Используем raw сразу, пока он действителен
fmt.Printf("Data: %s\n", raw)
// Если нужно сохранить данные, нужно сделать копию:
dataCopy := make([]byte, len(raw))
copy(dataCopy, raw)
// Теперь dataCopy можно использовать после rows.Next()
}
}
Пример 2: Сканирование нескольких столбцов
func scanMultipleColumns(db *sql.DB) error {
rows, err := db.Query("SELECT id, name, data FROM documents")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int
var name string
var data sql.RawBytes
if err := rows.Scan(&id, &name, &data); err != nil {
return err
}
fmt.Printf("ID: %d, Name: %s, Data length: %d\n", id, name, len(data))
// Обработка данных должна быть здесь
processData(data)
}
return rows.Err()
}
func processData(data []byte) {
// Обработка данных
}
Пример 3: Опасное использование (неправильное)
func incorrectUsage(db *sql.DB) {
rows, err := db.Query("SELECT data FROM large_blobs")
if err != nil {
panic(err)
}
defer rows.Close()
var allData [][]byte
for rows.Next() {
var raw sql.RawBytes
if err := rows.Scan(&raw); err != nil {
panic(err)
}
// ОШИБКА: сохраняем RawBytes, который станет недействительным
allData = append(allData, raw)
}
// Здесь allData содержит недействительные данные!
for _, data := range allData {
fmt.Println(data) // Может привести к панике или неверным данным
}
}
Когда использовать RawBytes
- Для больших бинарных данных, когда копирование нежелательно
- Для временной обработки данных, которые не нужно сохранять
- Когда производительность критична, и вы готовы следить за временем жизни данных
Альтернативы
Если нужно сохранить данные:
var raw sql.RawBytes
var data []byte
rows.Scan(&raw)
data = make([]byte, len(raw))
copy(data, raw)
// Теперь data можно использовать в любом месте
type Result ¶
type Result интерфейс {
// LastInsertId возвращает целое число, сгенерированное базой данных
// в ответ на команду. Обычно это будет из
// столбца «автоинкремент» при вставке новой строки. Не все
// базы данных поддерживают эту функцию, и синтаксис таких
// операторов варьируется.
LastInsertId() (int64, error)
// RowsAffected возвращает количество строк, затронутых
// обновлением, вставкой или удалением. Не все базы данных или драйверы баз данных
// драйвер базы данных может поддерживать эту функцию.
RowsAffected() (int64, error)
}
Результат обобщает выполненную команду SQL.
9 - Описание типа Row database/sql
Row — результат вызова DB.QueryRow для выбора одной строки. В этом разделе представлены другие типы: Rows, Scanner
type Row
type Row struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Row — результат вызова DB.QueryRow для выбора одной строки.
func (*Row) Err
func (r *Row) Err() error
Err предоставляет способ для обертывающих пакетов проверять ошибки запроса без вызова Row.Scan. Err возвращает ошибку, если таковая имела место при выполнении запроса. Если эта ошибка не равна nil, она также будет возвращена из Row.Scan.
func (*Row) Scan
func (r *Row) Scan(dest ...any) error
Scan копирует столбцы из соответствующей строки в значения, на которые указывает dest. Подробности см. в документации по Rows.Scan. Если запросу соответствует более одной строки, Scan использует первую строку и отбрасывает остальные. Если ни одна строка не соответствует запросу, Scan возвращает ErrNoRows.
type Rows
type Rows struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Rows — это результат запроса. Его курсор начинается перед первой строкой набора результатов. Используйте Rows.Next для перехода от строки к строке.
Пример
package main
import (
"context"
"database/sql"
"log"
"strings"
)
// Глобальные переменные лучше инициализировать
var (
ctx = context.Background() // Инициализация контекста
db *sql.DB
)
func main() {
// На практике db должен быть инициализирован перед использованием
// Например:
// var err error
// db, err = sql.Open("driver-name", "datasource")
// if err != nil {
// log.Fatal(err)
// }
// defer db.Close()
age := 27
// Добавляем проверку, что db не nil
if db == nil {
log.Fatal("database connection is not initialized")
}
// Используем QueryContext вместо Query для явного указания контекста
rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
names := make([]string, 0)
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
log.Fatal(err)
}
names = append(names, name)
}
// Проверяем ошибки после итерации
if err := rows.Err(); err != nil {
log.Fatal(err)
}
// Добавляем проверку на пустой результат
if len(names) == 0 {
log.Printf("No users found with age %d", age)
return
}
log.Printf("%s are %d years old", strings.Join(names, ", "), age)
}
func (*Rows) Close
func (rs *Rows) Close() error
Close закрывает Rows, предотвращая дальнейшее перечисление. Если Rows.Next вызывается и возвращает false, а дальнейших наборов результатов нет, Rows закрывается автоматически, и достаточно проверить результат Rows.Err. Close является идемпотентным и не влияет на результат Rows.Err.
func (*Rows) ColumnTypes
func (rs *Rows) ColumnTypes() ([]*ColumnType, error)
ColumnTypes возвращает информацию о столбцах, такую как тип столбца, длина и возможность принятия нулевого значения. Некоторая информация может быть недоступна из некоторых драйверов.
func (*Rows) Columns
func (rs *Rows) Columns() ([]string, error)
Columns возвращает имена столбцов. Columns возвращает ошибку, если строки закрыты.
func (*Rows) Err
func (rs *Rows) Err() error
Err возвращает ошибку, если таковая возникла во время итерации. Err может быть вызван после явного или неявного Rows.Close.
func (*Rows) Next
func (rs *Rows) Next() bool
Next подготавливает следующую строку результата для чтения с помощью метода Rows.Scan. Он возвращает true в случае успеха или false, если следующей строки результата нет или при ее подготовке произошла ошибка. Для различения этих двух случаев следует обратиться к Rows.Err.
Каждому вызову Rows.Scan, даже первому, должен предшествовать вызов Rows.Next.
Функция Rows.Next()
используется для итерации по строкам результата SQL-запроса.
Пример
Базовый пример использования Rows.Next()
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// Подключение к базе данных
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Выполнение запроса
rows, err := db.Query("SELECT id, name, email FROM users WHERE active = ?", true)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Итерация по строкам результата
for rows.Next() {
var id int
var name, email string
// Сканирование значений из текущей строки
if err := rows.Scan(&id, &name, &email); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s, Email: %s\n", id, name, email)
}
// Проверка ошибок после итерации
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
Почему используется цикл for?
Цикл for rows.Next()
выполняет итерации по всем строкам результата запроса:
-
Количество итераций равно количеству строк, возвращенных запросом.
-
Механизм работы:
- При первом вызове
rows.Next()
перемещает курсор к первой строке результата
- При последующих вызовах перемещает курсор к следующей строке
- Когда строки заканчиваются, возвращает
false
и цикл завершается
-
Важно: Каждый вызов rows.Scan()
должен быть предварен вызовом rows.Next()
Детализированный пример с обработкой ошибок
func getActiveUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name, email FROM users WHERE active = ?", true)
if err != nil {
return nil, fmt.Errorf("query failed: %v", err)
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
// Можно продолжить обработку других строк или прервать
return nil, fmt.Errorf("scan failed: %v", err)
}
users = append(users, u)
}
// Проверяем ошибки, которые могли возникнуть во время итерации
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration failed: %v", err)
}
return users, nil
}
type User struct {
ID int
Name string
Email string
}
Что происходит внутри цикла?
-
Первая итерация:
rows.Next()
перемещает курсор к первой строке (если она есть)
- Возвращает
true
, если строка доступна
- Выполняется
rows.Scan()
-
Последующие итерации:
rows.Next()
перемещает курсор к следующей строке
- Цикл продолжается, пока есть строки
-
Завершение:
- Когда строки заканчиваются,
rows.Next()
возвращает false
- Цикл прерывается
- Проверяется
rows.Err()
на наличие ошибок
Важные нюансы:
- Всегда вызывайте
defer rows.Close()
для освобождения ресурсов
- Проверяйте
rows.Err()
после цикла для выявления ошибок итерации
- Не используйте
rows.Next()
без последующего rows.Scan()
- Для пустых результатов цикл не выполнится ни разу
Альтернативный пример с обработкой пустого результата
func printUserCount(db *sql.DB) {
rows, err := db.Query("SELECT COUNT(*) FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// rows.Next() вернет true, даже если только одна строка
if rows.Next() {
var count int
if err := rows.Scan(&count); err != nil {
log.Fatal(err)
}
fmt.Printf("Total users: %d\n", count)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
Этот пример показывает, что даже для запросов, возвращающих одну строку (как COUNT), необходимо использовать rows.Next()
перед rows.Scan()
.
func (*Rows) NextResultSet
func (rs *Rows) NextResultSet() bool
NextResultSet подготавливает следующий набор результатов для чтения. Он сообщает, есть ли дальнейшие наборы результатов, или false, если дальнейших наборов результатов нет или если произошла ошибка при переходе к ним. Для различения этих двух случаев следует обратиться к методу Rows.Err.
После вызова NextResultSet перед сканированием всегда следует вызывать метод Rows.Next. Если есть дальнейшие наборы результатов, они могут не содержать строк в наборе результатов.
Функция NextResultSet()
используется, когда запрос к базе данных возвращает несколько наборов результатов (например, при выполнении хранимых процедур или пакетных запросов).
Пример
Пример с несколькими наборами результатов
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера MySQL
)
func main() {
// Инициализация подключения к базе данных
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Запрос, возвращающий несколько наборов результатов
// (например, хранимая процедура с несколькими SELECT)
query := `
SELECT id, name FROM users WHERE active = 1;
SELECT id, title FROM products WHERE price > 100;
SELECT COUNT(*) FROM orders WHERE status = 'completed';
`
// Выполнение запроса
rows, err := db.QueryContext(context.Background(), query)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Обработка первого набора результатов (пользователи)
fmt.Println("=== Active Users ===")
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s\n", id, name)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
// Переход к следующему набору результатов (продукты)
if rows.NextResultSet() {
fmt.Println("\n=== Expensive Products ===")
for rows.Next() {
var id int
var title string
if err := rows.Scan(&id, &title); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Title: %s\n", id, title)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
// Переход к следующему набору результатов (количество заказов)
if rows.NextResultSet() {
fmt.Println("\n=== Completed Orders Count ===")
for rows.Next() {
var count int
if err := rows.Scan(&count); err != nil {
log.Fatal(err)
}
fmt.Printf("Completed orders: %d\n", count)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
// Проверяем, есть ли еще наборы результатов
if rows.NextResultSet() {
fmt.Println("\nThere are more result sets available")
} else {
fmt.Println("\nNo more result sets")
}
}
Ключевые моменты работы с NextResultSet():
-
Порядок обработки: Всегда сначала обрабатывайте текущий набор результатов с помощью Next()
и Scan()
, прежде чем переходить к следующему.
-
Проверка наличия результатов: NextResultSet()
возвращает true
, если есть следующий набор результатов, даже если он пустой.
-
Обработка ошибок: После NextResultSet()
всегда проверяйте rows.Err()
на наличие ошибок.
-
Пустые наборы: Набор результатов может не содержать строк - это нормально, просто Next()
вернет false
сразу.
Альтернативный пример с хранимой процедурой
func callMultiResultStoredProcedure(db *sql.DB) error {
// Вызов хранимой процедуры, возвращающей несколько наборов результатов
rows, err := db.Query("CALL get_report_data()")
if err != nil {
return err
}
defer rows.Close()
// Обработка всех наборов результатов
resultSetIndex := 0
for {
// Обработка текущего набора результатов
for rows.Next() {
// В зависимости от набора результатов используем разную логику сканирования
switch resultSetIndex {
case 0:
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
return err
}
fmt.Printf("User: %d - %s\n", id, name)
case 1:
var product string
var price float64
if err := rows.Scan(&product, &price); err != nil {
return err
}
fmt.Printf("Product: %s - $%.2f\n", product, price)
// Можно добавить дополнительные case для других наборов
}
}
if err := rows.Err(); err != nil {
return err
}
// Переход к следующему набору результатов
if !rows.NextResultSet() {
break
}
resultSetIndex++
}
return nil
}
Важные предупреждения:
- Не все драйверы баз данных поддерживают несколько наборов результатов.
- Всегда проверяйте
rows.Err()
после NextResultSet()
.
- После перехода к новому набору результатов обязательно вызывайте
rows.Next()
перед сканированием.
- Структура данных в каждом наборе результатов может отличаться.
func (*Rows) Scan
func (rs *Rows) Scan(dest ...any) error
Scan копирует столбцы в текущей строке в значения, на которые указывает dest. Количество значений в dest должно совпадать с количеством столбцов в Rows.
Scan преобразует столбцы, прочитанные из базы данных, в следующие общие типы Go и специальные типы, предоставляемые пакетом sql:
*string
*[]byte
*int, *int8, *int16, *int32, *int64
*uint, *uint8, *uint16, *uint32, *uint64
*bool
*float32, *float64
*interface{}
*RawBytes
*Rows (cursor value)
любой тип, реализующий Scanner (см. документацию по Scanner)
В самом простом случае, если тип значения из исходного столбца является целым, bool или строковым типом T, а dest имеет тип *T, Scan просто присваивает значение через указатель.
Scan также преобразует строковые и числовые типы, если при этом не теряется информация. В то время как Scan преобразует все числа, сканированные из числовых столбцов базы данных, в *string, сканирование в числовые типы проверяется на переполнение. Например, float64 со значением 300 или строка со значением «300» могут быть отсканированы в uint16, но не в uint8, хотя float64(255) или «255» могут быть отсканированы в uint8. Исключением является то, что при сканировании некоторых чисел float64 в строки может произойти потеря информации при преобразовании в строку. В общем случае, сканируйте столбцы с плавающей запятой в *float64.
Если аргумент dest имеет тип *[]byte, Scan сохраняет в этом аргументе копию соответствующих данных. Копия принадлежит вызывающему и может быть изменена и храниться неограниченное время. Копирование можно избежать, используя вместо этого аргумент типа *RawBytes; ограничения на его использование см. в документации по RawBytes.
Если аргумент имеет тип *interface{}, Scan копирует значение, предоставленное базовым драйвером, без преобразования. При сканировании из исходного значения типа []byte в *interface{} создается копия среза, и вызывающая сторона владеет результатом.
Исходные значения типа time.Time могут быть отсканированы в значения типа *time.Time, *interface{}, *string или *[]byte. При преобразовании в два последних используется time.RFC3339Nano.
Источниковые значения типа bool могут быть просканированы в типы *bool, *interface{}, *string, *[]byte или *RawBytes.
Для сканирования в *bool источником могут быть true, false, 1, 0 или строковые входы, которые можно проанализировать с помощью strconv.ParseBool.
Scan также может преобразовать курсор, возвращенный из запроса, такого как «select cursor(select * from my_table) from dual»
, в значение *Rows, которое само по себе может быть просканировано. Родительский запрос select закроет любой курсор *Rows, если родительский *Rows закрыт.
Если любой из первых аргументов, реализующих Scanner, возвращает ошибку, эта ошибка будет обернута в возвращаемую ошибку.
type Scanner
type Scanner interface {
// Scan назначает значение из драйвера базы данных.
//
// Значение src будет одного из следующих типов:
//
// int64
// float64
// bool
// []byte
// string
// time.Time
// nil - для значений NULL
//
// Если значение не может быть сохранено
// без потери информации.
//
// Типы ссылок, такие как []byte, действительны только до следующего вызова Scan
// и не должны сохраняться. Их базовая память принадлежит драйверу.
// Если сохранение необходимо, скопируйте их значения перед следующим вызовом Scan.
Scan(src any) error
}
Scanner — интерфейс, используемый Rows.Scan.
10 - Описание типа Stmt database/sql
Stmt — это подготовленное выражение. Stmt безопасно для одновременного использования несколькими goroutines.
type Stmt
type Stmt struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Stmt — это подготовленное выражение. Stmt безопасно для одновременного использования несколькими goroutines.
Если Stmt подготовлен на Tx или Conn, он будет навсегда привязан к одному базовому соединению. Если Tx или Conn закрывается, Stmt станет непригодным для использования, и все операции будут возвращать ошибку. Если Stmt подготовлен на DB, он будет оставаться пригодным для использования в течение всего срока жизни DB. Когда Stmt необходимо выполнить на новом базовом соединении, он автоматически подготовится на новом соединении.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql" // Добавляем импорт драйвера
)
func main() {
// Инициализируем контекст
ctx := context.Background()
// Инициализируем соединение с базой данных (в реальном коде)
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Проверяем соединение
if err := db.PingContext(ctx); err != nil {
log.Fatal(err)
}
// Создаем подготовленное выражение
stmt, err := db.PrepareContext(ctx, "SELECT username FROM users WHERE id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// Выполняем запрос с параметром
id := 43
var username string
err = stmt.QueryRowContext(ctx, id).Scan(&username)
switch {
case err == sql.ErrNoRows:
log.Printf("no user with id %d", id) // Используем Printf вместо Fatalf чтобы не завершать программу
case err != nil:
log.Fatal(err)
default:
log.Printf("username is %s\n", username)
}
}
func (*Stmt) Close
func (s *Stmt) Close() error
Close закрывает оператор.
func (*Stmt) Exec
func (s *Stmt) Exec(args ...any) (Result, error)
Exec выполняет подготовленный оператор с заданными аргументами и возвращает Result, обобщающий результат оператора.
Exec использует context.Background внутренне; чтобы указать контекст, используйте Stmt.ExecContext.
func (*Stmt) ExecContext
func (s *Stmt) ExecContext(ctx context.Context, args ...any) (Result, error)
ExecContext выполняет подготовленное выражение с заданными аргументами и возвращает Result, обобщающий результат выражения.
func (*Stmt) Query
func (s *Stmt) Query(args ...any) (*Rows, error)
Query выполняет подготовленный запрос с заданными аргументами и возвращает результаты запроса в виде *Rows.
Query использует context.Background внутренне; для указания контекста используйте Stmt.QueryContext.
func (*Stmt) QueryContext ¶
func (s *Stmt) QueryContext(ctx context.Context, args ...any) (*Rows, error)
QueryContext выполняет подготовленное запросное выражение с заданными аргументами и возвращает результаты запроса в виде *Rows.
func (*Stmt) QueryRow
func (s *Stmt) QueryRow(args ...any) *Row
QueryRow выполняет подготовленный запрос с заданными аргументами. Если во время выполнения запроса происходит ошибка, она будет возвращена вызовом Scan на возвращенном *Row, который всегда не равен nil. Если запрос не выбирает никаких строк, *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
Пример использования:
var name string
err := nameByUseridStmt.QueryRow(id).Scan(&name)
QueryRow использует context.Background внутренне; для указания контекста используйте Stmt.QueryRowContext.
func (*Stmt) QueryRowContext
func (s *Stmt) QueryRowContext(ctx context.Context, args ...any) *Row
QueryRowContext выполняет подготовленный запрос с заданными аргументами. Если во время выполнения запроса происходит ошибка, она будет возвращена вызовом Scan на возвращенном *Row, который всегда не равен nil. Если запрос не выбирает никаких строк, *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql" // Не забудьте импортировать драйвер
)
func main() {
// 1. Инициализация контекста
ctx := context.Background()
// 2. Инициализация подключения к БД (пример для MySQL)
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// 3. Проверка соединения
if err := db.PingContext(ctx); err != nil {
log.Fatal("Failed to ping database:", err)
}
// 4. Создание подготовленного выражения
stmt, err := db.PrepareContext(ctx, "SELECT username FROM users WHERE id = ?")
if err != nil {
log.Fatal("Failed to prepare statement:", err)
}
defer stmt.Close()
// 5. Выполнение запроса
id := 43
var username string
err = stmt.QueryRowContext(ctx, id).Scan(&username)
// 6. Обработка результатов
switch {
case err == sql.ErrNoRows:
log.Printf("User with id %d not found", id) // Не фатальная ошибка
case err != nil:
log.Fatal("Query failed:", err)
default:
log.Printf("Found user: %s (ID: %d)", username, id)
}
}
11 - Описание типа Tx database/sql
Tx — это незавершенная транзакция базы данных.
type Tx
type Tx struct {
// содержит отфильтрованные или неэкспортируемые поля
}
Tx — это незавершенная транзакция базы данных.
Транзакция должна заканчиваться вызовом Tx.Commit или Tx.Rollback.
После вызова Tx.Commit или Tx.Rollback все операции по транзакции завершаются с ошибкой ErrTxDone.
Инструкции, подготовленные для транзакции вызовом методов Tx.Prepare или Tx.Stmt транзакции, закрываются вызовом Tx.Commit или Tx.Rollback.
func (*Tx) Commit
func (tx *Tx) Commit() error
Commit фиксирует транзакцию.
func (*Tx) Exec
func (tx *Tx) Exec(query string, args ...any) (Result, error)
Exec выполняет запрос, который не возвращает строки. Например: INSERT и UPDATE.
Exec использует context.Background внутренне; для указания контекста используйте Tx.ExecContext.
func (*Tx) ExecContext
func (tx *Tx) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
ExecContext выполняет запрос, который не возвращает строки. Например: INSERT и UPDATE.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)
func main() {
// Инициализация подключения к БД
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Проверка соединения
ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
log.Fatal("Database connection failed:", err)
}
// Начало транзакции
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// Выполнение операций в транзакции
id := 37
_, execErr := tx.ExecContext(ctx, "UPDATE users SET status = ? WHERE id = ?", "paid", id)
if execErr != nil {
// Попытка отката при ошибке
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Printf("UPDATE failed: %v, rollback also failed: %v", execErr, rollbackErr)
} else {
log.Printf("UPDATE failed, transaction rolled back: %v", execErr)
}
return
}
// Фиксация транзакции
if err := tx.Commit(); err != nil {
log.Fatal("Failed to commit transaction:", err)
}
log.Println("Transaction completed successfully")
}
func (*Tx) Prepare
func (tx *Tx) Prepare(query string) (*Stmt, error)
Prepare создает подготовленное выражение для использования в транзакции.
Возвращаемое выражение работает в рамках транзакции и будет закрыто после фиксации или отката транзакции.
Чтобы использовать существующее подготовленное выражение в этой транзакции, см. Tx.Stmt.
Prepare использует context.Background внутренне; чтобы указать контекст, используйте Tx.PrepareContext.
Пример
package main
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql" // Импорт драйвера БД
)
func main() {
// Инициализация подключения к БД
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname?parseTime=true")
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Проверка соединения
if err := db.Ping(); err != nil {
log.Fatal("Database connection failed:", err)
}
projects := []struct {
mascot string
release int
}{
{"tux", 1991},
{"duke", 1996},
{"gopher", 2009},
{"moby dock", 2013},
}
// Начало транзакции
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// Откат в случае ошибки (игнорируется если был Commit)
defer func() {
if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
log.Printf("Warning: rollback failed: %v", err)
}
}()
// Подготовка выражения
stmt, err := tx.PrepareContext(ctx, "INSERT INTO projects(id, mascot, release, category) VALUES(?, ?, ?, ?)")
if err != nil {
log.Fatal("Failed to prepare statement:", err)
}
defer stmt.Close()
// Вставка данных
for id, project := range projects {
if _, err := stmt.ExecContext(ctx, id+1, project.mascot, project.release, "open source"); err != nil {
log.Fatal("Failed to insert project:", err)
}
}
// Фиксация транзакции
if err := tx.Commit(); err != nil {
log.Fatal("Failed to commit transaction:", err)
}
log.Println("Successfully inserted", len(projects), "projects")
}
Дополнительные рекомендации:
-
Параметры подключения:
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5*time.Minute)
-
Пакетная вставка:
Для большого количества данных рассмотрите возможность:
// Начало пакетной вставки
values := make([]interface{}, 0, len(projects)*4)
query := "INSERT INTO projects(id, mascot, release, category) VALUES"
for id, project := range projects {
if id > 0 {
query += ","
}
query += "(?, ?, ?, ?)"
values = append(values, id+1, project.mascot, project.release, "open source")
}
if _, err := tx.ExecContext(ctx, query, values...); err != nil {
log.Fatal(err)
}
-
Таймауты:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
-
Валидация данных:
Добавьте проверку данных перед вставкой:
if project.release < 1990 || project.release > time.Now().Year() {
log.Printf("Invalid release year for %s: %d", project.mascot, project.release)
continue
}
func (*Tx) PrepareContext
func (tx *Tx) PrepareContext(ctx context.Context, query string) (*Stmt, error)
PrepareContext создает подготовленное выражение для использования в транзакции.
Возвращенное выражение работает в рамках транзакции и будет закрыто после фиксации или отката транзакции.
Чтобы использовать существующее подготовленное выражение в этой транзакции, см. Tx.Stmt.
Предоставленный контекст будет использоваться для подготовки контекста, а не для выполнения возвращаемого оператора. Возвращаемый оператор будет выполняться в контексте транзакции.
func (*Tx) Query
func (tx *Tx) Query(query string, args ...any) (*Rows, error)
Query выполняет запрос, который возвращает строки, обычно SELECT.
Query использует context.Background внутренне; чтобы указать контекст, используйте Tx.QueryContext.
func (*Tx) QueryContext
func (tx *Tx) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
QueryContext выполняет запрос, который возвращает строки, обычно SELECT.
func (*Tx) QueryRow
func (tx *Tx) QueryRow(query string, args ...any) *Row
QueryRow выполняет запрос, который должен вернуть не более одной строки. QueryRow всегда возвращает значение, отличное от nil. Ошибки откладываются до вызова метода Scan Row. Если запрос не выбирает ни одной строки, *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
QueryRow использует context.Background внутренне; для указания контекста используйте Tx.QueryRowContext.
func (*Tx) QueryRowContext
func (tx *Tx) QueryRowContext(ctx context.Context, query string, args ...any) *Row
QueryRowContext выполняет запрос, который должен вернуть не более одной строки. QueryRowContext всегда возвращает значение, отличное от nil. Ошибки откладываются до вызова метода Scan Row. Если запрос не выбирает ни одной строки, *Row.Scan вернет ErrNoRows. В противном случае *Row.Scan сканирует первую выбранную строку и отбрасывает остальные.
func (*Tx) Rollback
func (tx *Tx) Rollback() error
Rollback прерывает транзакцию.
Пример
код транзакции для обновления водителей и заказов
package main
import (
"context"
"database/sql"
"log"
_ "github.com/lib/pq" // Импорт драйвера PostgreSQL
)
func main() {
// Инициализация подключения к БД
connStr := "user=postgres dbname=mydb sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer db.Close()
// Проверка соединения
if err := db.Ping(); err != nil {
log.Fatal("Database connection failed:", err)
}
// Создаем контекст с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Начало транзакции
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// Гарантированный откат при ошибках
defer func() {
if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
log.Printf("Warning: rollback error: %v", err)
}
}()
id := 53
// 1. Обновление статуса водителя
_, err = tx.ExecContext(ctx, "UPDATE drivers SET status = $1 WHERE id = $2", "assigned", id)
if err != nil {
log.Printf("Failed to update driver status: %v", err)
return
}
// 2. Назначение водителя на заказы
_, err = tx.ExecContext(ctx, "UPDATE pickups SET driver_id = $1 WHERE driver_id IS NULL", id)
if err != nil {
log.Printf("Failed to assign driver to pickups: %v", err)
return
}
// Фиксация транзакции
if err := tx.Commit(); err != nil {
log.Fatal("Failed to commit transaction:", err)
}
log.Printf("Successfully assigned driver %d and updated pickup orders", id)
}
Дополнительные улучшения:
-
Безопасность UPDATE:
// Ограничиваем обновление только незанятыми заказами
"UPDATE pickups SET driver_id = $1 WHERE driver_id IS NULL"
-
Проверка результата:
res, err := tx.ExecContext(...)
if rowsAffected, _ := res.RowsAffected(); rowsAffected == 0 {
log.Printf("Warning: no rows were updated")
}
-
Повторные попытки:
maxRetries := 3
for i := 0; i < maxRetries; i++ {
// ... выполнение транзакции ...
if err == nil {
break
}
if shouldRetry(err) {
continue
}
break
}
-
Connection Pool:
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5*time.Minute)
func (*Tx) Stmt
func (tx *Tx) Stmt(stmt *Stmt) *Stmt
Stmt возвращает подготовленное заявление, специфичное для транзакции, из существующего заявления.
Пример:
updateMoney, err := db.Prepare(«UPDATE balance SET money=money+? WHERE id=?»)
...
tx, err := db.Begin()...
res, err := tx.Stmt(updateMoney).Exec(123.45, 98293203)
Возвращенное выражение работает в рамках транзакции и будет закрыто после фиксации или отката транзакции.
Stmt использует context.Background внутри всегда; для указания контекста используйте Tx.StmtContext.
func (*Tx) StmtContext
func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt
StmtContext возвращает подготовленное выражение, специфичное для транзакции, из существующего выражения.
Пример:
updateMoney, err := db.Prepare(«UPDATE balance SET money=money+? WHERE id=?»)
…
tx, err := db.Begin()…
res, err := tx.StmtContext(ctx, updateMoney).Exec(123.45, 98293203)
Предоставленный контекст используется для подготовки оператора, а не для его выполнения.
Возвращенный оператор работает в рамках транзакции и будет закрыт после фиксации или отката транзакции.
type TxOptions
type TxOptions struct {
// Isolation — уровень изоляции транзакции.
// Если равен нулю, используется уровень по умолчанию драйвера или базы данных.
Isolation IsolationLevel
ReadOnly bool
}
TxOptions содержит параметры транзакции, которые будут использоваться в DB.BeginTx.