Go has SQLite drivers that need cgo and drivers that don’t. The difference shows up when you build with CGO_ENABLED=0: go-sqlite3 then fails on its first call with a message about a stub. In this article I’ll walk you through the same small program with go-sqlite3 and with modernc.org/sqlite, which needs no cgo.
TLDR
- Go talks to SQLite through the standard
database/sqlpackage plus a driver. github.com/mattn/go-sqlite3wraps the C library. It needs cgo and a C compiler such asgcc.modernc.org/sqliteis a pure Go port, so it builds withCGO_ENABLED=0. Its driver name is"sqlite", not"sqlite3".- Pass values with
?placeholders, and group writes withdb.Begin().
What is Go SQLite and what will we build?
SQLite is a database engine that keeps a whole database in one file and runs inside your program, with no server. Go reaches it through database/sql, the standard library’s interface for SQL databases, and a driver package registers SQLite with it.
Here are the two drivers I tested:
| Driver | Import | sql.Open name | Needs cgo |
|---|---|---|---|
| mattn/go-sqlite3 | github.com/mattn/go-sqlite3 | "sqlite3" | Yes. The README says “you are required to set the environment variable CGO_ENABLED=1 and have a gcc compiler present” |
| modernc.org/sqlite | modernc.org/sqlite | "sqlite" | No. It’s “a CGo-free port of the C SQLite3 library” |
To show you both, I will build a small books program. It creates a table and inserts rows, two of them in a transaction. Then it reads them back and edits them.
Prerequisites
| Requirement | Version tested |
|---|---|
| Go | 1.27.1 (golang:1 Docker image, linux/arm64) |
| C compiler, for go-sqlite3 only | gcc 14.2.0 |
| mattn/go-sqlite3 | v1.14.52 |
| modernc.org/sqlite | v1.60.1 |
Both drivers bundle their own copy of SQLite, so you don’t need SQLite installed. In this test, both reported SQLite 3.53.4.
Building the books program
The whole program is one main.go. I’ll build it with go-sqlite3 first, then switch the driver.
Step 1: Create the project and add the driver
$ go mod init example.com/books
go: creating new go.mod: module example.com/books
go: to add module requirements and sums:
go mod tidy
$ go get github.com/mattn/go-sqlite3@latest 2>&1 | grep -E '^go: added'
go: added github.com/mattn/go-sqlite3 v1.14.52
Step 2: Open the database
sql.Open doesn’t create the file by itself. SQLite creates books.db the first time the program uses the connection. The _foreign_keys=on parameter turns on foreign key checks, which SQLite leaves off by default (go-sqlite3 connection parameters).
Step 3: Write the program
Here’s the whole file. Comments mark the main parts:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
type Book struct {
ID int64
Title string
Author string
Year int
}
func main() {
// Opens books.db, creating the file on first use.
db, err := sql.Open("sqlite3", "file:books.db?_foreign_keys=on")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
year INTEGER
)`); err != nil {
log.Fatal(err)
}
// Insert with placeholders, never with string formatting.
res, err := db.Exec("INSERT INTO books (title, author, year) VALUES (?, ?, ?)",
"The Go Programming Language", "Donovan and Kernighan", 2015)
if err != nil {
log.Fatal(err)
}
id, _ := res.LastInsertId()
fmt.Println("inserted id:", id)
// Several inserts in one transaction.
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
stmt, err := tx.Prepare("INSERT INTO books (title, author, year) VALUES (?, ?, ?)")
if err != nil {
log.Fatal(err)
}
for _, b := range []Book{
{Title: "SQL Antipatterns", Author: "Karwin", Year: 2010},
{Title: "Designing Data-Intensive Applications", Author: "Kleppmann", Year: 2017},
} {
if _, err := stmt.Exec(b.Title, b.Author, b.Year); err != nil {
tx.Rollback()
log.Fatal(err)
}
}
stmt.Close()
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
// Query many rows.
rows, err := db.Query("SELECT id, title, year FROM books WHERE year >= ? ORDER BY year", 2012)
if err != nil {
log.Fatal(err)
}
for rows.Next() {
var b Book
if err := rows.Scan(&b.ID, &b.Title, &b.Year); err != nil {
log.Fatal(err)
}
fmt.Printf("%d %-40s %d\n", b.ID, b.Title, b.Year)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
rows.Close()
// Query one row.
var count int
if err := db.QueryRow("SELECT count(*) FROM books").Scan(&count); err != nil {
log.Fatal(err)
}
fmt.Println("books:", count)
// A row that doesn't exist.
var title string
err = db.QueryRow("SELECT title FROM books WHERE id = ?", 99).Scan(&title)
if err == sql.ErrNoRows {
fmt.Println("no book with id 99")
}
// Update and delete.
res, _ = db.Exec("UPDATE books SET year = ? WHERE author = ?", 2016, "Donovan and Kernighan")
n, _ := res.RowsAffected()
fmt.Println("updated:", n)
res, _ = db.Exec("DELETE FROM books WHERE year < ?", 2012)
n, _ = res.RowsAffected()
fmt.Println("deleted:", n)
var version string
db.QueryRow("SELECT sqlite_version()").Scan(&version)
fmt.Println("SQLite version:", version)
}
These are the parts worth noticing:
- Placeholders.
?passes values safely. Never build SQL withfmt.Sprintffrom user input. - Transactions.
tx.Prepareplustx.Commitmakes the two inserts all-or-nothing, and it’s faster than separate commits for many rows. See SQLite transactions. - Rows. Always
rows.Close(), and checkrows.Err()after the loop. - A single row.
QueryRow(...).Scanreturnssql.ErrNoRowswhen there’s no match.
Step 4: Switch to the pure Go driver if you can’t use cgo
Only the import and the sql.Open line change. modernc.org/sqlite sets pragmas with _pragma=. Here’s the diff between the two versions of main.go:
$ diff mattn/main.go modernc/main.go
8c8
< _ "github.com/mattn/go-sqlite3"
---
> _ "modernc.org/sqlite"
20c20
< db, err := sql.Open("sqlite3", "file:books.db?_foreign_keys=on")
---
> db, err := sql.Open("sqlite", "file:books.db?_pragma=foreign_keys(1)")
Running it
With go-sqlite3:
$ go run .
inserted id: 1
1 The Go Programming Language 2015
3 Designing Data-Intensive Applications 2017
books: 3
no book with id 99
updated: 1
deleted: 1
SQLite version: 3.53.4
Row 2 (2010) didn’t print, because the query asks for books from 2012 on. It’s also the row the final DELETE removed.
For modernc.org/sqlite, add the driver in a separate folder. Then run the same program with cgo turned off. The output matches:
$ go mod init example.com/books
go: creating new go.mod: module example.com/books
go: to add module requirements and sums:
go mod tidy
$ go get modernc.org/sqlite@latest 2>&1 | grep -E '^go: added modernc.org/sqlite '
go: added modernc.org/sqlite v1.60.1
$ CGO_ENABLED=0 go run .
inserted id: 1
1 The Go Programming Language 2015
3 Designing Data-Intensive Applications 2017
books: 3
no book with id 99
updated: 1
deleted: 1
SQLite version: 3.53.4
Common problems
The first error below came from the test run. The other two are explained from the driver docs.
Binary was compiled with ‘CGO_ENABLED=0’, go-sqlite3 requires cgo to work
$ CGO_ENABLED=0 go run .
2026/10/07 08:29:58 Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub
exit status 1
go-sqlite3 needs cgo. Build with CGO_ENABLED=1 and a C compiler installed, or switch to modernc.org/sqlite. It’s the option to reach for when you cross-compile, or when you build small container images without a C toolchain.
sql: unknown driver “sqlite3”
The driver name doesn’t match the import. go-sqlite3 registers "sqlite3", and modernc.org/sqlite registers "sqlite". Also check that the blank import _ "…" is there, because that’s what registers the driver.
database is locked
Another connection was writing when yours tried to. database/sql keeps a pool of connections, so this can happen inside one program. See database is locked for the fixes.
Conclusion
For a Go program that builds with cgo, go-sqlite3 is the long-standing choice. If you need CGO_ENABLED=0 to cross-compile, use modernc.org/sqlite. Your database/sql code stays the same, apart from the driver name.
Further reading
- SQLite INSERT
- SQLite SELECT
- SQLite UPDATE
- SQLite DELETE
- SQLite advantages
- Python sqlite3
- Java SQLite
- Rust SQLite
- Go docs: database/sql, mattn/go-sqlite3, modernc.org/sqlite
Tested with Go 1.27.1, go-sqlite3 v1.14.52 and modernc.org/sqlite v1.60.1 (both bundling SQLite 3.53.4) on 7 October 2026.
