Code with Abrar


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)
}
Go

Below 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 implementWhere go-redis looks for itWhen it is calledWhat your method receives / returnsTypical use-case
encoding.BinaryMarshaler
MarshalBinary() ([]byte, error)
While building any command (SET, HSET, RPUSH, Lua args, …)Writing data to RedisYou return the exact bytes that should be sentSerialise structs or slices in one shot (e.g. JSON, MsgPack) before rc.Set(...)
encoding.TextMarshaler
MarshalText() ([]byte, error)
Same place as above but only if the type does not have MarshalBinaryWriting dataReturn UTF-8 text; Redis still stores it as bytesHuman-readable text representation (UIDs, URLs, “42”) when you don’t care about binary
encoding.BinaryUnmarshaler
UnmarshalBinary([]byte) error
When you call cmd.Scan(&dst) on replies coming from GET, HGET, EVAL, etc.Reading a single value backYou receive the raw byte slice Redis replied withTurn the bytes you wrote via MarshalBinary back into your struct
encoding.TextUnmarshaler
UnmarshalText([]byte) error
Inside the hash-to-struct helper rc.HGetAll(...).Scan(&myStruct) (only if ScanRedis isn’t present)Reading a hash field into a structYou 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 redis.Scanner)
ScanRedis(string) error
First choice in the same hash-to-struct helperReading a hash fieldYou 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 codeWhat go-redis does internallyPreference order that it checksInterface signature you implementTypical payload you handle
Writing data – any command argument (SET, HSET, Lua args, pipelines, …)appendArg() walks every value1. encoding.BinaryMarshaler
2. encoding.TextMarshaler
3. 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 reply1. Built-in scalar types (*string, *int64, *time.Time, …) ️️️️️️️️️️️️️️️️️️️
2. encoding.BinaryUnmarshaler
UnmarshalBinary([]byte) errorByte slice ↔ struct round-trip you stored with MarshalBinary
Reading a hash into a struct (HGetAll().Scan(&dstStruct))hscan maps each field1. hscan.Scanner / redis.Scanner
2. encoding.TextUnmarshaler
3. Built-in string→int/float/bool converters
ScanRedis(string) error
UnmarshalText([]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 even fmt.Stringer) is used.
  • Read path (single value) – only UnmarshalBinary matters.
  • Read path (hash → struct) – client tries ScanRedis first, then UnmarshalText, 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

ScenarioWhat to implement
Storing an entire struct with SET and later GET-ing it backMarshalBinary + UnmarshalBinary
Adding that same struct as a field value inside a Redis hashThe two above plus ScanRedis or UnmarshalText
Hash field is just an int but you want automatic conversionOnly UnmarshalText (no need for custom marshal; HSET will write the int as string automatically)
You never scan single values, only hashesSkip 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.