How to Connect to SQLite from Go
Connecting to SQLite from Go forces a decision the moment you pick a driver: do you accept a C dependency or not? SQLite is a C library, and the long-standing default driver, mattn/go-sqlite3, binds to it through cgo. That means a C compiler at build time and a harder cross-compile story. The pure-Go alternatives transpile or reimplement SQLite so your binary stays cgo-free. This guide shows working code for the main options, explains the tradeoff that actually matters, and covers the SQLite-specific configuration most tutorials skip: WAL mode, busy_timeout, and why an embedded database barely needs a pool.
The options
| Driver | Import path | cgo | Driver name | Version (as of June 2026) |
|---|---|---|---|---|
| mattn/go-sqlite3 | github.com/mattn/go-sqlite3 | yes | sqlite3 | v1.14.45 |
| modernc.org/sqlite | modernc.org/sqlite | no | sqlite | v1.52.0 |
| ncruces/go-sqlite3 | github.com/ncruces/go-sqlite3/driver | no | sqlite3 | v0.35.0 |
The decision tree:
- You're fine with cgo and want the de-facto standard:
mattn/go-sqlite3. It is a thin binding over the real SQLite C library, so behavior matches upstream SQLite exactly, and it has the largest install base and tutorial coverage. The cost is a C toolchain at build time and friction when cross-compiling. - You want a pure-Go binary (easy cross-compilation, static builds, no C compiler):
modernc.org/sqlite. It is SQLite's C source transpiled to Go, so there is no cgo. This is the common choice for projects that cross-compile or want a single statically linked binary. It tends to be somewhat slower than the cgo driver on write-heavy benchmarks, but for most applications the difference is not the bottleneck. - You want pure Go via a WASM-embedded SQLite:
ncruces/go-sqlite3. It runs an actual SQLite WASM build inside a Go runtime. Also cgo-free, with good standards conformance, and it exposes some lower-level SQLite features through its own API in addition to adatabase/sqldriver.
All three implement Go's standard database/sql interface, so your query code is nearly identical across them. The import you choose and the driver name string are the only things that change.
The cgo decision in one paragraph
If you have ever run CGO_ENABLED=0 go build and watched a project that uses mattn/go-sqlite3 fail to link, this is the whole story. cgo ties your build to a C compiler for the target platform. Building a Linux binary on a Mac, or a small static binary for a scratch Docker image, becomes a cross-compilation exercise. Pure-Go drivers (modernc.org/sqlite, ncruces/go-sqlite3) sidestep all of it: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build just works. That single property is why a lot of teams moved off the cgo driver despite its longer track record.
Connecting with database/sql
Because SQLite is a file, there is no host, port, username, or password. The data source name is a path (or :memory:). Here is the standard pattern, shown with the pure-Go modernc.org/sqlite driver:
package main
import (
"context"
"database/sql"
"log"
"time"
_ "modernc.org/sqlite"
)
func main() {
// driver name "sqlite" for modernc; the DSN is a file path.
db, err := sql.Open("sqlite", "file:app.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// sql.Open does not connect or even open the file. Ping forces it.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
log.Fatal(err)
}
var n int
if err := db.QueryRowContext(ctx,
"SELECT count(*) FROM sqlite_master WHERE type = ?", "table",
).Scan(&n); err != nil {
log.Fatal(err)
}
log.Printf("tables: %d", n)
}To switch to mattn/go-sqlite3, change the import to _ "github.com/mattn/go-sqlite3" and the driver name to "sqlite3". The PRAGMA syntax in the DSN differs between drivers (see below), but the database/sql calls do not change at all.
sql.Open does not open the file
This is the same trap as every other Go SQL driver: sql.Open only validates arguments and prepares a handle. It does not touch the disk. A typo in the path, a permissions problem, or a missing directory surfaces only on the first real operation. Call db.PingContext at startup so failures appear where you expect them, not deep inside a request handler later.
The accidental-creation trap
By default SQLite creates the database file if it does not exist. That is convenient until a typo in the path silently spawns a fresh empty database and your app reports zero rows instead of an error. If the file must already exist, open it read-write without create using a URI flag. With mattn/go-sqlite3 the file is created on open by default; to forbid creation, open with mode=rw in the DSN (file:app.db?mode=rw), which returns an error rather than creating a new file.
"database is locked" and WAL mode
The error new SQLite users hit most is database is locked (SQLITE_BUSY). SQLite allows one writer at a time. If a second connection tries to write while the first holds the lock, it fails immediately unless you tell it to wait.
Two settings fix the common cases:
busy_timeoutmakes a blocked connection retry for the given number of milliseconds before giving up, instead of erroring instantly. Set it to a few seconds.- WAL (write-ahead logging) journal mode lets readers and a writer proceed concurrently. In the default rollback-journal mode, a writer blocks readers. WAL is almost always the right choice for an application that does concurrent reads and writes.
Both are set as PRAGMAs. With modernc.org/sqlite you pass them in the DSN as _pragma=busy_timeout(5000) and _pragma=journal_mode(WAL), as shown above. With mattn/go-sqlite3 the DSN parameters are named differently, for example file:app.db?_busy_timeout=5000&_journal_mode=WAL. Always confirm the parameter names against the driver you actually imported, because they are not standardized across drivers.
Pooling: an embedded database is not a server
database/sql manages a connection pool, and that abstraction was designed for client/server databases. SQLite is a local file, so the usual reasons to grow a pool do not apply, and a large pool of writers actively makes lock contention worse.
A robust pattern for a write-capable SQLite handle:
db.SetMaxOpenConns(1) // serialize writers; avoids most SQLITE_BUSY
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0) // a local file connection does not go staleLimiting to a single open connection serializes all access through one connection and eliminates most lock contention at the cost of write concurrency, which SQLite does not really offer anyway. A common refinement is to keep one write connection (MaxOpenConns(1)) and a separate read-only connection (or pool) opened with mode=ro, since WAL allows concurrent readers. Setting ConnMaxLifetime to zero is correct here: unlike a network connection to a remote server, a handle to a local file does not time out or get killed by an idle-connection reaper.
Common errors
| Error | Likely cause |
|---|---|
unable to open database file | Bad path, missing parent directory, or no write permission |
database is locked (SQLITE_BUSY) | Concurrent writers without busy_timeout; set it and enable WAL |
no such table | Opened a freshly auto-created empty file (wrong path); use mode=rw to forbid creation |
binary was built with CGO_ENABLED=0 build failure | Using mattn/go-sqlite3 without a C toolchain; switch to a pure-Go driver or enable cgo |
cannot find -lsqlite3 / C compiler errors | cgo driver on a machine without a C compiler installed |
The cgo-related failures are the giveaway that you picked the C-binding driver in an environment that cannot compile C. Either install a C toolchain for the target, or move to modernc.org/sqlite and keep CGO_ENABLED=0.
Querying without writing connection code
If your goal is to explore a SQLite database rather than embed one in a service, a GUI client skips the driver question entirely. Mako opens SQLite files with AI-assisted SQL -- see our SQLite GUI client guide for how it compares to the alternatives. Connecting from another language? See How to Connect to SQLite from Python and from Node.js.
Mako connects to SQLite with AI-powered autocomplete. Try it free at mako.ai.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.