# Use the Go SDK

**Applies to:** the SDK supplied with your SMKV build; examples of outcome classification and cluster retries describe the current development SDK. Requires Go 1.22 or newer.

## Add the local module

The SDK is currently a local module named `go-smkv`, not a publicly published Go module. Obtain the matching SDK source through your release/support channel. In your application module, adjust the replacement path:

```sh
go mod edit -require=go-smkv@v0.0.0
go mod edit -replace=go-smkv=../smkv/go-smkv
```

## Connect and write

Save this as `main.go`, start a standalone node, then run `go run .`:

```go
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "time"
    smkv "go-smkv"
)

func main() {
    client, err := smkv.NewClient("127.0.0.1:7379", smkv.Options{
        PoolSize: 4,
        Timeout: 5 * time.Second,
    })
    if err != nil { log.Fatal(err) }
    defer client.Close()
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    key := []byte("greeting")
    if err := client.Put(ctx, key, []byte("hello"), smkv.WithTTL(time.Minute), smkv.WithSync()); err != nil {
        log.Fatal(err)
    }
    value, err := client.Get(ctx, key)
    if errors.Is(err, smkv.ErrNotFound) { fmt.Println("cache miss"); return }
    if err != nil { log.Fatal(err) }
    fmt.Println(string(value))
}
```

Clients are safe for concurrent goroutines and use a bounded connection pool. Reuse clients, close them during shutdown, and do not mutate input slices until a call returns. Connections are lazy; constructing a direct client does not verify a live server. Empty byte values are valid.

## Route across a cluster

Use management seeds, not data addresses. With an existing `ctx`:

```go
cluster, err := smkv.NewClusterClient(ctx,
    []string{"10.0.0.11:7381", "10.0.0.12:7381"},
    smkv.Options{PoolSize: 4, Timeout: 5 * time.Second})
if err != nil { return err }
defer cluster.Close()
err = cluster.Put(ctx, []byte("key"), []byte("value"))
```

This fragment belongs inside a function returning `error`. The SDK discovers the map and routes to the primary owner. Use `GetReplica` explicitly when stale replica reads are acceptable; primary reads do not silently fall back to replicas. All advertised data and management addresses must be reachable from the application.

## Set deadlines and durability

The earlier of the context deadline and configured timeout bounds the entire call, including pool waits and connection setup. Await a write before issuing a dependent read. Concurrent calls across connections have no ordering guarantee.

Omit `WithTTL` for no expiration. `WithTTL(0)` expires immediately. `WithSync()` waits for local sync, not replica confirmation. Current quorum RF2 builds additionally support `WithReplicaAck()` and `WithDurableReplicaAck()`. Keep the client timeout above the server's confirmation budget, with transport headroom.

## Handle outcomes deliberately

| Error or condition | Application response |
| --- | --- |
| `ErrNotFound` | Treat as absent or expired; choose your cache-miss path |
| `ErrBusy` | Request rejected before admission; back off within your deadline |
| `ErrCapacity` | Inspect capacity; repeated immediate retries cannot create headroom |
| `ErrOutcomeUnknown` | Mutation may have executed; reconcile before retrying |
| `ErrUnauthorized` | Repair identity, role, or credential configuration |
| Context cancellation/deadline | Stop waiting; still check mutation outcome classification |

Use `errors.Is`, not message matching. Check `ErrOutcomeUnknown` before deciding a failed mutation is safe to retry. Direct clients never retry automatically. The current cluster client can retry bounded transient reads and explicit wrong-owner rejections; it never blindly replays uncertain mutations. SDK behavior has evolved, so deploy clients and servers from compatible releases.

For typed data, continue with [tables and columns](tables.html).
