Skip to content

Redis client

Installation

go-redis supports 2 last Go versions and requires support for Go modules. So make sure to initialize a Go module:

go mod init github.com/my/repo

And then install redis/v8 (note v8 in the import; omitting it is a popular mistake):

go get github.com/go-redis/redis/v8

Connecting to Redis Server

To connect to a Redis database:

import "github.com/go-redis/redis/v8"

rdb := redis.NewClient(&redis.Options{
    Addr:     "localhost:6379",
    Password: "", // no password set
    DB:       0,  // use default DB
})

Another popular way is using a connection string:

opt, err := redis.ParseURL("redis://<user>:<pass>@localhost:6379/<db>")
if err != nil {
    panic(err)
}

rdb := redis.NewClient(opt)

redis.Nil

go-redis exports the redis.Nil error and returns it only when Redis Server responds with (nil). You can check with redis-cli what response Redis returns.

In the following example redis.Nil is useful to distinguish between an empty string reply and a nil reply (key does not exist):

val, err := rdb.Get(ctx, "key").Result()
switch {
case err == redis.Nil:
    fmt.Println("key does not exist")
case err != nil:
    fmt.Println("Get failed", err)
case val == "":
    fmt.Println("value is empty")
}

You should be aware that GET is not the only command that returns redis.Nil. For example, BLPOP and ZSCORE also return redis.Nil.

Executing commands

To execute a command:

val, err := rdb.Get(ctx, "key").Result()
fmt.Println(val)

Alternatively you can save the command and access the value and the error separately:

get := rdb.Get(ctx, "key")
fmt.Println(get.Val(), get.Err())

When appropriate commands provide helper methods:

// Shortcut for get.Val().(string) with error handling.
s, err := get.Text()

num, err := get.Int()

num, err := get.Int64()

num, err := get.Uint64()

num, err := get.Float32()

num, err := get.Float64()

flag, err := get.Bool()

Executing unsupported commands

To execute arbitrary/custom command:

val, err := rdb.Do(ctx, "get", "key").Result()
if err != nil {
    if err == redis.Nil {
        fmt.Println("key does not exists")
        return
    }
    panic(err)
}
fmt.Println(val.(string))

Pipelining

To execute multiple commands in a single write/read pipeline:

var incr *redis.IntCmd
_, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
    incr = pipe.Incr(ctx, "pipelined_counter")
    pipe.Expire(ctx, "pipelined_counter", time.Hour)
    return nil
})
if err != nil {
    panic(err)
}

// The value is available only after pipeline is executed.
fmt.Println(incr.Val())

Alternatively you can create and execute pipeline manually:

pipe := rdb.Pipeline()

incr := pipe.Incr(ctx, "pipeline_counter")
pipe.Expire(ctx, "pipeline_counter", time.Hour)

_, err := pipe.Exec(ctx)
if err != nil {
    panic(err)
}

// The value is available only after Exec.
fmt.Println(incr.Val())

To wrap commands with multi and exec commands, use TxPipelined / TxPipeline.

Transactions and Watch

To watch for changes in keys and commit a transaction only if keys remain unchanged. Note how we use redis.TxFailedErr to check if a transaction has failed or not.

const maxRetries = 1000

// Increment transactionally increments key using GET and SET commands.
increment := func(key string) error {
    // Transactional function.
    txf := func(tx *redis.Tx) error {
        // Get current value or zero.
        n, err := tx.Get(ctx, key).Int()
        if err != nil && err != redis.Nil {
            return err
        }

        // Actual operation (local in optimistic lock).
        n++

        // Operation is commited only if the watched keys remain unchanged.
        _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
            pipe.Set(ctx, key, n, 0)
            return nil
        })
        return err
    }

    for i := 0; i < maxRetries; i++ {
        err := rdb.Watch(ctx, txf, key)
        if err == nil {
            // Success.
            return nil
        }
        if err == redis.TxFailedErr {
            // Optimistic lock lost. Retry.
            continue
        }
        // Return any other error.
        return err
    }

    return errors.New("increment reached maximum number of retries")
}

PubSub

go-redis allows to publish messages and subscribe to channels. It also automatically handles reconnects.

To publish a message:

err := rdb.Publish(ctx, "mychannel1", "payload").Err()
if err != nil {
    panic(err)
}

To subscribe to a channel:

// There is no error because go-redis automatically reconnects on error.
pubsub := rdb.Subscribe(ctx, "mychannel1")

To receive a message:

for {
    msg, err := pubsub.ReceiveMessage(ctx)
    if err != nil {
        panic(err)
    }

    fmt.Println(msg.Channel, msg.Payload)
}

But the simplest way is using a Go channel:

ch := pubsub.Channel()

for msg := range ch {
    fmt.Println(msg.Channel, msg.Payload)
}