tutoriales.com

Conectando Go con Bases de Datos SQL: Dominando el Paquete database/sql y Controladores de Bases de Datos

Este tutorial te guiará a través del proceso de conectar aplicaciones Go con bases de datos SQL, utilizando el paquete estándar `database/sql`. Cubriremos desde la configuración inicial hasta operaciones CRUD avanzadas, gestión de transacciones y mejores prácticas para garantizar un código robusto y eficiente.

Intermedio20 min de lectura6 views
Reportar error

El paquete database/sql de Go es la interfaz estándar para interactuar con bases de datos SQL. Es una abstracción que permite a los desarrolladores escribir código genérico para interactuar con diferentes tipos de bases de datos, siempre y cuando exista un controlador (driver) que implemente la interfaz database/sql para esa base de datos específica.

Este tutorial te proporcionará una comprensión profunda de cómo utilizar database/sql para construir aplicaciones Go que interactúen de manera efectiva con bases de datos relacionales.

🚀 Introducción al Paquete database/sql

El paquete database/sql no es un controlador de base de datos en sí mismo, sino una interfaz genérica que los controladores de bases de datos de terceros deben implementar. Esto significa que puedes cambiar fácilmente el tipo de base de datos (por ejemplo, de PostgreSQL a MySQL) sin tener que reescribir la mayor parte de tu lógica de acceso a datos.

💡 Consejo: Piensa en `database/sql` como una capa de abstracción. Tu código interactúa con esta capa, y la capa delega el trabajo específico al controlador de la base de datos que estés utilizando.

Componentes Clave

Los componentes principales con los que interactuarás son:

  • sql.DB: Representa un pool de conexiones a una base de datos. Es seguro para la concurrencia y debe crearse una única instancia por aplicación.
  • sql.Row: Representa una única fila de resultados de una consulta.
  • sql.Rows: Representa un conjunto de filas de resultados de una consulta.
  • sql.Tx: Representa una transacción en la base de datos.
  • sql.Stmt: Representa una sentencia SQL preparada.

🛠️ Configuración Inicial: Elegir y Conectar un Controlador

Antes de poder interactuar con una base de datos, necesitas un controlador específico para ella. Algunos de los controladores más populares incluyen:

  • PostgreSQL: github.com/lib/pq
  • MySQL: github.com/go-sql-driver/mysql
  • SQLite: github.com/mattn/go-sqlite3
  • SQL Server: github.com/denisenkom/go-mssqldb

Para este tutorial, utilizaremos go-sqlite3 por su facilidad de configuración y no requerir un servidor de base de datos externo. Los principios aplicados son los mismos para cualquier otra base de datos.

Paso 1: Instalar el Controlador

Primero, instala el controlador de SQLite:

go get github.com/mattn/go-sqlite3

Paso 2: Importar y Abrir la Conexión

Para usar el controlador, solo necesitas importarlo en tu código. La función init() del paquete del controlador registrará automáticamente el controlador con database/sql.

package main

