Tip: If your type already implements MarshalBinary, UnmarshalBinary and ScanRedis, you’re covered for every read/write path—single values, hashes, and command arguments—without adding any other interfaces.
Minimal example showing all three methods on a User struct:
// RedisScanner is implemented by any type that can unmarshal itself
// from a Redis string.
import "github.com/redis/go-redis/v9"
var (
_ encoding.BinaryMarshaler = (*User)(nil)
_ encoding.BinaryUnmarshaler = (*User)(nil)
_ redis.Scanner = (*User)(nil)
)
// You can also use and implement custom RedisScanner interface
// import "github.com/redis/go-redis/v9"
// var _ RedisScanner = (*User)(nil)
// type RedisScanner interface {
// ScanRedis(string) error
// }
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
// MarshalBinary encodes User as JSON before writing to Redis.
func (u User) MarshalBinary() ([]byte, error) {
return json.Marshal(u)
}
// UnmarshalBinary decodes JSON returned by GET or cmd.Scan(&user).
func (u *User) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, u)
}
// ScanRedis lets rc.HGetAll(...).Scan(&user) populate the struct from a hash field.
func (u *User) ScanRedis(s string) error {
return json.Unmarshal([]byte(s), u)
}GoBelow is a “cheat-sheet” for the five interfaces you ever need to think about with go-redis /v9.
Read it row-by-row: pick the operation you’re doing and see which interface the client will look for.
| Interface you implement | Where go-redis looks for it | When it is called | What your method receives / returns | Typical use-case |
|---|---|---|---|---|
encoding.BinaryMarshaler | While building any command (SET, HSET, RPUSH, Lua args, …) | Writing data to Redis | You return the exact bytes that should be sent | Serialise structs or slices in one shot (e.g. JSON, MsgPack) before rc.Set(...) |
encoding.TextMarshaler | Same place as above but only if the type does not have MarshalBinary | Writing data | Return UTF-8 text; Redis still stores it as bytes | Human-readable text representation (UIDs, URLs, “42”) when you don’t care about binary |
encoding.BinaryUnmarshaler | When you call cmd.Scan(&dst) on replies coming from GET, HGET, EVAL, etc. | Reading a single value back | You receive the raw byte slice Redis replied with | Turn the bytes you wrote via MarshalBinary back into your struct |
encoding.TextUnmarshaler | Inside the hash-to-struct helper rc.HGetAll(...).Scan(&myStruct) (only if ScanRedis isn’t present) | Reading a hash field into a struct | You get the field’s text ([]byte, UTF-8) | Quick way to parse simple string fields (int, time, enum) without custom logic |
hscan.Scanner (re-exported as | First choice in the same hash-to-struct helper | Reading a hash field | You get the field as a string (already decoded from bytes) | Full control over complex fields in hashes; preferred if you need validation |
| Operation in your code | What go-redis does internally | Preference order that it checks | Interface signature you implement | Typical payload you handle |
|---|---|---|---|---|
Writing data – any command argument (SET, HSET, Lua args, pipelines, …) | appendArg() walks every value | 1. encoding.BinaryMarshaler2. encoding.TextMarshaler3. fmt.Stringer or bare value | MarshalBinary() ([]byte, error)MarshalText() ([]byte, error) | JSON / MsgPack blob, or plain text/number |
Reading a single value (GET key, HGET field, script return, …) followed by cmd.Scan(&dst) | proto.Scan() converts the raw reply | 1. Built-in scalar types (*string, *int64, *time.Time, …) ️️️️️️️️️️️️️️️️️️️2. encoding.BinaryUnmarshaler | UnmarshalBinary([]byte) error | Byte slice ↔ struct round-trip you stored with MarshalBinary |
Reading a hash into a struct (HGetAll().Scan(&dstStruct)) | hscan maps each field | 1. hscan.Scanner / redis.Scanner2. encoding.TextUnmarshaler3. Built-in string→int/float/bool converters | ScanRedis(string) errorUnmarshalText([]byte) error | Custom field parsing or quick string→time.Duration, enum, etc. |
How to read the table
- Write path (to Redis) – look at the two “Marshaler” rows.
- If your type has
MarshalBinary, that wins. - Otherwise,
MarshalText(or evenfmt.Stringer) is used.
- If your type has
- Read path (single value) – only
UnmarshalBinarymatters. - Read path (hash → struct) – client tries
ScanRedisfirst, thenUnmarshalText, then falls back to the built-in converters (string→int, bool, etc.).
Do you still need UnmarshalBinary if you already have ScanRedis or UnmarshalText?
Yes, when you also read the value outside of a hash (e.g. GET key followed by cmd.Scan(&v)).ScanRedis/UnmarshalText are only for the hash helper; they are never called for plain replies.
Quick recipes
| Scenario | What to implement |
|---|---|
Storing an entire struct with SET and later GET-ing it back | MarshalBinary + UnmarshalBinary |
| Adding that same struct as a field value inside a Redis hash | The two above plus ScanRedis or UnmarshalText |
| Hash field is just an int but you want automatic conversion | Only UnmarshalText (no need for custom marshal; HSET will write the int as string automatically) |
| You never scan single values, only hashes | Skip UnmarshalBinary; stick to ScanRedis/UnmarshalText |
With this table you can decide, at a glance, which interface your custom type really needs and avoid the classic “can’t marshal/unmarshal (implement …)” errors.