El paquete database/sql de Go proporciona una interfaz genérica para bases de datos relacionales. El driver específico (PostgreSQL, MySQL, SQLite) se importa como efecto secundario y database/sql se encarga del pool de conexiones, las transacciones y la gestión de recursos.
Abrir la conexión
sql.Open no abre ninguna conexión real: solo valida el DSN y configura el pool. Para verificar que la BD es accesible, usa Ping:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq" // driver PostgreSQL, importado por efecto secundario
)
func main() {
dsn := "host=localhost user=go dbname=tienda password=secreto sslmode=disable"
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal("open:", err)
}
defer db.Close()
// Configurar el pool de conexiones
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
if err := db.Ping(); err != nil {
log.Fatal("ping:", err)
}
fmt.Println("conectado a PostgreSQL")
}
QueryContext: leer múltiples filas
Usa siempre las variantes Context para que el timeout del contexto cancele la query si tarda demasiado:
type Producto struct {
ID int
Nombre string
Precio float64
Stock int
}
func listarProductos(ctx context.Context, db *sql.DB) ([]Producto, error) {
rows, err := db.QueryContext(ctx,
"SELECT id, nombre, precio, stock FROM productos WHERE activo = true ORDER BY nombre")
if err != nil {
return nil, fmt.Errorf("query: %w", err)
}
defer rows.Close()
var productos []Producto
for rows.Next() {
var p Producto
if err := rows.Scan(&p.ID, &p.Nombre, &p.Precio, &p.Stock); err != nil {
return nil, fmt.Errorf("scan: %w", err)
}
productos = append(productos, p)
}
return productos, rows.Err()
}
QueryRowContext: leer una sola fila
func obtenerProducto(ctx context.Context, db *sql.DB, id int) (*Producto, error) {
var p Producto
err := db.QueryRowContext(ctx,
"SELECT id, nombre, precio, stock FROM productos WHERE id = $1", id).
Scan(&p.ID, &p.Nombre, &p.Precio, &p.Stock)
if err == sql.ErrNoRows {
return nil, nil // no encontrado, sin error
}
if err != nil {
return nil, fmt.Errorf("obtener producto %d: %w", id, err)
}
return &p, nil
}
ExecContext: INSERT, UPDATE y DELETE
func crearProducto(ctx context.Context, db *sql.DB, p Producto) (int64, error) {
result, err := db.ExecContext(ctx,
"INSERT INTO productos (nombre, precio, stock) VALUES ($1, $2, $3)",
p.Nombre, p.Precio, p.Stock)
if err != nil {
return 0, fmt.Errorf("insert: %w", err)
}
id, err := result.LastInsertId()
return id, err
}
Transacciones con BeginTx
Las transacciones agrupan varias operaciones en una unidad atómica. Si algo falla, el defer tx.Rollback() deshace todos los cambios:
func transferirStock(ctx context.Context, db *sql.DB, origenID, destinoID, cantidad int) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // no hace nada si ya hicimos Commit
// Reducir stock del origen
_, err = tx.ExecContext(ctx,
"UPDATE productos SET stock = stock - $1 WHERE id = $2 AND stock >= $1",
cantidad, origenID)
if err != nil {
return fmt.Errorf("reducir stock origen: %w", err)
}
// Aumentar stock del destino
_, err = tx.ExecContext(ctx,
"UPDATE productos SET stock = stock + $1 WHERE id = $2",
cantidad, destinoID)
if err != nil {
return fmt.Errorf("aumentar stock destino: %w", err)
}
return tx.Commit()
}
Prepared statements
Los prepared statements mejoran el rendimiento cuando ejecutas la misma query muchas veces y protegen contra inyección SQL:
func cargaMasiva(ctx context.Context, db *sql.DB, productos []Producto) error {
stmt, err := db.PrepareContext(ctx,
"INSERT INTO productos (nombre, precio, stock) VALUES ($1, $2, $3)")
if err != nil {
return err
}
defer stmt.Close()
for _, p := range productos {
if _, err := stmt.ExecContext(ctx, p.Nombre, p.Precio, p.Stock); err != nil {
return fmt.Errorf("insertar %s: %w", p.Nombre, err)
}
}
return nil
}
Valores nulos con sql.NullString y sql.NullInt64
type Pedido struct {
ID int
UsuarioID int
NotaEspecial sql.NullString // puede ser NULL en la BD
}
func obtenerPedido(ctx context.Context, db *sql.DB, id int) (*Pedido, error) {
var p Pedido
err := db.QueryRowContext(ctx,
"SELECT id, usuario_id, nota_especial FROM pedidos WHERE id = $1", id).
Scan(&p.ID, &p.UsuarioID, &p.NotaEspecial)
if err != nil {
return nil, err
}
if p.NotaEspecial.Valid {
fmt.Println("nota:", p.NotaEspecial.String)
}
return &p, nil
}
