Go Binding Reference¶
The Go binding provides a cgo-based interface to libwlite. It wraps the C ABI with Go-idiomatic types, error handling, and resource management patterns. The binding is designed for use in services, command-line tools, and embedded applications that need schema-driven SQLite management from Go.
Prerequisites¶
Before using the Go binding you need:
- Go 1.21 or later
- A C compiler (gcc or clang)
- libwlite installed on your system
- SQLite 3 development libraries
- CGO enabled (it is enabled by default on most platforms)
Install libwlite from source if it is not already available on your system:
git clone https://github.com/dbwarden-org/libwlite.git
cd libwlite
cmake -B build
cmake --build build
sudo cmake --install build
Verify the installation:
Installation¶
Add the Go binding to your module:
This pulls the binding package into your module and records it in go.mod and go.sum.
CGO configuration¶
CGO must be enabled and the linker and compiler flags must point to libwlite headers and shared library. Set these environment variables before building:
export CGO_ENABLED=1
export CGO_LDFLAGS="-L/usr/local/lib -lwlite -lsqlite3"
export CGO_CFLAGS="-I/usr/local/include"
If libwlite ships a pkg-config file, use it instead:
The binding's wlite.go file includes cgo directives that reference the correct include path and linker flags relative to the repository layout. When you install the package with go get, Go resolves these paths automatically as long as libwlite is available to the linker.
Platform notes¶
| Platform | Notes |
|---|---|
| Linux | Use gcc. Ensure libsqlite3-dev is installed. |
| macOS | Use clang (Xcode Command Line Tools). |
| Windows | Use mingw-w64 or MSYS2. Cross-compilation from Linux requires the appropriate cross-compiler toolchain. |
Building your application¶
After setting the CGO flags:
Or with pkg-config:
CGO_LDFLAGS="$(pkg-config --libs wlite)" \
CGO_CFLAGS="$(pkg-config --cflags wlite)" \
go build -o myapp .
Cross-compilation¶
Cross-compilation requires a C cross-compiler for the target platform:
Quick start¶
The following program loads a schema model, opens a database, runs a migration, and queries data:
package main
import (
"fmt"
"log"
wlite "github.com/dbwarden-org/wlite/bindings/go"
)
func main() {
model, err := wlite.LoadModel("app.wlite")
if err != nil {
log.Fatal(err)
}
defer model.Close()
if err := model.Validate(); err != nil {
log.Fatal(err)
}
fmt.Printf("Model loaded: %d table(s)\n", model.TableCount())
db, err := wlite.Open("app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.Migrate(model); err != nil {
log.Fatal(err)
}
fmt.Println("Migration complete.")
_, err = db.Execute(
"INSERT INTO todos (title, completed, created_at) VALUES (?, 0, datetime('now'))",
"Buy groceries",
)
if err != nil {
log.Fatal(err)
}
rows, err := db.Query("SELECT id, title, completed FROM todos")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int64
var title string
var completed int64
if err := rows.Scan(&id, &title, &completed); err != nil {
log.Fatal(err)
}
status := " "
if completed != 0 {
status = "x"
}
fmt.Printf(" [%s] %s (id=%d)\n", status, title, id)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
Run it with:
Types¶
The Go binding exposes five primary types. Each wraps a C-level opaque type and manages its lifecycle.
| Go Type | C Equivalent | Description |
|---|---|---|
wlite.DB |
wlite_db |
An open database connection. Provides methods for executing SQL, preparing statements, running migrations, and starting transactions. |
wlite.Model |
wlite_model |
A parsed .wlite schema file. Immutable after loading. Can be used to migrate multiple databases. Must be freed with Close(). |
wlite.Stmt |
wlite_stmt |
A prepared SQL statement. Supports parameter binding, stepping through results, and reading column values. Must be finalized with Close(). |
wlite.Tx |
wlite_tx |
An active database transaction. Supports commit, rollback, and savepoints. Must be closed or rolled back. |
wlite.Rows |
result set | An iterator over query results returned by db.Query(). Supports Next(), Scan(), and Close(). |
wlite.DB¶
The DB type manages a single database connection. Create one with wlite.Open or wlite.OpenMemory. Every DB must be closed when no longer needed:
DB is not safe for concurrent use. Each goroutine that needs database access should open its own connection.
wlite.Model¶
The Model type holds a parsed schema definition loaded from a .wlite file. Models are immutable and can be shared across goroutines. Load a model with wlite.LoadModel:
You can also load a model from raw bytes with wlite.ModelFromBytes, which is useful when the schema is embedded in the binary or fetched from a remote source.
wlite.Stmt¶
The Stmt type wraps a prepared SQL statement. Prepare one from a DB:
stmt, err := db.Prepare("INSERT INTO users (name, email) VALUES (?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
Bind parameters with BindInt64, BindDouble, BindText, or BindNull, then call Step to execute. After stepping, read results with the column access methods.
wlite.Tx¶
The Tx type represents an active transaction. Start one with db.Transaction():
Call tx.Commit() to persist changes or tx.Rollback() to discard them. You can also create savepoints within a transaction.
wlite.Rows¶
The Rows type is an iterator over query results. It is returned by db.Query():
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int64
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s\n", id, name)
}
Always check rows.Err() after the loop and close the rows when done.
Database creation and opening¶
Open a file database¶
Open an in-memory database¶
In-memory databases are useful for testing and temporary data processing. They exist only for the lifetime of the process.
Open with options¶
The underlying C API supports additional open options (read-only mode, foreign key enforcement, busy timeout). Use wlite.Open for the common case and configure the database after opening if you need advanced options.
Executing SQL¶
The Execute method runs SQL that does not return rows. Use it for DDL and DML statements:
err := db.Execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
if err != nil {
log.Fatal(err)
}
err = db.Execute(
"INSERT INTO users (name) VALUES (?)",
"Alice",
)
if err != nil {
log.Fatal(err)
}
Execute accepts variadic arguments that are bound as parameters. The argument types supported are int, int64, float64, string, and nil.
Prepared statements¶
Preparing a statement once and executing it multiple times is more efficient than executing raw SQL repeatedly:
stmt, err := db.Prepare("INSERT INTO users (name, email) VALUES (?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
users := []struct{ Name, Email string }{
{"Alice", "[email protected]"},
{"Bob", "[email protected]"},
{"Charlie", "[email protected]"},
}
for _, u := range users {
stmt.BindText(1, u.Name)
stmt.BindText(2, u.Email)
stmt.Step()
stmt.Reset()
}
Always finalize statements with defer stmt.Close() to release the underlying C resources.
Querying data¶
Use db.Query for queries that return multiple rows and db.QueryRow for single-value results:
// Multiple rows
rows, err := db.Query("SELECT id, name, email FROM users ORDER BY name")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int64
var name, email string
rows.Scan(&id, &name, &email)
fmt.Printf("%d: %s <%s>\n", id, name, email)
}
// Single value
var count int64
err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total users: %d\n", count)
Transactions¶
Wrap multiple related operations in a transaction for atomicity:
tx, err := db.Transaction()
if err != nil {
log.Fatal(err)
}
_, err = db.Execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
_, err = db.Execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
CGO configuration reference¶
The binding's cgo directives in wlite.go are:
These relative paths work when building from within the repository. When the package is installed via go get, Go uses the cached module source and the system-installed libwlite. Ensure the library and headers are in the standard system paths (/usr/local/lib and /usr/local/include) or set the CGO flags explicitly.
Environment variables¶
| Variable | Purpose | Example |
|---|---|---|
CGO_ENABLED |
Enable CGO compilation | 1 |
CGO_CFLAGS |
C compiler flags | -I/usr/local/include |
CGO_LDFLAGS |
Linker flags | -L/usr/local/lib -lwlite -lsqlite3 |
CC |
C compiler for cross-compilation | x86_64-linux-gnu-gcc |
GOOS |
Target operating system | linux |
GOARCH |
Target architecture | amd64 |
Troubleshooting¶
If you see linker errors like cannot find -lwlite, verify that libwlite.so (or libwlite.a) is in a path that the linker can find. You can check with:
If the library is installed in a non-standard location, add it to LD_LIBRARY_PATH or set CGO_LDFLAGS with the full path:
If you see compiler errors about missing headers, verify the include path:
Build tags¶
The Go binding does not use build tags. The same source works on all platforms that have a C compiler and libwlite available.
Complete example¶
The following program demonstrates model loading, validation, migration, inserts, queries, and transactions in a single file:
package main
import (
"fmt"
"log"
wlite "github.com/dbwarden-org/wlite/bindings/go"
)
type User struct {
ID int64
Name string
Email string
Active bool
}
func main() {
model, err := wlite.LoadModel("app.wlite")
if err != nil {
log.Fatal(err)
}
defer model.Close()
if err := model.Validate(); err != nil {
log.Fatal(err)
}
fmt.Printf("Model: %d table(s)\n", model.TableCount())
db, err := wlite.Open("app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.Migrate(model); err != nil {
log.Fatal(err)
}
fmt.Println("Migration complete.")
_, err = db.Execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
"Alice", "[email protected]",
)
if err != nil {
log.Fatal(err)
}
_, err = db.Execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
"Bob", "[email protected]",
)
if err != nil {
log.Fatal(err)
}
var count int64
err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Users: %d\n", count)
rows, err := db.Query("SELECT id, name, email FROM users ORDER BY name")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
log.Fatal(err)
}
fmt.Printf(" %d: %s <%s>\n", u.ID, u.Name, u.Email)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
fmt.Println("Done.")
}
Version information¶
Query the library version at runtime:
This is useful for diagnostics and for ensuring that the installed library matches the version your application was built against.
Further reading¶
- Migration guide for schema management with
LoadModel,Migrate,Diff,Plan, andSnapshot. - Query guide for prepared statements, binding, column access, and transactions.
- Error handling guide for the
Errortype, error constants, and patterns for safe resource management.