Acceso a bases de datos con database/sql y PostgreSQL

Avanzado
Go
Go
Actualizado: 03/04/2026

Arquitectura de database/sql

Go separa la interfaz genérica (paquete database/sql) de los drivers específicos de cada base de datos. Esta separación permite cambiar de base de datos con mínimos cambios de código:

Acceso a bases de datos en Go: database/sql, connection pool y CRUD

Tu código → database/sql → Driver (pgx, lib/pq, go-sqlite3, go-mysql...)

El driver se registra automáticamente con un import de efectos secundarios:

import (
    "database/sql"
    _ "github.com/jackc/pgx/v5/stdlib" // registra el driver "pgx"
)

Conectar y configurar el pool

func nuevaBD(dsn string) (*sql.DB, error) {
    db, err := sql.Open("pgx", dsn)
    if err != nil {
        return nil, fmt.Errorf("sql.Open: %w", err)
    }

    // Pool de conexiones — ajustar según la carga
    db.SetMaxOpenConns(25)               // máximo conexiones abiertas
    db.SetMaxIdleConns(5)                // conexiones en standby
    db.SetConnMaxLifetime(5 * time.Minute)  // tiempo máximo de vida
    db.SetConnMaxIdleTime(1 * time.Minute)  // tiempo máximo en idle

    // Verificar conectividad
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := db.PingContext(ctx); err != nil {
        return nil, fmt.Errorf("ping: %w", err)
    }
    return db, nil
}

DSN de PostgreSQL:

host=localhost port=5432 user=admin password=secreto dbname=tienda sslmode=disable

Definir el modelo y la tabla

CREATE TABLE productos (
    id      SERIAL PRIMARY KEY,
    nombre  VARCHAR(200) NOT NULL,
    precio  DECIMAL(10,2) NOT NULL,
    stock   INT NOT NULL DEFAULT 0,
    activo  BOOLEAN NOT NULL DEFAULT TRUE
);
type Producto struct {
    ID     int
    Nombre string
    Precio float64
    Stock  int
    Activo bool
}

Consulta de múltiples filas: QueryContext

func (r *productosRepo) ObtenerTodos(ctx context.Context) ([]Producto, error) {
    const query = `
        SELECT id, nombre, precio, stock, activo
        FROM productos
        WHERE activo = TRUE
        ORDER BY nombre`

    rows, err := r.db.QueryContext(ctx, query)
    if err != nil {
        return nil, fmt.Errorf("query: %w", err)
    }
    defer rows.Close() // SIEMPRE cerrar rows

    var productos []Producto
    for rows.Next() {
        var p Producto
        if err := rows.Scan(&p.ID, &p.Nombre, &p.Precio, &p.Stock, &p.Activo); err != nil {
            return nil, fmt.Errorf("scan: %w", err)
        }
        productos = append(productos, p)
    }

    // Verificar errores después del bucle (error de red, etc.)
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("rows.Err: %w", err)
    }
    return productos, nil
}

Consulta de una fila: QueryRowContext

func (r *productosRepo) ObtenerPorID(ctx context.Context, id int) (*Producto, error) {
    const query = `SELECT id, nombre, precio, stock, activo FROM productos WHERE id = $1`

    var p Producto
    err := r.db.QueryRowContext(ctx, query, id).
        Scan(&p.ID, &p.Nombre, &p.Precio, &p.Stock, &p.Activo)

    if errors.Is(err, sql.ErrNoRows) {
        return nil, fmt.Errorf("producto %d: no encontrado", id)
    }
    if err != nil {
        return nil, fmt.Errorf("scan: %w", err)
    }
    return &p, nil
}

Inserción con retorno del ID generado

func (r *productosRepo) Crear(ctx context.Context, p Producto) (int, error) {
    const query = `
        INSERT INTO productos (nombre, precio, stock)
        VALUES ($1, $2, $3)
        RETURNING id`

    var id int
    err := r.db.QueryRowContext(ctx, query, p.Nombre, p.Precio, p.Stock).Scan(&id)
    if err != nil {
        return 0, fmt.Errorf("insertar producto: %w", err)
    }
    return id, nil
}

Actualización y eliminación: ExecContext

func (r *productosRepo) ActualizarPrecio(ctx context.Context, id int, precio float64) error {
    resultado, err := r.db.ExecContext(ctx,
        "UPDATE productos SET precio = $1 WHERE id = $2", precio, id)
    if err != nil {
        return fmt.Errorf("actualizar: %w", err)
    }

    filas, err := resultado.RowsAffected()
    if err != nil {
        return fmt.Errorf("rows affected: %w", err)
    }
    if filas == 0 {
        return fmt.Errorf("producto %d: no encontrado", id)
    }
    return nil
}

func (r *productosRepo) Eliminar(ctx context.Context, id int) error {
    _, err := r.db.ExecContext(ctx, "DELETE FROM productos WHERE id = $1", id)
    return err
}

Transacciones

func (r *productosRepo) TransferirStock(ctx context.Context, origenID, destinoID, cantidad int) error {
    tx, err := r.db.BeginTx(ctx, nil)
    if err != nil {
        return fmt.Errorf("begin: %w", err)
    }
    defer tx.Rollback() // ignorado si Commit tiene éxito

    // Descontar stock del origen
    resultado, err := tx.ExecContext(ctx,
        "UPDATE productos SET stock = stock - $1 WHERE id = $2 AND stock >= $1",
        cantidad, origenID)
    if err != nil {
        return fmt.Errorf("descontar stock: %w", err)
    }
    if filas, _ := resultado.RowsAffected(); filas == 0 {
        return fmt.Errorf("stock insuficiente en producto %d", origenID)
    }

    // Añadir stock al destino
    _, err = tx.ExecContext(ctx,
        "UPDATE productos SET stock = stock + $1 WHERE id = $2",
        cantidad, destinoID)
    if err != nil {
        return fmt.Errorf("añadir stock: %w", err)
    }

    return tx.Commit()
}

Patrón repositorio

type ProductoRepositorio interface {
    ObtenerTodos(ctx context.Context) ([]Producto, error)
    ObtenerPorID(ctx context.Context, id int) (*Producto, error)
    Crear(ctx context.Context, p Producto) (int, error)
    ActualizarPrecio(ctx context.Context, id int, precio float64) error
    Eliminar(ctx context.Context, id int) error
}

type productosRepo struct {
    db *sql.DB
}

func NuevoProductosRepo(db *sql.DB) ProductoRepositorio {
    return &productosRepo{db: db}
}

La interfaz permite inyectar un mock en pruebas sin necesitar una base de datos real.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Go es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Go

Explora más contenido relacionado con Go y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Conectar a PostgreSQL usando database/sql y el driver pgx o lib/pq.
  • Ejecutar consultas SELECT con QueryContext y QueryRowContext.
  • Insertar, actualizar y eliminar registros con ExecContext.
  • Gestionar transacciones con BeginTx, Commit y Rollback.
  • Configurar el pool de conexiones para producción.
  • Implementar el patrón repositorio con interfaces para testabilidad.