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.
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.
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!")
}
⚙️ 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)
📝 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)
}
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)
}
🛡️ Sentencias Preparadas (db.Prepare)
Las sentencias preparadas son útiles por varias razones:
- Seguridad: Previenen la inyección SQL al separar la lógica de la consulta de los datos de los parámetros.
- 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.NullStringsql.NullInt32sql.NullInt64sql.NullBoolsql.NullFloat64sql.NullTime(del paquetetime)
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 usardefer rows.Close().stmt.Close(): Libera los recursos de una sentencia preparada. También debe usarsedefer.
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.
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ón | Función Principal | Descripción | Retorno Clave |
|---|---|---|---|
| --- | --- | --- | --- |
| Abrir Conexión | sql.Open() | Inicializa un pool de conexiones, no conecta de inmediato. | *sql.DB |
| Cerrar Conexión | db.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 fila | db.QueryRow() | Ejecuta SELECT y espera una única fila. | *sql.Row |
| --- | --- | --- | --- |
| Consultar múltiples filas | db.Query() | Ejecuta SELECT y espera cero o más filas. | *sql.Rows, error |
| Escanear resultados | row.Scan(), rows.Scan() | Mapea columnas de resultados a variables Go. | error |
| --- | --- | --- | --- |
| Iniciar Transacción | db.Begin() | Inicia una nueva transacción. | *sql.Tx, error |
| Confirmar Transacción | tx.Commit() | Confirma todas las operaciones de la transacción. | error |
| --- | --- | --- | --- |
| Revertir Transacción | tx.Rollback() | Revierte todas las operaciones de la transacción. | error |
| Preparar Sentencia | db.Prepare() | Prepara una sentencia SQL para ejecución eficiente y segura. | *sql.Stmt, error |
| --- | --- | --- | --- |
| Ejecutar Sentencia Preparada | stmt.Exec(), stmt.Query(), stmt.QueryRow() | Ejecuta una sentencia preparada. | sql.Result/*sql.Rows/*sql.Row |
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
- Desarrollo de APIs RESTful en Go: Creando Servicios Web Eficientes con Gin Gonicintermediate20 min
- Desarrollo de CLI Robustas en Go: Construyendo Herramientas de Línea de Comandos Interactivasintermediate25 min
- Desarrollo de Microservicios Basados en Eventos en Go: Implementando un Patrón Saga con NATSintermediate35 min
- Manejo Eficiente de Errores en Go: Estrategias y Buenas Prácticas para Código Robustointermediate10 min
- Serialización y Deserialización en Go: Manejo de JSON, XML y Gob para Intercambio de Datosintermediate20 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!