0%

go redis can't marshal xxx (implement encoding.BinaryMarshaler)

在go项目中使用redis时,难免要使用Get/Set存取一些复杂的数据结构
直接缓存会报错

1
redis: can't marshal xxx (implement encoding.BinaryMarshaler)

xxx为对应的数据结构
此时就要自定义一些方法来解决了

当前使用的redis客户端为 go-redis v6
msgpack版本为 msgpack v4(4.0.4)
需要为缓存的对象添加两个方法,go-redis会自动应用

1
2
MarshalBinary() ([]byte, error)
UnmarshalBinary(data []byte) error

在其中实现序列化与反序列化的方法即可,一般使用json库
当然为了性能考虑,也可以使用其它更高效的库,如msgpack,benchmark有5-10倍提升

1
2
3
4
5
BenchmarkStructVmihailencoMsgpack-4   	  200000	     12814 ns/op	    2128 B/op	      26 allocs/op 
BenchmarkStructUgorjiGoMsgpack-4 100000 17678 ns/op 3616 B/op 70 allocs/op
BenchmarkStructUgorjiGoCodec-4 100000 19053 ns/op 7346 B/op 23 allocs/op
BenchmarkStructJSON-4 20000 69438 ns/op 7864 B/op 26 allocs/op
BenchmarkStructGOB-4 10000 104331 ns/op 14664 B/op 278 allocs/op

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"time"
"github.com/go-redis/redis"
"github.com/vmihailenco/msgpack"
)

func main() {
client := redis.NewClient(&redis.Options{
Addr: "192.168.0.1:6379",
Password: "kljsdfslkdfj", // password
DB: 0, // use default DB
})

pong, err := client.Ping().Result()
fmt.Println(pong, err)

fmt.Println(client.Set("cache1", &something{100, "101"}, time.Second*1).Result())
fmt.Println(client.Get("cache1").Result())
}

type something struct {
ID int
Name string
}

func (s *something) MarshalBinary() ([]byte, error) {
return msgpack.Marshal(s)
}

func (s *something) UnmarshalBinary(data []byte) error {
return msgpack.Unmarshal(data, s)
}

新版本不可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// types.go
var (
binaryMarshalerType = reflect.TypeOf((*encoding.BinaryMarshaler)(nil)).Elem()
binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
)

// encode_value.go
func _getEncoder(typ reflect.Type) encoderFunc {
kind := typ.Kind()

if kind == reflect.Ptr {
if _, ok := typeEncMap.Load(typ.Elem()); ok {
return ptrEncoderFunc(typ)
}
}

if typ.Implements(customEncoderType) {
return encodeCustomValue
}
if typ.Implements(marshalerType) {
return marshalValue
}
if typ.Implements(binaryMarshalerType) {
return marshalBinaryValue
}
...
}

msgpack更新到4.3版本后(不确认具体版本),上述用法会由于循环调用而造成堆栈泄露

1
2
3
4
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc020bc0378 stack=[0xc020bc0000, 0xc040bc0000]
fatal error: stack overflow
...

由于上述判断,实现*encoding.BinaryMarshaler接口的类型,将调用其MarshalBinary方法,但该方法又是通过调用msgpack的Marshal方法实现的,从而导致循环

1
NewEncoder() -> Pool Get Encocer -> Marshal() -> 数据的MarshalBinary方法 -> marshalBinaryValue() -> EncodeValue() -> Encode() -> Marshal() -> 数据的MarshalBinary方法 ...

解决方案

如果一定还是要用之前的方式,那么需要做如下修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "github.com/vmihailenco/msgpack/v5"

type baseType struct {
Tag string
}

type newType []baseType

func (s *newType) MarshalBinary() ([]byte, error) {
return msgpack.Marshal((*[]baseType)(s))
}

func (s *newType) UnmarshalBinary(data []byte) error {
return msgpack.Unmarshal(data, (*[]baseType)(s))
}

参考链接

go-redis
go-redis issues
msgpack
原文链接