SQLite es un archivo, no un servidor: no hay nada que instalar ni que arrancar. Con el controlador modernc.org/sqlite, que está escrito en Go puro, tampoco hace falta un compilador de C. Es la base de datos ideal para una herramienta de línea de órdenes, una caché local o las pruebas.
También te puede interesar
El controlador
go mod init ejemplo/biblioteca go get modernc.org/sqlite
import ( "database/sql" _ "modernc.org/sqlite" // el guion bajo: se importa solo por su efecto )
_ delante del import significa «no voy a usar ningún nombre de este paquete, pero quiero que se cargue». Al cargarse, su init() se registra en database/sql con el nombre "sqlite". Sin el guion bajo, el compilador se queja de un import sin usar; sin el import entero, sql.Open falla con unknown driver.Hay dos controladores habituales. mattn/go-sqlite3 envuelve el SQLite de C y necesita cgo, lo que complica la compilación cruzada y pide un compilador de C. modernc.org/sqlite es una traducción de SQLite a Go: compila en cualquier parte con CGO_ENABLED=0, a cambio de ser algo más lento. Para casi todo, el segundo.
Abrir y crear la tabla
db, err := sql.Open("sqlite", "biblioteca.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatal("no se puede abrir la base:", err)
}
_, err = db.Exec(`
CREATE TABLE libros (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
autor TEXT NOT NULL,
anio INTEGER NOT NULL,
notas TEXT,
UNIQUE(titulo, autor)
)`)
sql.Open no abre nada. Prepara un grupo de conexiones y devuelve sin tocar el disco; por eso casi nunca falla, ni aunque la ruta sea absurda. La primera conexión de verdad ocurre en la primera consulta, o cuando se lo pides tú con db.Ping(). Si quieres que el programa falle al arrancar y no en mitad del trabajo, el Ping es obligatorio.El *sql.DB es un grupo de conexiones, no una conexión: se crea una vez al arrancar, se comparte entre todas las goroutines —es seguro— y se cierra al final. Abrirlo y cerrarlo en cada función es un error de principiante que se paga caro.
Insertar
res, err := db.Exec(
`INSERT INTO libros (titulo, autor, anio) VALUES (?, ?, ?)`,
"Rayuela", "Julio Cortázar", 1963)
if err != nil {
log.Fatal(err)
}
id, _ := res.LastInsertId()
fmt.Println("insertado con id", id)
salidainsertado con id 1
Exec es para lo que no devuelve filas: INSERT, UPDATE, DELETE, CREATE. Devuelve un sql.Result con LastInsertId() y RowsAffected().
Varios de golpe: una transacción
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
stmt, err := tx.Prepare(`INSERT INTO libros (titulo, autor, anio) VALUES (?, ?, ?)`)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
for _, l := range libros {
if _, err := stmt.Exec(l...); err != nil {
tx.Rollback()
log.Fatal(err)
}
}
stmt.Close()
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
salidainsertados 4 más en una transacción
En SQLite esto no es una cuestión de pureza: cada INSERT suelto es una transacción implícita con su sincronización a disco. Meter mil filas una a una frente a hacerlo dentro de una transacción es una diferencia de dos órdenes de magnitud. El Prepare además compila la consulta una vez en lugar de mil.
Consultar
filas, err := db.Query(
`SELECT id, titulo, autor, anio, notas FROM libros WHERE anio < ? ORDER BY anio`, 1990)
if err != nil {
log.Fatal(err)
}
defer filas.Close()
for filas.Next() {
var l Libro
if err := filas.Scan(&l.ID, &l.Titulo, &l.Autor, &l.Anio, &l.Notas); err != nil {
log.Fatal(err)
}
...
}
if err := filas.Err(); err != nil { // el error que el bucle no vio
log.Fatal(err)
}
salidalibros anteriores a 1990: 5 Ficciones Jorge Luis Borges 1944 (sin notas) 2 Pedro Páramo Juan Rulfo 1955 (sin notas) 1 Rayuela Julio Cortázar 1963 (sin notas) 3 La casa de los espíritus Isabel Allende 1982 (sin notas)
defer filas.Close(), o la conexión no vuelve al grupo y acabas agotándolo. filas.Err() después del bucle, porque Next() devuelve false tanto al terminar como al fallar y sin esa comprobación un error de red se convierte en «no había resultados». Y los & del Scan: necesita punteros, y el número y el orden tienen que coincidir con el SELECT.Una sola fila
var cuantos int
if err := db.QueryRow(`SELECT COUNT(*) FROM libros`).Scan(&cuantos); err != nil {
log.Fatal(err)
}
var t string
err = db.QueryRow(`SELECT titulo FROM libros WHERE autor = ?`, "Nadie").Scan(&t)
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("no hay ningún libro de ese autor")
}
salidatotal de libros: 5 no hay ningún libro de ese autor
QueryRow no devuelve error: lo guarda y te lo da en el Scan. Y «no hay ninguna fila» llega como sql.ErrNoRows, que muchas veces no es un fallo sino una respuesta: hay que distinguirlo con errors.Is.
Las columnas que pueden ser NULL
var notas sql.NullString
filas.Scan(..., ¬as)
if notas.Valid {
fmt.Println(notas.String)
}
Si escaneas una columna NULL sobre un string normal, revienta con converting NULL to string is unsupported. Los tipos sql.NullString, sql.NullInt64, sql.NullTime y compañía tienen el valor y un Valid. Desde Go 1.22 también está el genérico sql.Null[T]. La otra opción, a menudo mejor, es un *string: queda nil cuando es nulo.
Actualizar, borrar y los errores de la base
res, _ = db.Exec(`UPDATE libros SET notas = ? WHERE autor = ?`, "prestado", "Juan Rulfo") n, _ := res.RowsAffected() res, _ = db.Exec(`DELETE FROM libros WHERE anio > ?`, 1990) n, _ = res.RowsAffected()
salidafilas actualizadas: 1 filas borradas: 1
Y lo que pasa al chocar con la restricción UNIQUE(titulo, autor):
salidaal repetir: constraint failed: UNIQUE constraint failed: libros.titulo, libros.autor (2067)
El 2067 es el código extendido de SQLite para constraint violation / unique. Si necesitas distinguir ese caso de otros errores —por ejemplo para responder «ya existe» en vez de «error interno»— se compara contra sqlite3.ResultCodeConstraintUnique en lugar de buscar texto dentro del mensaje.
Una transacción con rollback automático
Mover dinero entre dos cuentas: o pasan las dos cosas o no pasa ninguna. El patrón con defer y un resultado con nombre evita tener que acordarse del Rollback en cada return:
func mover(db *sql.DB, de, a string, cantidad int) (err error) {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
}()
if _, err = tx.Exec(`UPDATE cuentas SET saldo = saldo - ? WHERE nombre = ?`, cantidad, de); err != nil {
return err
}
var saldo int
if err = tx.QueryRow(`SELECT saldo FROM cuentas WHERE nombre = ?`, de).Scan(&saldo); err != nil {
return err
}
if saldo < 0 {
return fmt.Errorf("%s se queda en %d: no hay saldo", de, saldo)
}
_, err = tx.Exec(`UPDATE cuentas SET saldo = saldo + ? WHERE nombre = ?`, cantidad, a)
return err
}
salidaal empezar: ana=100 luis=30 tras mover 40: ana=60 luis=70 error: luis se queda en -430: no hay saldo tras intentar 500: ana=60 luis=70
La segunda operación ya había restado los 500 cuando se detectó el problema, y aun así los saldos no se movieron: el Rollback deshizo el UPDATE. Esa es toda la gracia de una transacción.
El truco está en el (err error) del final de la firma: al tener nombre, el defer puede leer el valor que se va a devolver y decidir entre confirmar y deshacer.
Los interrogantes no son concatenación
malo := "ana'; DROP TABLE cuentas; --" var n int db.QueryRow(`SELECT COUNT(*) FROM cuentas WHERE nombre = ?`, malo).Scan(&n)
salidabuscando "ana'; DROP TABLE cuentas; --" -> 0 filas, y la tabla sigue ahí: ana=60 luis=70
El valor viaja aparte de la consulta y la base de datos nunca lo interpreta como SQL: para ella es un nombre de cliente larguísimo y raro, que simplemente no existe. Esto deja de ser cierto en cuanto construyes la consulta con fmt.Sprintf o con +, y ahí es donde aparecen las inyecciones de SQL.
SELECT * FROM ? no funciona. Si de verdad necesitas que sean variables, valídalos contra una lista blanca de nombres permitidos, nunca los pegues tal cual.Resumen
| Para | Usa | Y no olvides |
|---|---|---|
| Abrir | sql.Open("sqlite", ruta) |
db.Ping(): Open no conecta |
| INSERT / UPDATE / DELETE | db.Exec |
RowsAffected() |
| Varias filas | db.Query |
defer Close() y filas.Err() |
| Una fila | db.QueryRow |
errors.Is(err, sql.ErrNoRows) |
| Columnas nulas | sql.NullString o *string |
Comprobar .Valid |
| Muchas inserciones | db.Begin + Prepare |
Un solo Commit al final |
| Valores del usuario | Siempre ? |
Nunca fmt.Sprintf |
Y una recomendación que no es de Go sino de SQLite: si varias goroutines van a escribir, activa el modo WAL añadiendo ?_pragma=journal_mode(WAL) a la ruta. Sin él, los escritores se bloquean entre sí mucho más de lo necesario.
Todo el código se compiló y ejecutó con Go 1.25 y modernc.org/sqlite v1.59.0 antes de publicarla; las salidas y los mensajes de error están copiados de esa ejecución. Documentación oficial: database/sql y modernc.org/sqlite.