Go SQLite: database/sql with go-sqlite3 or a Pure Go Driver (Tested)

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/sql package plus a driver.
  • github.com/mattn/go-sqlite3 wraps the C library. It needs cgo and a C compiler such as gcc.
  • modernc.org/sqlite is a pure Go port, so it builds with CGO_ENABLED=0. Its driver name is "sqlite", not "sqlite3".
  • Pass values with ? placeholders, and group writes with db.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:

DriverImportsql.Open nameNeeds cgo
mattn/go-sqlite3github.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/sqlitemodernc.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

RequirementVersion tested
Go1.27.1 (golang:1 Docker image, linux/arm64)
C compiler, for go-sqlite3 onlygcc 14.2.0
mattn/go-sqlite3v1.14.52
modernc.org/sqlitev1.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 with fmt.Sprintf from user input.
  • Transactions. tx.Prepare plus tx.Commit makes the two inserts all-or-nothing, and it’s faster than separate commits for many rows. See SQLite transactions.
  • Rows. Always rows.Close(), and check rows.Err() after the loop.
  • A single row. QueryRow(...).Scan returns sql.ErrNoRows when 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

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.