Go Client
The Go client is a thread-safe, connection-pooled driver for VeltrixDB with the broadest API surface of any SDK — single-key ops, batches, namespaces, hash fields, atomic ops (CAS/INCR/DECR/SETNX), range scans, optimistic transactions, secondary indexes, and vector search.
Install
bashgo get github.com/VeltrixDB/veltrixdb-client-go
Requires Go 1.19+. No external dependencies — the client is built entirely on the standard library (net, crypto/tls, encoding/binary).
Quick start
This example is drawn directly from the SDK's bundled go/example/main.go: connect, ping, put/get/delete, run a batch, and fan out concurrent writes across the pool.
gopackage main import ( "context" "errors" "fmt" "log" "time" vdb "github.com/VeltrixDB/veltrixdb-client-go" ) func main() { // ── Connect ────────────────────────────────────────────────────── cfg := vdb.DefaultConfig() cfg.PoolSize = 16 // 16 persistent connections client, err := vdb.New("127.0.0.1:9000", cfg) if err != nil { log.Fatalf("connect: %v", err) } defer client.Close() ctx := context.Background() // ── Ping ───────────────────────────────────────────────────────── if err := client.Ping(ctx); err != nil { log.Fatalf("ping: %v", err) } // ── Put / Get / Delete ─────────────────────────────────────────── if err := client.Put(ctx, "hello", []byte("world")); err != nil { log.Fatalf("put: %v", err) } val, err := client.Get(ctx, "hello") if err != nil { log.Fatalf("get: %v", err) } fmt.Printf("GET hello = %s\n", val) if err := client.Delete(ctx, "hello"); err != nil { log.Fatalf("delete: %v", err) } // ErrNotFound is a known sentinel, not a hard error. _, err = client.Get(ctx, "hello") if errors.Is(err, vdb.ErrNotFound) { fmt.Println("GET hello → not found (expected after delete)") } }
Config
Config is passed to New; DefaultConfig() returns production-sane defaults that you can override selectively.
gotype Config struct { PoolSize int // connections to maintain (default 8) DialTimeout time.Duration // TCP/TLS handshake timeout (default 5s) IOTimeout time.Duration // per-operation deadline (default 10s); 0 = no deadline TLS *tls.Config // nil = plain TCP }
The client pre-dials all PoolSize connections inside New, so construction fails fast if the server is unreachable. Close() drains and closes every pooled connection; after Close, all methods return ErrClosed.
API surface
Every method takes a context.Context as its first argument, so cancellation and per-call deadlines compose naturally with the pool's own IOTimeout.
Core key-value
| Method | Description |
|---|---|
New(addr, cfg) | Create client and pre-dial all pool connections |
Put(ctx, key, value) | Write a key |
Get(ctx, key) | Read a key. Returns nil, ErrNotFound if missing |
Delete(ctx, key) | Delete a key (no error if already absent) |
Ping(ctx) | Check connection health |
Info(ctx) | Get server stats string |
Auth(ctx, user, pass) | Authenticate (only needed if the server has auth enabled) |
Close() | Close all pool connections |
Batch operations
| Method | Description |
|---|---|
MultiPut(ctx, entries) | Write multiple keys in one round trip |
MultiGet(ctx, keys) | Read multiple keys in one round trip |
goentries := []vdb.Entry{ {Key: "user:1", Value: []byte(`{"name":"Alice","age":30}`)}, {Key: "user:2", Value: []byte(`{"name":"Bob","age":25}`)}, // With a 60-second TTL: {Key: "session:abc", Value: []byte("token-xyz"), TTLSeconds: 60}, } mputResults, err := client.MultiPut(ctx, entries) if err != nil { log.Fatalf("multiput: %v", err) } for i, r := range mputResults { if r.Err != nil { log.Printf("entry %d failed: %v", i, r.Err) } } getResults, err := client.MultiGet(ctx, []string{"user:1", "user:2", "user:99"}) for _, r := range getResults { if r.Found { fmt.Printf("GET %s = %s\n", r.Key, r.Value) } else { fmt.Printf("GET %s → not found\n", r.Key) } }
Results are returned in the same order as the input. Only individual entries fail on partial errors — the batch itself only errors on connection or protocol-level failure.
Namespaces
| Method | Description |
|---|---|
PutNS(ctx, ns, key, value, ttl) | Write into a namespace |
GetNS(ctx, ns, key) | Read from a namespace |
DeleteNS(ctx, ns, key) | Delete from a namespace |
ScanNamespace(ctx, ns, prefix, limit) | Scan keys in a namespace |
ListNamespaces(ctx) | List all namespaces and key counts |
DropNamespace(ctx, ns) | Delete all keys in a namespace |
Hash fields (per-field TTL)
| Method | Description |
|---|---|
HSet(ctx, key, field, value, ttl) | Set a field with its own TTL |
HGet(ctx, key, field) | Get a field value |
HDel(ctx, key, field) | Delete one field |
HGetAll(ctx, key) | Get all live fields and values |
HKeys(ctx, key) | Get all live field names |
HLen(ctx, key) | Count live fields |
HExpire(ctx, key, field, ttl) | Update TTL of an existing field |
HTTL(ctx, key, field) | Remaining TTL (-1=forever, -2=not found) |
Atomic operations
New in v1.1.0 — typed atomic ops (previously Rust/C++ only):
| Method | Description |
|---|---|
CAS(ctx, key, expected, newValue) (bool, error) | Compare-and-swap. (true, nil) = swapped, (false, nil) = current value didn't match expected, (false, ErrNotFound) = key doesn't exist |
Incr(ctx, key, delta) (int64, error) | Atomically add delta (int64) and return the new value; a missing key starts from 0. Errors if the existing value is not an integer |
Decr(ctx, key, delta) (int64, error) | Atomically subtract delta and return the new value; a missing key starts from 0 |
SetNX(ctx, key, value) (bool, error) | Set only if absent. (true, nil) = created, (false, nil) = key already existed |
go// Counter — missing key starts from 0 n, err := client.Incr(ctx, "counter", 5) // n = 5 n, err = client.Decr(ctx, "counter", 2) // n = 3 // Compare-and-swap swapped, err := client.CAS(ctx, "hello", []byte("world"), []byte("earth")) if err != nil { if errors.Is(err, vdb.ErrNotFound) { // key doesn't exist } } else if !swapped { // current value didn't match "world" } // Distributed lock-style set-if-absent created, err := client.SetNX(ctx, "lock", []byte("owner-A")) // created == false → someone else holds the key
Range scans, transactions, indexes, and vectors
Beyond the wire-protocol operations documented in the client README, the Go client exposes a further set of methods (RangeScan, ScanCursor, Txn, KeyVersion, IdxCreate/IdxDrop/IdxQuery, VSet/VSearch, Query) implemented directly in client.go:
| Method | Description |
|---|---|
RangeScan(ctx, start, end, limit, reverse) | Ordered key-range scan; requires the server's ordered index |
ScanCursor(ctx, cursor, limit) | Cursor-paginated full-keyspace scan; pass "" to start |
Txn(ctx, ops) | Atomic optimistic transaction (SET/SETIF/DEL ops); returns ErrTxnConflict on a failed version guard |
KeyVersion(ctx, key) | Optimistic version token for a key (0 = absent) — feeds TxnOp.ExpectedVersion |
IdxCreate(ctx, name, field) / IdxDrop(ctx, name) | Manage a persistent secondary index on a JSON field |
IdxQuery(ctx, name, value, limit) | Look up primary keys by indexed field value |
VSet(ctx, key, vec) / VSearch(ctx, k, query) | Store/search vectors by cosine similarity |
Query(ctx, ns, field, op, value, limit) | Field-predicate query over a namespace (= != > < >= <= contains) |
Txn semantics are read-committed optimistic CAS — atomic within the batch, but not serializable. Re-read versions with KeyVersion and retry on ErrTxnConflict.
TLS
goimport "crypto/tls" // Server verification only tlsCfg := &tls.Config{RootCAs: loadCACert("ca.crt")} client, _ := veltrix.New("veltrix.example.com:9443", veltrix.Config{TLS: tlsCfg}) // mTLS (client certificate + server certificate) tlsCfg := &tls.Config{ Certificates: []tls.Certificate{loadCert("client.crt", "client.key")}, RootCAs: loadCACert("ca.crt"), }
Auth
goclient, _ := veltrix.New("127.0.0.1:9000", veltrix.Config{PoolSize: 4}) client.Auth(ctx, "alice", "secret") client.Put(ctx, "key", []byte("value"))
Error handling
The client defines three sentinel errors you should check with errors.Is: ErrNotFound, ErrClosed, and ErrTxnConflict.
goval, err := client.Get(ctx, "missing-key") if errors.Is(err, veltrix.ErrNotFound) { // key doesn't exist } if errors.Is(err, context.DeadlineExceeded) { // operation timed out }
Internally, when an operation fails with anything other than ErrNotFound or ErrTxnConflict, the pool assumes the connection is broken: it discards it and dials a fresh replacement before returning the error to the caller.
Concurrent usage
The Client is safe for concurrent use from multiple goroutines — each call borrows a connection from the pool independently:
goconst workers = 50 done := make(chan struct{}, workers) for i := 0; i < workers; i++ { go func(id int) { key := fmt.Sprintf("concurrent:%d", id) val := []byte(fmt.Sprintf("value-%d", id)) if err := client.Put(ctx, key, val); err != nil { log.Printf("worker %d put: %v", id, err) } done <- struct{}{} }(i) } for i := 0; i < workers; i++ { <-done }
PoolSize: 16. A larger pool doesn't help throughput and just wastes idle connections.
Cluster-aware variant
This page covers github.com/VeltrixDB/veltrixdb-client-go — a single-node, connection-pooled client. The VeltrixDB server repository also bundles a separate cluster-aware client under github.com/VeltrixDB/veltrixdb/client (client.NewClient), which discovers cluster topology, builds a consistent-hash ring matching the server's, and follows Raft MOVED redirects automatically. Reach for that package specifically when talking to a raft or replicated mode deployment — see Deployment Modes.