Go Driver
Native Go driver for Stoolap. Loads libstoolap at runtime via direct ABI calls. No C compiler, no CGO required. Provides two ways to use Stoolap from Go:
- Direct API for maximum performance and control
database/sqldriver for standard Go database access
For a pure Go driver with no shared library dependency, see Go WASM Driver.
Requirements
- Go 1.24+
CGO_ENABLED=0works (no C compiler needed)CGO_ENABLED=1also works (if linked with other CGO code)
Installation
go get github.com/stoolap/stoolap-go
Prebuilt shared libraries for macOS (arm64), Linux (x64), and Windows (x64) are bundled
in the module. No extra downloads or environment variables needed, just go get and build.
The binary dynamically loads libstoolap at runtime via dlopen. For deployment, place the
shared library next to your executable or set the STOOLAP_LIB environment variable.
Other Platforms
For platforms without a bundled library (e.g. Linux arm64, macOS x64), download from the releases page or build from source, then:
export STOOLAP_LIB=/path/to/libstoolap.so
go build ./...
Quick Start
Direct API
package main
import (
"context"
"fmt"
"github.com/stoolap/stoolap-go"
)
func main() {
db, err := stoolap.Open("memory://")
if err != nil {
panic(err)
}
defer db.Close()
ctx := context.Background()
db.Exec(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
db.Exec(ctx, "INSERT INTO users VALUES (1, 'Alice', 30), (2, 'Bob', 25)")
rows, _ := db.Query(ctx, "SELECT id, name, age FROM users ORDER BY id")
defer rows.Close()
for rows.Next() {
var id, age int64
var name string
rows.Scan(&id, &name, &age)
fmt.Printf("id=%d name=%s age=%d\n", id, name, age)
}
}
database/sql Driver
package main
import (
"context"
"database/sql"
"fmt"
_ "github.com/stoolap/stoolap-go"
)
func main() {
db, err := sql.Open("stoolap", "memory://")
if err != nil {
panic(err)
}
defer db.Close()
ctx := context.Background()
db.ExecContext(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
db.ExecContext(ctx, "INSERT INTO users VALUES (1, 'Alice', 30), (2, 'Bob', 25)")
rows, _ := db.QueryContext(ctx, "SELECT id, name, age FROM users ORDER BY id")
defer rows.Close()
for rows.Next() {
var id, age int64
var name string
rows.Scan(&id, &name, &age)
fmt.Printf("id=%d name=%s age=%d\n", id, name, age)
}
}
Connection Strings
| DSN | Description |
|---|---|
memory:// |
In-memory database (unique, isolated instance) |
memory://mydb |
Named in-memory database (same name shares the engine) |
file:///path/to/db |
File-based persistent database |
file:///path/to/db?sync_mode=full |
File-based with configuration options |
See Connection String Reference for all configuration options.
Parameters
Use positional parameters $1, $2, etc. with the direct API:
db.ExecParams(ctx, "INSERT INTO users VALUES ($1, $2, $3)",
[]any{int64(1), "Alice", int64(30)})
rows, _ := db.QueryParams(ctx, "SELECT name FROM users WHERE id = $1",
[]any{int64(1)})
With database/sql, use standard ? positional arguments:
db.ExecContext(ctx, "INSERT INTO users VALUES (?, ?, ?)", 1, "Alice", 30)
rows, _ := db.QueryContext(ctx, "SELECT name FROM users WHERE id = ?", 1)
Transactions
Default Isolation (Read Committed)
tx, err := db.Begin(ctx)
if err != nil {
panic(err)
}
tx.ExecParams(ctx, "INSERT INTO users VALUES ($1, $2)",
[]any{int64(1), "Alice"})
if err := tx.Commit(); err != nil {
panic(err)
}
Snapshot Isolation
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSnapshot,
})
if err != nil {
panic(err)
}
defer tx.Rollback()
// All reads within this transaction see the same snapshot
rows, _ := tx.Query(ctx, "SELECT * FROM users")
// ...
tx.Commit()
| Level | Description |
|---|---|
| Read Committed (default) | Each statement sees data committed before it started |
| Snapshot | The transaction sees a consistent snapshot from when it began |
Prepared Statements
Parse once, execute many times:
stmt, err := db.Prepare(ctx, "INSERT INTO users VALUES ($1, $2)")
if err != nil {
panic(err)
}
defer stmt.Close()
for i := int64(1); i <= 1000; i++ {
stmt.ExecContext(ctx, []any{i, "User"})
}
NULL Handling
Use sql.Null* types for nullable columns:
var (
name sql.NullString
age sql.NullInt64
score sql.NullFloat64
active sql.NullBool
ts sql.NullTime
)
rows, _ := db.QueryParams(ctx,
"SELECT name, age, score, active, created_at FROM users WHERE id = $1",
[]any{int64(1)})
defer rows.Close()
if rows.Next() {
rows.Scan(&name, &age, &score, &active, &ts)
if name.Valid {
fmt.Println("Name:", name.String)
} else {
fmt.Println("Name is NULL")
}
}
Scanning into any
rows, _ := db.Query(ctx, "SELECT id, name, age FROM users")
defer rows.Close()
for rows.Next() {
var id, name, age any
rows.Scan(&id, &name, &age)
fmt.Printf("id=%v name=%v age=%v\n", id, name, age)
}
JSON
JSON values are stored and retrieved as strings:
db.Exec(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, data JSON)")
db.Exec(ctx, `INSERT INTO docs VALUES (1, '{"name":"Alice","age":30}')`)
rows, _ := db.Query(ctx, "SELECT data FROM docs WHERE id = 1")
defer rows.Close()
var data string
if rows.Next() {
rows.Scan(&data)
// data = `{"name":"Alice","age":30}`
}
Vector Search
Vectors are stored as packed little-endian f32 bytes:
import (
"encoding/binary"
"math"
)
db.Exec(ctx, "CREATE TABLE vectors (id INTEGER PRIMARY KEY, embedding VECTOR(3))")
// Encode a float32 vector to bytes
vec := []float32{1.0, 2.0, 3.0}
buf := make([]byte, len(vec)*4)
for i, f := range vec {
binary.LittleEndian.PutUint32(buf[i*4:], math.Float32bits(f))
}
db.ExecParams(ctx, "INSERT INTO vectors VALUES ($1, $2)",
[]any{int64(1), buf})
// Read back
var blob []byte
rows, _ := db.QueryParams(ctx, "SELECT embedding FROM vectors WHERE id = $1",
[]any{int64(1)})
defer rows.Close()
if rows.Next() {
rows.Scan(&blob)
}
// Decode packed f32 bytes back to float32 slice
result := make([]float32, len(blob)/4)
for i := range result {
result[i] = math.Float32frombits(binary.LittleEndian.Uint32(blob[i*4:]))
}
Bulk Fetch
FetchAll() fetches all remaining rows in a single native call and returns them as [][]any, minimizing per-row FFI overhead:
rows, _ := db.Query(ctx, "SELECT id, name, age FROM users")
defer rows.Close()
allRows, err := rows.FetchAll()
if err != nil {
panic(err)
}
for _, row := range allRows {
// row[0] = id, row[1] = name, row[2] = age
}
Cloning for Concurrency
A single DB handle must not be used from multiple goroutines simultaneously. Use Clone() to create per-goroutine handles that share the underlying engine:
db, _ := stoolap.Open("memory://mydb")
defer db.Close()
db.Exec(ctx, "CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)")
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
clone, _ := db.Clone()
defer clone.Close()
clone.ExecParams(ctx, "INSERT INTO t VALUES ($1, $2)",
[]any{int64(workerID), fmt.Sprintf("worker-%d", workerID)})
}(i)
}
wg.Wait()
The database/sql driver handles this automatically. Each connection in the pool gets its own cloned handle.
Type Mapping
| SQL Type | Go Type | Nullable Go Type |
|---|---|---|
| INTEGER | int64, int, int32 |
sql.NullInt64 |
| FLOAT | float64, float32 |
sql.NullFloat64 |
| TEXT | string |
sql.NullString |
| BOOLEAN | bool |
sql.NullBool |
| TIMESTAMP | time.Time |
sql.NullTime |
| JSON | string |
sql.NullString |
| VECTOR/BLOB | []byte |
[]byte (nil for NULL) |
Error Handling
All errors returned by the stoolap engine are typed as *stoolap.Error, which carries a categorized error code:
var stErr *stoolap.Error
if errors.As(err, &stErr) {
switch stErr.Code() {
case stoolap.ErrUniqueConstraint:
// handle duplicate
case stoolap.ErrPrimaryKeyConstraint:
// handle primary key violation
case stoolap.ErrTableNotFound:
// handle missing table
}
}
Use IsConstraintViolation() to check for any constraint error (unique, primary key, not null, check, foreign key):
if errors.As(err, &stErr) && stErr.IsConstraintViolation() {
// handle any constraint violation
}
| ErrorCode | Description |
|---|---|
ErrGeneral |
Uncategorized error |
ErrUniqueConstraint |
Unique index violation |
ErrPrimaryKeyConstraint |
Primary key violation |
ErrNotNullConstraint |
NOT NULL violation |
ErrCheckConstraint |
CHECK constraint violation |
ErrForeignKeyViolation |
Foreign key violation |
ErrTableNotFound |
Table does not exist |
ErrTableExists |
Table already exists |
Thread Safety
- Direct API: A single
DBhandle must not be shared across goroutines. UseClone()for per-goroutine handles. database/sql: Thread-safe by default. The connection pool creates cloned handles automatically.- Tx, Stmt, Rows: Must remain on the goroutine that created them.
Direct API Reference
Package Functions
func Version() (string, error)
func Open(dsn string) (*DB, error)
func OpenMemory() (*DB, error)
DB
func (db *DB) Close() error
func (db *DB) Clone() (*DB, error)
func (db *DB) Exec(ctx context.Context, query string) (sql.Result, error)
func (db *DB) ExecParams(ctx context.Context, query string, args []any) (sql.Result, error)
func (db *DB) Query(ctx context.Context, query string) (*Rows, error)
func (db *DB) QueryParams(ctx context.Context, query string, args []any) (*Rows, error)
func (db *DB) Prepare(ctx context.Context, query string) (*Stmt, error)
func (db *DB) Begin(ctx context.Context) (*Tx, error)
func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
Rows
func (r *Rows) Next() bool
func (r *Rows) Scan(dest ...any) error
func (r *Rows) Columns() []string
func (r *Rows) Close() error
func (r *Rows) FetchAll() ([][]any, error)
Stmt
func (s *Stmt) ExecContext(ctx context.Context, args []any) (sql.Result, error)
func (s *Stmt) QueryContext(ctx context.Context, args []any) (*Rows, error)
func (s *Stmt) Close() error
Tx
func (tx *Tx) Exec(ctx context.Context, query string) (sql.Result, error)
func (tx *Tx) ExecParams(ctx context.Context, query string, args []any) (sql.Result, error)
func (tx *Tx) Query(ctx context.Context, query string) (*Rows, error)
func (tx *Tx) QueryParams(ctx context.Context, query string, args []any) (*Rows, error)
func (tx *Tx) Commit() error
func (tx *Tx) Rollback() error
database/sql Driver
The driver is registered as "stoolap" and implements the following database/sql/driver interfaces:
| Interface | Description |
|---|---|
driver.Driver |
Basic driver |
driver.DriverContext |
Connector-based driver |
driver.Connector |
Connection factory with pooling |
driver.Conn |
Connection |
driver.ConnBeginTx |
Transaction with isolation levels |
driver.ExecerContext |
Direct exec (bypasses prepare) |
driver.QueryerContext |
Direct query (bypasses prepare) |
driver.ConnPrepareContext |
Prepared statements |
driver.Pinger |
Connection health check |
driver.SessionResetter |
Session reset on pool return |
driver.Validator |
Connection validation |
driver.Tx |
Transaction commit/rollback |
driver.Stmt |
Prepared statement |
driver.StmtExecContext |
Prepared exec with context |
driver.StmtQueryContext |
Prepared query with context |
driver.Rows |
Result set iteration |
Architecture
The driver loads libstoolap at runtime via dlopen and dispatches FFI calls through hand-written assembly trampolines entered via runtime.asmcgocall. This bypasses the standard CGO overhead (~40ns per call) and achieves near-native performance (~3ns per call).
On Linux, a minimal fake-cgo runtime preserves glibc thread-local storage so the shared library’s thread-local state works correctly without requiring CGO_ENABLED=1. On macOS and Windows, the native OS loader handles this directly.
Building from Source
git clone https://github.com/stoolap/stoolap-go.git
cd stoolap-go
go test -v ./...