Go SDK
On this page
Related contents
Query ScopeDB and write events from Go applications. This guide uses goscopedb 0.7.0.
Install and configure
Use Go 1.26 or later. In your application's Go module, install the SDK:
go get github.com/scopedb/goscopedb@v0.7.0Commit go.mod and go.sum. Follow the application connection guide to obtain an endpoint and API key, then set them in the shell where you run the examples:
export SCOPEDB_ENDPOINT="https://<endpoint>"
export SCOPEDB_API_KEY="<api-key>"Keep the key on your server. SDKs do not reuse the CLI's interactive login.
Run a query
Save this complete program as main.go and run go run .. It verifies the connection without creating a table or requiring sample data.
package main
import (
"context"
"fmt"
"log"
"os"
scopedb "github.com/scopedb/goscopedb"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
client, err := scopedb.NewClient(scopedb.Config{
Endpoint: os.Getenv("SCOPEDB_ENDPOINT"),
APIKey: os.Getenv("SCOPEDB_API_KEY"),
})
if err != nil {
return err
}
defer client.Close()
ctx := context.Background()
result, err := client.Query(ctx, "SELECT 1 AS ready")
if err != nil {
return err
}
rows, err := result.ToObjects()
if err != nil {
return err
}
fmt.Printf("ready: %v\n", rows[0]["ready"])
return nil
}Expected output:
ready: 1Query waits for execution to finish; ToObjects returns rows keyed by column name. Reuse the client, and add the following examples inside run before return nil. Close releases idle connections owned by the SDK; an application-supplied Config.HTTPClient remains the application's responsibility.
Write rows
Create the events table used by the quickstart. This statement leaves an existing table unchanged; if you already have events, check that its columns match before continuing.
if _, err := client.Query(ctx, `
CREATE TABLE IF NOT EXISTS events (
time timestamp,
service string,
name string,
message string,
var object
)
`); err != nil {
return err
}Append one event and wait for it to commit:
table := client.Table("events")
row := map[string]any{
"time": "2026-10-23T09:00:00Z",
"service": "checkout",
"name": "payment_failed",
"message": "Credit card gateway timed out",
"var": map[string]string{"region": "apac", "gateway": "credit_card"},
}
stream, err := table.AppendStream(scopedb.AppendStreamOptions{})
if err != nil {
return err
}
if err := stream.Send(ctx, row); err != nil {
_, _ = stream.Shutdown(ctx)
return err
}
report, err := stream.Shutdown(ctx)
if err != nil {
return err
}
fmt.Printf("Written rows: %d\n", report.CommittedRows)Send accepts a row into the SDK's buffer. Shutdown sends remaining rows, waits for delivery, and closes the stream. With this default failure policy, a failed batch returns an error. A successful run prints Written rows: 1.
Read the event back after shutdown:
written, err := client.Query(ctx, `
FROM events
WHERE time = '2026-10-23T09:00:00Z'::timestamp
AND service = 'checkout'
AND name = 'payment_failed'
SELECT service, name, var['gateway']::string AS gateway
LIMIT 10
`)
if err != nil {
return err
}
writtenRows, err := written.ToObjects()
if err != nil {
return err
}
fmt.Println(writtenRows)The result includes checkout, payment_failed, and credit_card. Each run appends another event. The examples use the default scopedb database and public schema; set the table's Database and Schema fields for another table location.
Work with results
Use ToObjects for named columns or ToValues for positional rows. An empty result produces no rows. First also tells you whether a row was found:
first, found, err := written.First()
if err != nil {
return err
}
if found {
fmt.Println(first["name"])
}Object conversion rejects duplicate column names; use unique aliases or positional results. Integers become int64 or uint64, timestamps become time.Time, intervals become time.Duration, and binary values become []byte. Array, object, and any values remain strings; NULL becomes nil.
Use RawRows when you need the original result text. See ResultSet for conversion methods and errors.
Manage table definitions
Read the table's complete definition and ETag:
catalog := table.Catalog()
snapshot, err := catalog.Get(ctx)
if err != nil {
return err
}
fmt.Println(snapshot.Definition.Columns, snapshot.ETag)To change its comment, preserve the definition and preview the replacement. Add encoding/json to the program's imports:
snapshot.Definition.Comment = new("Checkout events")
plan, err := catalog.Apply(ctx, snapshot.Definition, scopedb.CatalogOptions{
DryRun: true,
IfMatch: snapshot.ETag,
})
if err != nil {
return err
}
preview, err := json.MarshalIndent(plan, "", " ")
if err != nil {
return err
}
fmt.Println(string(preview))Review Before, After, and Destructive. Apply the same definition with the preview's ETag:
applied, err := catalog.Apply(ctx, snapshot.Definition, scopedb.CatalogOptions{
IfMatch: plan.ETag,
})
if err != nil {
return err
}
fmt.Println(*applied.After.Comment)Apply replaces the complete definition; omitted fields are cleared. Typed reads reject unknown fields; use Raw() to retain them during exports or edits. An ETag conflict requires a fresh read and preview. Potentially destructive changes also require AllowDestructive: true; this comment edit does not. See the Catalog contract for resource selection and interrupted writes.
Configure queries and writes
Query execution
Use a statement to set execution limits or retain a handle for progress and cancellation. Add time to the program's imports:
statement := client.Statement("SELECT 1 AS ready")
statement.MaxElapsed = 60 * time.Second
handle, err := statement.Submit(ctx)
if err != nil {
return err
}
fmt.Println(handle.Snapshot().Status)
if _, err := handle.Wait(ctx); err != nil {
return err
}Snapshot reads cached state; Refresh(ctx) fetches an active statement's state. client.StatementHandle(id) resumes tracking a known ID. MaxElapsed includes queueing; MaxEstimated separately limits estimated duration.
Cancelling a context stops local requests and waiting. To request server-side cancellation, call handle.Cancel with a fresh, bounded context and inspect its returned snapshot. Execution may have finished already or may still be active.
Batching and delivery
Use Flush to wait for buffered writes while keeping a stream open. Streams target 4 MiB batches and allow four concurrent requests by default. Each SDK request is capped at 8 MiB and 200,000 rows.
For a long-running producer that must continue after a failed batch:
telemetry, err := table.AppendStream(scopedb.AppendStreamOptions{
FailurePolicy: scopedb.AppendFailureContinue,
MaxConcurrentBatches: 1,
})
if err != nil {
return err
}
if err := telemetry.TrySend(row); err != nil {
fmt.Println("Not queued:", err)
}
// Stop producers before shutting down the stream.
delivery, err := telemetry.Shutdown(ctx)
if err != nil {
return err
}
fmt.Println(delivery.Outcome, delivery.CommittedRows, delivery.FailedRows, delivery.UnknownRows)TrySend returns an error when a row cannot enter the local buffer. Continue mode reports failures without retaining failed payloads for replay; use durable storage when loss is unacceptable. Inspect failed and unknown rows as well as committed rows. See AppendStreamOptions for buffering and AppendRetryOptions for retry controls.
Streams retry confirmed temporary rejections by default. RetryUnknown explicitly allows replay of uncertain writes and can duplicate rows. Client timeouts and cancellations are never replayed. Cancelling a Flush or Shutdown wait does not abandon accepted rows; wait again with a live context to settle them.
Row encoding
Streams encode structs with JSON tags or maps through encoding/json. For existing NDJSON, use Table.AppendNDJSON, which makes one attempt. Shape values in the producer; Append does not run a ScopeQL transformation. See the shared JSON input types for accepted field values and schema rules.
Timeouts
Queries and Catalog reads have no SDK client deadline by default; use a caller context or Config.HTTPClient to bound waiting.
Config.WriteTimeout bounds each append or Catalog mutation attempt, including response-body reads. Omit it for 30 seconds, set new(2 * time.Minute) to override it, or new(time.Duration(0)) to disable it. AppendStreamOptions.AttemptTimeout inherits when nil; a pointer overrides it, with zero disabling the attempt limit. Negative durations are invalid.
The stream's five-minute default delivery/retry budget still applies. Encoding, queue admission, retry backoff, and the complete Flush or Shutdown wait are outside the per-attempt timeout.
Handle errors
Use errors.As to inspect a ScopeDB failure without matching its message text. Add errors to the program's imports:
if _, err := client.Query(ctx, "SELECT 1 AS ready"); err != nil {
var apiError *scopedb.Error
if errors.As(err, &apiError) {
fmt.Println(apiError.Code, apiError.HTTPStatus, apiError.RequestID)
fmt.Println(apiError.StatementDetails, apiError.WriteDetails)
}
return err
}For an Append failure, inspect AppendDetails.Status and its row diagnostics. WriteDetails identifies a timeout or cancellation after sending, with an unknown outcome. Reconcile an uncertain write before replaying it; temporary or retryable classification alone does not establish that replay is safe.
Catalog writes never retry automatically. catalog_commit_outcome_unknown requires reading the current definition before another mutation; dry runs cannot commit. Follow the HTTP error and retry contract for operation-specific decisions.