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:

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
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.