Go API¶
The Go client provides idiomatic bindings with proper error handling and context support.
Installation¶
Prerequisites
The FoundationDB client library must be installed on your system. See Installation.
Quick Start¶
Go
package main
import (
"fmt"
"log"
"github.com/apple/foundationdb/bindings/go/src/fdb"
)
func main() {
// REQUIRED: Set API version (must match or be less than installed version)
fdb.MustAPIVersion(730)
// Open the default database
db := fdb.MustOpenDefault()
// Run a transaction
_, err := db.Transact(func(tr fdb.Transaction) (interface{}, error) {
tr.Set(fdb.Key("hello"), []byte("world"))
return nil, nil
})
if err != nil {
log.Fatal(err)
}
// Read the value
result, err := db.Transact(func(tr fdb.Transaction) (interface{}, error) {
return tr.Get(fdb.Key("hello")).MustGet(), nil
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Value: %s\n", result.([]byte))
}
Core Concepts¶
API Versioning¶
Go
// Must be called before any FDB operations
fdb.MustAPIVersion(730)
// Or with error handling
err := fdb.APIVersion(730)
if err != nil {
log.Fatal(err)
}
Opening the Database¶
Go
// Default cluster file
db := fdb.MustOpenDefault()
// Custom cluster file
db, err := fdb.OpenDatabase("/path/to/fdb.cluster")
if err != nil {
log.Fatal(err)
}
// With options
db, err := fdb.OpenDefault(fdb.DefaultClusterFile)
Transactions¶
Using Transact() (Recommended)¶
The Transact() method handles retries automatically:
Go
result, err := db.Transact(func(tr fdb.Transaction) (interface{}, error) {
// Read
value := tr.Get(fdb.Key("counter")).MustGet()
// Compute
count := 0
if value != nil {
count = int(binary.LittleEndian.Uint64(value))
}
// Write
newValue := make([]byte, 8)
binary.LittleEndian.PutUint64(newValue, uint64(count+1))
tr.Set(fdb.Key("counter"), newValue)
return count + 1, nil
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("New count: %d\n", result.(int))
Using ReadTransact()¶
For read-only transactions:
Go
result, err := db.ReadTransact(func(tr fdb.ReadTransaction) (interface{}, error) {
return tr.Get(fdb.Key("mykey")).MustGet(), nil
})
Manual Transaction Control¶
Go
tr, err := db.CreateTransaction()
if err != nil {
log.Fatal(err)
}
for {
tr.Set(fdb.Key("key"), []byte("value"))
err := tr.Commit().Get()
if err == nil {
break
}
// Handle retryable errors
fe := err.(fdb.Error)
err = tr.OnError(fe).Get()
if err != nil {
log.Fatal(err)
}
}
Reading Data¶
Single Key¶
Go
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
// Get returns a FutureByteSlice
future := tr.Get(fdb.Key("mykey"))
// Block until value is available
value := future.MustGet() // Returns nil if key doesn't exist
// Or with error handling
value, err := future.Get()
return value, err
})
Range Reads¶
Go
import "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
// Define range
begin := tuple.Tuple{"users"}.Pack()
end := tuple.Tuple{"users", nil}.Pack() // nil = max value
// Create range
r := fdb.KeyRange{Begin: begin, End: end}
// Iterate over results
ri := tr.GetRange(r, fdb.RangeOptions{}).Iterator()
for ri.Advance() {
kv := ri.MustGet()
key, _ := tuple.Unpack(kv.Key)
fmt.Printf("User: %v = %s\n", key, kv.Value)
}
return nil, nil
})
Range Options¶
Go
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
r := fdb.KeyRange{Begin: begin, End: end}
opts := fdb.RangeOptions{
Limit: 100, // Max results
Reverse: true, // Reverse order
Mode: fdb.StreamingModeWantAll,
}
ri := tr.GetRange(r, opts).Iterator()
for ri.Advance() {
kv := ri.MustGet()
// Process key-value
}
// Or get all as slice
slice, err := tr.GetRange(r, opts).GetSliceWithError()
return slice, err
})
Writing Data¶
Set and Clear¶
Go
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
// Set a value
tr.Set(fdb.Key("key"), []byte("value"))
// Clear a key
tr.Clear(fdb.Key("key"))
// Clear a range
tr.ClearRange(fdb.KeyRange{
Begin: fdb.Key("prefix\x00"),
End: fdb.Key("prefix\xff"),
})
return nil, nil
})
Atomic Operations¶
Go
import "encoding/binary"
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
key := fdb.Key("counter")
// Atomic add
delta := make([]byte, 8)
binary.LittleEndian.PutUint64(delta, 1)
tr.Add(key, delta)
// Bitwise operations
tr.BitAnd(key, mask)
tr.BitOr(key, mask)
tr.BitXor(key, mask)
// Min/Max
tr.Min(key, value)
tr.Max(key, value)
return nil, nil
})
Tuple Layer¶
Go
import "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
// Create tuples
key := tuple.Tuple{"users", "alice", "profile"}
packed := key.Pack()
// Unpack tuples
unpacked, err := tuple.Unpack(packed)
userType := unpacked[0].(string) // "users"
userId := unpacked[1].(string) // "alice"
// Supported types
mixed := tuple.Tuple{
"string", // string
int64(42), // int64
float64(3.14), // float64
true, // bool
nil, // nil (sorts last)
[]byte{1, 2, 3}, // []byte
tuple.UUID{...}, // UUID
}
Directory Layer¶
Go
import "github.com/apple/foundationdb/bindings/go/src/fdb/directory"
// Open directories
users, err := directory.CreateOrOpen(db, []string{"myapp", "users"}, nil)
if err != nil {
log.Fatal(err)
}
orders, err := directory.CreateOrOpen(db, []string{"myapp", "orders"}, nil)
if err != nil {
log.Fatal(err)
}
// Use subspace for keys
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
key := users.Pack(tuple.Tuple{"alice", "email"})
tr.Set(key, []byte("alice@example.com"))
return nil, nil
})
Error Handling¶
FDB Errors¶
Go
result, err := db.Transact(func(tr fdb.Transaction) (interface{}, error) {
return tr.Get(fdb.Key("key")).Get()
})
if err != nil {
if fe, ok := err.(fdb.Error); ok {
switch fe.Code {
case 1007: // past_version
fmt.Println("Transaction too old")
case 1020: // not_committed
fmt.Println("Conflict detected")
default:
fmt.Printf("FDB error %d: %s\n", fe.Code, fe.Error())
}
}
}
Common Error Codes¶
| Code | Name | Description |
|---|---|---|
| 1007 | past_version | Transaction too old |
| 1009 | future_version | Cluster version ahead |
| 1020 | not_committed | Conflict during commit |
| 1021 | commit_unknown_result | Commit outcome unknown |
| 2000 | client_invalid_operation | Invalid API usage |
Common Patterns¶
Thread-Safe Counter¶
Go
type Counter struct {
db fdb.Database
key fdb.Key
}
func NewCounter(db fdb.Database, name string) *Counter {
return &Counter{
db: db,
key: tuple.Tuple{"counters", name}.Pack(),
}
}
func (c *Counter) Increment() error {
_, err := c.db.Transact(func(tr fdb.Transaction) (interface{}, error) {
delta := make([]byte, 8)
binary.LittleEndian.PutUint64(delta, 1)
tr.Add(c.key, delta)
return nil, nil
})
return err
}
func (c *Counter) Get() (int64, error) {
result, err := c.db.ReadTransact(func(tr fdb.ReadTransaction) (interface{}, error) {
value := tr.Get(c.key).MustGet()
if value == nil {
return int64(0), nil
}
return int64(binary.LittleEndian.Uint64(value)), nil
})
if err != nil {
return 0, err
}
return result.(int64), nil
}
Watch for Changes¶
Go
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
watch := tr.Watch(fdb.Key("mykey"))
// Start a goroutine to wait for changes
go func() {
err := watch.Get()
if err != nil {
log.Printf("Watch error: %v", err)
return
}
fmt.Println("Key changed!")
}()
return nil, nil
})
Context Support¶
Go
import "context"
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Use context with options
opts := fdb.TransactionOptions{}
opts.SetTimeout(5000) // 5 seconds in milliseconds
result, err := db.Transact(func(tr fdb.Transaction) (interface{}, error) {
tr.Options().SetTimeout(5000)
return tr.Get(fdb.Key("key")).Get()
})
Best Practices¶
Do
- Use
Transact()for automatic retry handling - Use
MustGet()only when errors are truly unexpected - Use the tuple layer for structured keys
- Handle
fdb.Errorappropriately in production
Don't
- Don't ignore errors from FDB operations
- Don't store values larger than 100KB
- Don't hold transactions open for long periods
- Don't use
panic()in production transaction code