Handle command results efficiently
Scan go-redis command results into Go values and use buffers for large string payloads.
go-redis can convert command results directly into Go values. This is useful
when you want to keep application code close to your domain types instead of
working with strings and maps everywhere. You can scan hash
results into structs, scan list-style command results into slices, and use
byte buffers for large string
values.
Initialize
Import the packages you need:
import (
"context"
"fmt"
"strings"
"github.com/redis/go-redis/v9"
)
package example_commands_test
import (
"context"
"fmt"
"strings"
"github.com/redis/go-redis/v9"
)
func ExampleClient_scanBuffer() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password
DB: 0, // use default DB
Protocol: 2,
})
defer rdb.Close()
type Bike struct {
Model string `redis:"model"`
Brand string `redis:"brand"`
Price int `redis:"price"`
}
if err := rdb.HSet(ctx, "scanbuf:bike:1",
"model", "Deimos",
"brand", "Ergonom",
"price", 4972,
).Err(); err != nil {
panic(err)
}
var bike Bike
if err := rdb.HGetAll(ctx, "scanbuf:bike:1").Scan(&bike); err != nil {
panic(err)
}
fmt.Printf("Model: %s, brand: %s, price: $%d\n",
bike.Model, bike.Brand, bike.Price)
// >>> Model: Deimos, brand: Ergonom, price: $4972
type BikeStock struct {
Model string `redis:"model"`
Stock int `redis:"stock"`
Price int `redis:"price"`
}
var partial BikeStock
if err := rdb.HMGet(ctx, "scanbuf:bike:1", "model", "stock").Scan(&partial); err != nil {
panic(err)
}
fmt.Printf("Model: %s, stock: %d, price: $%d\n",
partial.Model, partial.Stock, partial.Price)
// >>> Model: Deimos, stock: 0, price: $0
if err := rdb.RPush(ctx, "scanbuf:stock", 3, 4, 5).Err(); err != nil {
panic(err)
}
var stockCounts []int
if err := rdb.LRange(ctx, "scanbuf:stock", 0, -1).ScanSlice(&stockCounts); err != nil {
panic(err)
}
fmt.Println("Stock counts:", stockCounts)
// >>> Stock counts: [3 4 5]
payload := []byte("compact bike data")
if err := rdb.SetFromBuffer(ctx, "scanbuf:payload", payload).Err(); err != nil {
panic(err)
}
fmt.Println("OK") // >>> OK
buf := make([]byte, len(payload))
cmd := rdb.GetToBuffer(ctx, "scanbuf:payload", buf)
n, err := cmd.Result()
if err != nil {
panic(err)
}
fmt.Printf("Read %d bytes: %s\n", n, string(cmd.Bytes()))
// >>> Read 17 bytes: compact bike data
smallBuf := make([]byte, 4)
smallCmd := rdb.GetToBuffer(ctx, "scanbuf:payload", smallBuf)
if err := smallCmd.Err(); err != nil {
if strings.Contains(err.Error(), "buffer too small") {
fmt.Println("Buffer too small")
} else {
panic(err)
}
}
// >>> Buffer too small
}
Connect to Redis:
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password
DB: 0, // use default DB
Protocol: 2,
})
defer rdb.Close()
Scan hashes into structs
Use struct tags of the form redis:"field" to map Redis hash fields to Go
struct fields. The Scan() method converts matching fields to the destination
types and returns an error if a conversion fails.
The following example stores a hash with HSET
and then scans the result of HGETALL
into a Bike struct:
type Bike struct {
Model string `redis:"model"`
Brand string `redis:"brand"`
Price int `redis:"price"`
}
if err := rdb.HSet(ctx, "scanbuf:bike:1",
"model", "Deimos",
"brand", "Ergonom",
"price", 4972,
).Err(); err != nil {
panic(err)
}
var bike Bike
if err := rdb.HGetAll(ctx, "scanbuf:bike:1").Scan(&bike); err != nil {
panic(err)
}
fmt.Printf("Model: %s, brand: %s, price: $%d\n",
bike.Model, bike.Brand, bike.Price)
// >>> Model: Deimos, brand: Ergonom, price: $4972
You can also scan a subset of fields with HMGET.
Fields that are not returned keep their Go zero values:
type BikeStock struct {
Model string `redis:"model"`
Stock int `redis:"stock"`
Price int `redis:"price"`
}
var partial BikeStock
if err := rdb.HMGet(ctx, "scanbuf:bike:1", "model", "stock").Scan(&partial); err != nil {
panic(err)
}
fmt.Printf("Model: %s, stock: %d, price: $%d\n",
partial.Model, partial.Stock, partial.Price)
// >>> Model: Deimos, stock: 0, price: $0
Scan lists into slices
Commands that return a list of strings, such as LRANGE,
can use ScanSlice() to convert the result into a typed Go slice:
if err := rdb.RPush(ctx, "scanbuf:stock", 3, 4, 5).Err(); err != nil {
panic(err)
}
var stockCounts []int
if err := rdb.LRange(ctx, "scanbuf:stock", 0, -1).ScanSlice(&stockCounts); err != nil {
panic(err)
}
fmt.Println("Stock counts:", stockCounts)
// >>> Stock counts: [3 4 5]
Use byte buffers
For large string payloads, you can avoid creating a new string for every read by
providing a caller-owned byte buffer. Use SetFromBuffer() to write a []byte
value and GetToBuffer() to read the value into an existing buffer.
github.com/redis/go-redis/v9 v9.21.0
or later. payload := []byte("compact bike data")
if err := rdb.SetFromBuffer(ctx, "scanbuf:payload", payload).Err(); err != nil {
panic(err)
}
fmt.Println("OK") // >>> OK
buf := make([]byte, len(payload))
cmd := rdb.GetToBuffer(ctx, "scanbuf:payload", buf)
n, err := cmd.Result()
if err != nil {
panic(err)
}
fmt.Printf("Read %d bytes: %s\n", n, string(cmd.Bytes()))
// >>> Read 17 bytes: compact bike data
GetToBuffer() returns a *redis.ZeroCopyStringCmd. Use Val() or Result()
to get the number of bytes read, Bytes() to access the populated slice
(buf[:n]), and Err() to check for errors such as redis.Nil when the key
does not exist.
GetToBuffer() requires a buffer large enough to hold the whole value. It also
opts out of automatic retries because a failed read might already have written
partial data into your buffer. If a buffer is too small, Err() reports a
buffer too small error.The following example shows how to detect a buffer that is too small:
smallBuf := make([]byte, 4)
smallCmd := rdb.GetToBuffer(ctx, "scanbuf:payload", smallBuf)
if err := smallCmd.Err(); err != nil {
if strings.Contains(err.Error(), "buffer too small") {
fmt.Println("Buffer too small")
} else {
panic(err)
}
}
// >>> Buffer too small
SetFromBuffer() does not set an expiration. If you need a TTL, call
EXPIRE separately with Expire(), or use
Set() with an expiration when the extra allocation is acceptable.
More information
See the go-redis repository for more
examples and API details.