Это многостраничный печатный вид этого раздела. Нажмите что бы печатать.

Вернуться к обычному просмотру страницы.

Описание пакета 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

  1. Всегда проверяйте ошибки после операций с базой данных.
  2. Используйте errors.Is для проверки конкретных ошибок пакета database/sql.
  3. Закрывайте ресурсы (соединения, транзакции, результаты запросов) с помощью defer.
  4. Используйте контексты для управления таймаутами и отменой операций.

Полный пример работы с базой данных:

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 контекст играет ключевую роль.

Основные цели использования контекста в транзакциях

  1. Отмена операций - можно прервать долгий запрос
  2. Таймауты - установка максимального времени выполнения
  3. Распространение значений - передача метаданных через цепочку вызовов

Методы с поддержкой контекста

В 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)
}

Особенности работы контекста в транзакциях

  1. Каскадная отмена - отмена контекста прерывает все операции в транзакции
  2. Изоляция транзакций - контекст не влияет на другие транзакции
  3. Ресурсы - отмена не освобождает соединение автоматически

Практические сценарии использования

  1. HTTP-обработчики - привязка к времени жизни запроса:
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    tx, err := db.BeginTx(ctx, nil)
    // ...
}
  1. Долгие отчеты - возможность отмены:
ctx, cancel := context.WithCancel(context.Background())
go func() {
    time.Sleep(10*time.Second)
    cancel() // Принудительная отмена через 10 сек
}()

rows, err := db.QueryContext(ctx, "SELECT * FROM big_table")
  1. Распределенные транзакции - передача идентификаторов:
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.

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

Зачем это нужно?

  1. Использование специфичных функций драйвера (например, PostgreSQL-специфичные команды)
  2. Оптимизация производительности для критичных участков кода
  3. Работа с нестандартными возможностями БД

Простой пример (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)
	}
}

Важные предупреждения:

  1. Тип соединения зависит от драйвера:

    • PostgreSQL: *pq.Conn
    • MySQL: *mysql.Conn
    • SQLite: зависит от используемого драйвера
  2. Безопасность:

    • Не сохраняйте соединение вне функции f
    • Все ошибки должны быть обработаны
  3. Совместимость:

    • Код становится зависимым от конкретного драйвера
    • Может сломаться при смене драйвера

Когда стоит использовать Raw()?

  1. Когда вам нужен доступ к специфичным функциям СУБД
  2. Для оптимизации критических участков
  3. При работе с нестандартными возможностями (нотификации, специфичные команды)

В большинстве случаев стандартного 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

type IsolationLevel int

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-значениями из базы данных. Разберём на реальных примерах.

Интерфейсы

  1. Scanner - позволяет сканировать (читать) значение из БД
  2. 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-типы?

  1. Когда поле в БД может быть NULL
  2. Когда нужно отличать “нулевое значение” от “неустановленного”
  3. При работе с опциональными полями

Альтернативы

В новых версиях 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

type RawBytes []byte

RawBytes — это байтовый срез, который содержит ссылку на память, принадлежащую самой базе данных. После выполнения Rows.Scan в RawBytes срез остается действительным только до следующего вызова Rows.Next, Rows.Scan или Rows.Close.

RawBytes - это специальный тип в пакете database/sql, определённый как []byte, который используется для сканирования данных из базы данных без копирования памяти. Это может быть полезно для оптимизации производительности при работе с большими бинарными данными.

Основные принципы работы с RawBytes

  1. Временное владение памятью: RawBytes содержит ссылку на память, управляемую драйвером БД, а не на копию данных.
  2. Ограниченное время жизни: Данные в RawBytes действительны только до следующего вызова методов:
    • Next()
    • Scan()
    • Close()
  3. Небезопасность: Если сохранить 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

  1. Для больших бинарных данных, когда копирование нежелательно
  2. Для временной обработки данных, которые не нужно сохранять
  3. Когда производительность критична, и вы готовы следить за временем жизни данных

Альтернативы

Если нужно сохранить данные:

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() выполняет итерации по всем строкам результата запроса:

  1. Количество итераций равно количеству строк, возвращенных запросом.

  2. Механизм работы:

    • При первом вызове rows.Next() перемещает курсор к первой строке результата
    • При последующих вызовах перемещает курсор к следующей строке
    • Когда строки заканчиваются, возвращает false и цикл завершается
  3. Важно: Каждый вызов 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
}

Что происходит внутри цикла?

  1. Первая итерация:

    • rows.Next() перемещает курсор к первой строке (если она есть)
    • Возвращает true, если строка доступна
    • Выполняется rows.Scan()
  2. Последующие итерации:

    • rows.Next() перемещает курсор к следующей строке
    • Цикл продолжается, пока есть строки
  3. Завершение:

    • Когда строки заканчиваются, rows.Next() возвращает false
    • Цикл прерывается
    • Проверяется rows.Err() на наличие ошибок

Важные нюансы:

  1. Всегда вызывайте defer rows.Close() для освобождения ресурсов
  2. Проверяйте rows.Err() после цикла для выявления ошибок итерации
  3. Не используйте rows.Next() без последующего rows.Scan()
  4. Для пустых результатов цикл не выполнится ни разу

Альтернативный пример с обработкой пустого результата

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():

  1. Порядок обработки: Всегда сначала обрабатывайте текущий набор результатов с помощью Next() и Scan(), прежде чем переходить к следующему.

  2. Проверка наличия результатов: NextResultSet() возвращает true, если есть следующий набор результатов, даже если он пустой.

  3. Обработка ошибок: После NextResultSet() всегда проверяйте rows.Err() на наличие ошибок.

  4. Пустые наборы: Набор результатов может не содержать строк - это нормально, просто 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
}

Важные предупреждения:

  1. Не все драйверы баз данных поддерживают несколько наборов результатов.
  2. Всегда проверяйте rows.Err() после NextResultSet().
  3. После перехода к новому набору результатов обязательно вызывайте rows.Next() перед сканированием.
  4. Структура данных в каждом наборе результатов может отличаться.

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")
}

Дополнительные рекомендации:

  1. Параметры подключения:

    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(25)
    db.SetConnMaxLifetime(5*time.Minute)
    
  2. Пакетная вставка: Для большого количества данных рассмотрите возможность:

    // Начало пакетной вставки
    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)
    }
    
  3. Таймауты:

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
  4. Валидация данных: Добавьте проверку данных перед вставкой:

    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)
}

Дополнительные улучшения:

  1. Безопасность UPDATE:

    // Ограничиваем обновление только незанятыми заказами
    "UPDATE pickups SET driver_id = $1 WHERE driver_id IS NULL"
    
  2. Проверка результата:

    res, err := tx.ExecContext(...)
    if rowsAffected, _ := res.RowsAffected(); rowsAffected == 0 {
        log.Printf("Warning: no rows were updated")
    }
    
  3. Повторные попытки:

    maxRetries := 3
    for i := 0; i < maxRetries; i++ {
        // ... выполнение транзакции ...
        if err == nil {
            break
        }
        if shouldRetry(err) {
            continue
        }
        break
    }
    
  4. 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.