import (
	"database/sql"
	"fmt"
	"log"
	// Importa el controlador de SQLite3. El guion bajo indica que se importa solo por sus efectos laterales (su función init()).
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// Abrir la conexión a la base de datos SQLite. Se crea un archivo 'test.db' si no existe.
	// El primer argumento es el nombre del controlador ('sqlite3').
	// El segundo argumento es la cadena de conexión específica del controlador.
	db, err := sql.Open("sqlite3", "./test.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close() // Asegúrate de cerrar la conexión cuando termines.

	// Verificar que la conexión está activa
	err = db.Ping()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("¡Conexión a la base de datos SQLite establecida con éxito!")
}
🔥 Importante: La función `sql.Open()` *no establece una conexión inmediata* a la base de datos. Solo valida los argumentos y prepara la estructura para futuras conexiones. La conexión real se establece en el primer acceso a la base de datos, como `db.Ping()`, `db.Exec()` o `db.Query()`.

⚙️ Configuración del Pool de Conexiones

Una de las características más importantes de sql.DB es que maneja un pool de conexiones. Esto significa que no necesitas abrir y cerrar conexiones repetidamente para cada operación, lo que mejora significativamente el rendimiento. Puedes configurar este pool:

  • db.SetMaxOpenConns(n int): Número máximo de conexiones abiertas a la base de datos. Las solicitudes adicionales esperarán.
  • db.SetMaxIdleConns(n int): Número máximo de conexiones inactivas en el pool. Si hay más conexiones inactivas que este número, se cerrarán.
  • db.SetConnMaxLifetime(d time.Duration): Tiempo máximo que una conexión puede ser reutilizada. Después de este tiempo, se cerrará y se abrirá una nueva.
// ... (código anterior)

db, err := sql.Open("sqlite3", "./test.db")
// ... (manejo de errores)

db.SetMaxOpenConns(10) // Máximo 10 conexiones abiertas
db.SetMaxIdleConns(5)  // Máximo 5 conexiones inactivas
db.SetConnMaxLifetime(5 * time.Minute) // Las conexiones se cierran después de 5 minutos de uso

// ... (código restante)
⚠️ Advertencia: Una mala configuración del pool de conexiones puede llevar a cuellos de botella de rendimiento o, peor aún, a que tu aplicación se quede sin conexiones de base de datos. Ajusta estos valores según la carga esperada y los recursos de tu servidor de BD.

📝 Operaciones CRUD Básicas

Ahora que tenemos la conexión configurada, podemos realizar operaciones de creación (Create), lectura (Read), actualización (Update) y eliminación (Delete).

Creando una Tabla

Primero, vamos a crear una tabla simple para almacenar usuarios.

package main

import (
	"database/sql"
	"fmt"
	"log"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

type User struct {
	ID    int
	Name  string
	Email string
}

func main() {
	db, err := sql.Open("sqlite3", "./test.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(5 * time.Minute)

	err = db.Ping()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("¡Conexión a la base de datos SQLite establecida con éxito!")

	// Crear tabla de usuarios
	sqlStmt := `
	CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		name TEXT NOT NULL,
		email TEXT NOT NULL UNIQUE
	);
	`
	_, err = db.Exec(sqlStmt)
	if err != nil {
		log.Fatalf("Error al crear tabla: %v", err)
	}
	fmt.Println("Tabla 'users' creada o ya existe.")

	// ... Realizar operaciones CRUD
}

➕ Insertar Datos (db.Exec)

Para sentencias SQL que no devuelven filas (INSERT, UPDATE, DELETE, CREATE TABLE), usa db.Exec().

// ... (dentro de main, después de crear la tabla)

	// Insertar un nuevo usuario
	result, err := db.Exec("INSERT INTO users(name, email) VALUES(?, ?)", "Alice", "alice@example.com")
	if err != nil {
		log.Printf("Error al insertar usuario: %v", err)
	} else {
		id, _ := result.LastInsertId()
		rowsAffected, _ := result.RowsAffected()
		fmt.Printf("Usuario insertado con ID: %d, Filas afectadas: %d\n", id, rowsAffected)
	}

	result, err = db.Exec("INSERT INTO users(name, email) VALUES(?, ?)", "Bob", "bob@example.com")
	if err != nil {
		log.Printf("Error al insertar usuario: %v", err)
	} else {
		id, _ := result.LastInsertId()
		rowsAffected, _ := result.RowsAffected()
		fmt.Printf("Usuario insertado con ID: %d, Filas afectadas: %d\n", id, rowsAffected)
	}

	// Intentar insertar un usuario con email duplicado para ver el error
	_, err = db.Exec("INSERT INTO users(name, email) VALUES(?, ?)", "Charlie", "alice@example.com")
	if err != nil {
		fmt.Printf("Error esperado al insertar usuario con email duplicado: %v\n", err)
	}

Nótese el uso de ? como placeholder para los parámetros. Es una práctica altamente recomendada para prevenir ataques de inyección SQL. El controlador se encargará de escapar los valores correctamente.

🔍 Leer Datos (db.Query y db.QueryRow)

Para sentencias SQL que devuelven filas (SELECT), usa db.Query() para múltiples filas y db.QueryRow() para una sola fila.

Leer múltiples usuarios

// ... (dentro de main)

	fmt.Println("\n--- Lista de Usuarios ---")
	rows, err := db.Query("SELECT id, name, email FROM users")
	if err != nil {
		log.Fatalf("Error al consultar usuarios: %v", err)
	}
	defer rows.Close() // Muy importante cerrar las filas para liberar recursos

	var users []User
	for rows.Next() {
		var user User
		err := rows.Scan(&user.ID, &user.Name, &user.Email)
		if err != nil {
			log.Printf("Error al escanear fila: %v", err)
			continue
		}
		users = append(users, user)
	}

	// Verificar si hubo algún error durante la iteración de las filas
	if err = rows.Err(); err != nil {
		log.Fatalf("Error durante la iteración de filas: %v", err)
	}

	for _, user := range users {
		fmt.Printf("ID: %d, Nombre: %s, Email: %s\n", user.ID, user.Name, user.Email)
	}
🔥 Importante: Siempre llama a `rows.Close()` con `defer` para asegurarte de que los recursos de la base de datos se liberen correctamente, incluso si ocurre un error. Además, es crucial verificar `rows.Err()` después del bucle `rows.Next()` para detectar cualquier error que pudiera haber ocurrido durante la iteración.

Leer un usuario por ID

// ... (dentro de main)

	fmt.Println("\n--- Buscar Usuario por ID ---")	
	var singleUser User
	err = db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", 1).Scan(&singleUser.ID, &singleUser.Name, &singleUser.Email)
	if err == sql.ErrNoRows {
		fmt.Println("No se encontró ningún usuario con ID 1.")
	} else if err != nil {
		log.Fatalf("Error al buscar usuario por ID: %v", err)
	} else {
		fmt.Printf("Usuario encontrado: ID: %d, Nombre: %s, Email: %s\n", singleUser.ID, singleUser.Name, singleUser.Email)
	}

	// Intentar buscar un ID que no existe
	var nonExistentUser User
	err = db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", 999).Scan(&nonExistentUser.ID, &nonExistentUser.Name, &nonExistentUser.Email)
	if err == sql.ErrNoRows {
		fmt.Println("No se encontró ningún usuario con ID 999 (esperado).")
	} else if err != nil {
		log.Fatalf("Error inesperado al buscar usuario por ID 999: %v", err)
	}

✏️ Actualizar Datos (db.Exec)

// ... (dentro de main)

	fmt.Println("\n--- Actualizar Usuario ---")
	result, err = db.Exec("UPDATE users SET name = ?, email = ? WHERE id = ?", "Alicia Wonderland", "alicia.w@example.com", 1)
	if err != nil {
		log.Fatalf("Error al actualizar usuario: %v", err)
	}
	rowsAffected, _ = result.RowsAffected()
	fmt.Printf("Filas actualizadas: %d\n", rowsAffected)

	// Verificar el cambio
	err = db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", 1).Scan(&singleUser.ID, &singleUser.Name, &singleUser.Email)
	if err != nil {
		log.Fatalf("Error al verificar actualización: %v", err)
	}
	fmt.Printf("Usuario actualizado: ID: %d, Nombre: %s, Email: %s\n", singleUser.ID, singleUser.Name, singleUser.Email)

🗑️ Eliminar Datos (db.Exec)

// ... (dentro de main)

	fmt.Println("\n--- Eliminar Usuario ---")
	result, err = db.Exec("DELETE FROM users WHERE id = ?", 2)
	if err != nil {
		log.Fatalf("Error al eliminar usuario: %v", err)
	}
	rowsAffected, _ = result.RowsAffected()
	fmt.Printf("Filas eliminadas: %d\n", rowsAffected)

	// Verificar que el usuario ya no existe
	err = db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", 2).Scan(&singleUser.ID, &singleUser.Name, &singleUser.Email)
	if err == sql.ErrNoRows {
		fmt.Println("Usuario con ID 2 eliminado correctamente (esperado).")
	} else if err != nil {
		log.Fatalf("Error inesperado al verificar eliminación: %v", err)
	}

🔄 Gestión de Transacciones (db.Begin)

Las transacciones son cruciales para mantener la integridad de los datos. Permiten agrupar múltiples operaciones SQL en una única unidad atómica: o todas las operaciones tienen éxito (COMMIT) o ninguna lo hace (ROLLBACK).

package main

import (
	"database/sql"
	"fmt"
	"log"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

type Account struct {
	ID      int
	Owner   string
	Balance float64
}

func main() {
	db, err := sql.Open("sqlite3", "./test.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(5 * time.Minute)

	// Crear tabla de cuentas si no existe
	sqlStmt := `
	CREATE TABLE IF NOT EXISTS accounts (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		owner TEXT NOT NULL,
		balance REAL NOT NULL
	);
	`
	_, err = db.Exec(sqlStmt)
	if err != nil {
		log.Fatalf("Error al crear tabla de cuentas: %v", err)
	}

	// Insertar algunas cuentas iniciales
	db.Exec("DELETE FROM accounts") // Limpiar para el ejemplo
	db.Exec("INSERT INTO accounts(owner, balance) VALUES(?, ?)", "Juan", 1000.00)
	db.Exec("INSERT INTO accounts(owner, balance) VALUES(?, ?)", "Maria", 500.00)

	fmt.Println("\n--- Simulación de Transferencia Bancaria con Transacción ---")

	// Iniciar una transacción
	tx, err := db.Begin()
	if err != nil {
		log.Fatal(err)
	}

	// Intentar transferir 200 de Juan a Maria
	fromAccountID := 1
	toAccountID := 2
	amount := 200.00

	// 1. Retirar dinero de Juan
	_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromAccountID)
	
	if err != nil {
		tx.Rollback() // Si hay error, revertir todo
		log.Fatalf("Error al retirar de la cuenta de origen: %v", err)
	}

	// 2. Insertar dinero a Maria
	_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toAccountID)
	if err != nil {
		tx.Rollback() // Si hay error, revertir todo
		log.Fatalf("Error al depositar en la cuenta de destino: %v", err)
	}

	// Si todo fue bien, confirmar la transacción
	err = tx.Commit()
	if err != nil {
		log.Fatalf("Error al confirmar la transacción: %v", err)
	}

	fmt.Println("Transferencia de 200 de Juan a Maria realizada con éxito.")

	// Mostrar saldos finales
	var juan Account
	db.QueryRow("SELECT id, owner, balance FROM accounts WHERE id = ?", fromAccountID).Scan(&juan.ID, &juan.Owner, &juan.Balance)
	fmt.Printf("Saldo de %s: %.2f\n", juan.Owner, juan.Balance)

	var maria Account
	db.QueryRow("SELECT id, owner, balance FROM accounts WHERE id = ?", toAccountID).Scan(&maria.ID, &maria.Owner, &maria.Balance)
	fmt.Printf("Saldo de %s: %.2f\n", maria.Owner, maria.Balance)
}
Inicio db.Begin() Actualizar Cuenta A Actualizar Cuenta B ¿Error? No tx.Rollback() tx.Commit() Fin
💡 Consejo: Es una buena práctica usar `defer tx.Rollback()` justo después de `tx, err := db.Begin()`. Luego, al final, si todo ha ido bien, llamas a `tx.Commit()`. Si ocurre un error antes de `Commit()`, `Rollback()` ya estará programado para ejecutarse.

🛡️ Sentencias Preparadas (db.Prepare)

Las sentencias preparadas son útiles por varias razones:

  1. Seguridad: Previenen la inyección SQL al separar la lógica de la consulta de los datos de los parámetros.
  2. Rendimiento: La base de datos puede precompilar la consulta, lo que mejora el rendimiento si la misma consulta se ejecuta varias veces con diferentes parámetros.
package main

import (
	"database/sql"
	"fmt"
	"log"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

func main() {
	db, err := sql.Open("sqlite3", "./test.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(5 * time.Minute)

	// Asegurarse de que la tabla 'productos' existe
	_, err = db.Exec(`
	CREATE TABLE IF NOT EXISTS productos (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		name TEXT NOT NULL,
		price REAL NOT NULL
	);
	`)
	if err != nil {
		log.Fatalf("Error al crear tabla productos: %v", err)
	}
	db.Exec("DELETE FROM productos") // Limpiar para el ejemplo

	fmt.Println("\n--- Insertando múltiples productos con sentencia preparada ---")
	
	// Preparar la sentencia para insertar productos
	stmt, err := db.Prepare("INSERT INTO productos(name, price) VALUES(?, ?)")
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close() // Cierra la sentencia preparada cuando ya no se necesite

	// Ejecutar la sentencia varias veces con diferentes valores
	products := []struct { Name string; Price float64 }{
		{"Laptop", 1200.50},
		{"Mouse", 25.99},
		{"Teclado", 75.00},
	}

	for _, p := range products {
		_, err := stmt.Exec(p.Name, p.Price)
		if err != nil {
			log.Printf("Error al insertar %s: %v", p.Name, err)
		} else {
			fmt.Printf("Producto '%s' insertado.\n", p.Name)
		}
	}

	fmt.Println("\n--- Consultando productos con sentencia preparada ---")
	
	// Preparar la sentencia para consultar un producto por nombre
	stmtQuery, err := db.Prepare("SELECT id, name, price FROM productos WHERE name = ?")
	if err != nil {
		log.Fatal(err)
	}
	defer stmtQuery.Close()

	var id int
	var name string
	var price float64

	err = stmtQuery.QueryRow("Laptop").Scan(&id, &name, &price)
	if err == sql.ErrNoRows {
		fmt.Println("Laptop no encontrada.")
	} else if err != nil {
		log.Fatalf("Error al consultar laptop: %v", err)
	} else {
		fmt.Printf("Producto encontrado: ID=%d, Nombre='%s', Precio=%.2f\n", id, name, price)
	}
}

✨ Tipos Nulables (sql.Null*)

En las bases de datos SQL, algunas columnas pueden aceptar valores NULL. Los tipos primitivos de Go (como int, string, bool, float64) no pueden representar NULL. El paquete database/sql proporciona tipos especiales para manejar esto:

  • sql.NullString
  • sql.NullInt32
  • sql.NullInt64
  • sql.NullBool
  • sql.NullFloat64
  • sql.NullTime (del paquete time)

Cada uno de estos tipos tiene un campo Valid (bool) que indica si el valor es realmente NULL o si tiene un valor real, y un campo para el valor (String, Int32, etc.).

package main

import (
	"database/sql"
	"fmt"
	"log"
	"time"

	_ "github.com/mattn/go-sqlite3"
)

type Product struct {
	ID        int
	Name      string
	Description sql.NullString // Puede ser NULL
	Price     float64
}

func main() {
	db, err := sql.Open("sqlite3", "./test.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	db.SetConnMaxLifetime(5 * time.Minute)

	// Crear tabla de productos con descripción opcional
	_, err = db.Exec(`
	CREATE TABLE IF NOT EXISTS products_nullable (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		name TEXT NOT NULL,
		description TEXT,
		price REAL NOT NULL
	);
	`)
	if err != nil {
		log.Fatalf("Error al crear tabla products_nullable: %v", err)
	}
	db.Exec("DELETE FROM products_nullable")

	fmt.Println("\n--- Manejo de valores NULL ---")

	// Insertar un producto con descripción
	_, err = db.Exec("INSERT INTO products_nullable(name, description, price) VALUES(?, ?, ?)", "Monitor", "Monitor de alta resolución", 300.00)
	if err != nil {
		log.Printf("Error al insertar monitor: %v", err)
	}

	// Insertar un producto sin descripción (NULL)
	_, err = db.Exec("INSERT INTO products_nullable(name, description, price) VALUES(?, ?, ?)", "Webcam", nil, 50.00)
	if err != nil {
		log.Printf("Error al insertar webcam: %v", err)
	}

	// Leer productos y manejar la descripción nulable
	rows, err := db.Query("SELECT id, name, description, price FROM products_nullable")
	if err != nil {
		log.Fatalf("Error al consultar productos: %v", err)
	}
	defer rows.Close()

	for rows.Next() {
		var p Product
		err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Price)
		if err != nil {
			log.Printf("Error al escanear fila: %v", err)
			continue
		}

		desc := "N/A"
		if p.Description.Valid {
			desc = p.Description.String
		}
		fmt.Printf("ID: %d, Nombre: %s, Descripción: %s, Precio: %.2f\n", p.ID, p.Name, desc, p.Price)
	}

	if err = rows.Err(); err != nil {
		log.Fatalf("Error durante la iteración de filas: %v", err)
	}
}

💡 Buenas Prácticas y Consejos Avanzados

Siempre Cierra los Recursos

  • db.Close(): Cierra el pool de conexiones. Se debe llamar una vez al finalizar la aplicación.
  • rows.Close(): Libera los recursos asociados a un conjunto de resultados. Es vital usar defer rows.Close().
  • stmt.Close(): Libera los recursos de una sentencia preparada. También debe usarse defer.

Gestión de Errores

Siempre verifica los errores devueltos por las funciones de database/sql. sql.ErrNoRows es un error común y esperado que indica que una consulta no devolvió resultados.

Concurrencia

El tipo sql.DB es seguro para la concurrencia y está diseñado para ser compartido entre múltiples goroutines. Cada llamada a db.Query(), db.Exec(), etc., obtendrá una conexión del pool (si hay una disponible o se puede crear una nueva) y la devolverá al pool una vez completada la operación. No crees múltiples instancias de sql.DB.

Uso de Estructuras para Mapear Filas

Para consultas más complejas o cuando trabajas con muchos campos, es común crear una función auxiliar para escanear una fila en una estructura:

func scanUser(row *sql.Row) (User, error) {
	var u User
	err := row.Scan(&u.ID, &u.Name, &u.Email)
	return u, err
}

// Luego, en tu código:
// user, err := scanUser(db.QueryRow("SELECT ... WHERE id = ?", 1))

O para múltiples filas:

func scanUsers(rows *sql.Rows) ([]User, error) {
	var users []User
	for rows.Next() {
		var u User
		if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
			return nil, err
		}
		users = append(users, u)
	}
	if err := rows.Err(); err != nil {
		return nil, err	
	}
	return users, nil
}

// Luego, en tu código:
// users, err := scanUsers(db.Query("SELECT ..."))

Abstracciones y ORMs Ligeros

Para proyectos más grandes, podrías considerar el uso de una capa de abstracción más alta o un ORM (Object-Relational Mapper) ligero que se construya sobre database/sql para reducir el boilerplate. Algunos ejemplos populares incluyen:

  • GORM: Un ORM completo y popular.
  • SQLBoiler: Genera ORMs para Go desde tu esquema de base de datos.
  • Squirrel: Un constructor de consultas SQL fluido y seguro.

Aunque estos pueden simplificar el código, entender database/sql es fundamental para depurar problemas y optimizar el rendimiento.

📌 Nota: Este tutorial se centra en `database/sql`. Elegir un ORM es una decisión de diseño para proyectos más grandes, pero la base siempre será el paquete estándar.

Esquema de Migraciones

Para gestionar los cambios en el esquema de tu base de datos a lo largo del tiempo, es crucial usar herramientas de migración. Algunas opciones populares para Go son:

  • golang-migrate: Herramienta CLI y biblioteca para migraciones de bases de datos.
  • goose: Herramienta de migración con énfasis en la simplicidad.

Estas herramientas te permiten definir tus cambios de esquema como scripts SQL (o Go) y aplicarlos de forma controlada a diferentes entornos.


📊 Resumen de Comandos y Flujos

Aquí tienes un resumen rápido de los comandos más importantes de database/sql:

OperaciónFunción PrincipalDescripciónRetorno Clave
------------
Abrir Conexiónsql.Open()Inicializa un pool de conexiones, no conecta de inmediato.*sql.DB
Cerrar Conexióndb.Close()Cierra el pool de conexiones.error
------------
Ejecutar SQL (sin resultados)db.Exec(), tx.Exec()Ejecuta INSERT, UPDATE, DELETE, CREATE, etc.sql.Result, error
Consultar una filadb.QueryRow()Ejecuta SELECT y espera una única fila.*sql.Row
------------
Consultar múltiples filasdb.Query()Ejecuta SELECT y espera cero o más filas.*sql.Rows, error
Escanear resultadosrow.Scan(), rows.Scan()Mapea columnas de resultados a variables Go.error
------------
Iniciar Transaccióndb.Begin()Inicia una nueva transacción.*sql.Tx, error
Confirmar Transaccióntx.Commit()Confirma todas las operaciones de la transacción.error
------------
Revertir Transaccióntx.Rollback()Revierte todas las operaciones de la transacción.error
Preparar Sentenciadb.Prepare()Prepara una sentencia SQL para ejecución eficiente y segura.*sql.Stmt, error
------------
Ejecutar Sentencia Preparadastmt.Exec(), stmt.Query(), stmt.QueryRow()Ejecuta una sentencia preparada.sql.Result/*sql.Rows/*sql.Row
Tutorial Completo

Este tutorial te ha proporcionado las herramientas fundamentales para interactuar con bases de datos SQL en Go utilizando el paquete database/sql. Al dominar estos conceptos, estarás bien equipado para construir aplicaciones robustas y eficientes que persistan datos de manera confiable. ¡Sigue experimentando y construyendo!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!