gomodule / redigo

Go client for Redis
Apache License 2.0
9.76k stars 1.25k forks source link

Serialising slices #591

Closed jspc closed 2 years ago

jspc commented 2 years ago

Hi there,

Given the following sample:

package main

import (
    "log"

    "github.com/gomodule/redigo/redis"
)

type Something struct {
    Foo string
    Bar string
    Baz []string
}

func main() {
    c, err := redis.DialURL("redis://localhost:6379/0")
    if err != nil {
        panic(err)
    }

    s1 := Something{
        Foo: "abc123",
        Bar: "def456",
        Baz: []string{
            "blah",
            "blah",
            "blah",
        },
    }

    _, err = c.Do("HSET", redis.Args{}.Add("something").AddFlat(s1)...)

    values, err := redis.Values(c.Do("HGETALL", "something"))
    if err != nil {
        panic(err)
    }

    s2 := Something{}

    err = redis.ScanStruct(values, &s2)
    if err != nil {
        panic(err)
    }

    log.Printf("s1: %#v", s1)
    log.Printf("s2: %#v", s2)
}

My program errors with:

panic: redigo.ScanStruct: cannot assign field Baz: cannot convert from Redis bulk string to []string

goroutine 1 [running]:
main.main()
        /home/jspc/sandbox/swip-swap-slice/main.go:42 +0x405

Because while redigo can serialise a slice of strings to redis (which it turns to "[blah blah blah]"), I can't figure out how to get redigo to turn it back into something useful.

Is there a way? Or does redigo need to fail on serialising in the first place, to avoid a situation where it can't read something back?

stevenh commented 2 years ago

To store a struct which contains more than just a simple key value pair, in your case key -> []string you need to write a custom encoder leveraging the RedisArg and RedisScan interfaces. Something like the following (not the most efficient code) will do the trick.

type Something struct {
        Foo string
        Bar string
        Baz stringSlice
}

type stringSlice []string

func (ss stringSlice) RedisArg() interface{} {
        var buf bytes.Buffer
        enc := gob.NewEncoder(&buf)
        enc.Encode(ss)
        return buf.Bytes()
}

func (ss *stringSlice) RedisScan(src interface{}) error {
        v, ok := src.([]byte)
        if !ok {
                return fmt.Errorf("string slice: cannot convert from %T to %T", src, ss)
        }

        var buf bytes.Buffer
        if _, err := buf.Write(v); err != nil {
                return fmt.Errorf("string slice: write failed: %w", err)
        }

        dec := gob.NewDecoder(&buf)
        if err := dec.Decode(ss); err != nil {
                return fmt.Errorf("string slice: decode failed: %w", err)
        }
        return nil
}
jspc commented 2 years ago

Wonderful, thanks @stevenh