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