最近,同事遇到一个神奇的问题,在日志中,发现了打印了下面的 JSON 请求:
{
"types": "Bgk=",
"data": "something"
}
这个 types 就很神奇,按理说,它应该是一个枚举类型数组,至少是几个数字,而不是这样一串莫名其妙的字符;它也没有引起任何的异常,一切都像应该的那样运行。
看到这样一串数据,我首先想到的就是,它会不会是一个 base64 编码?因为它以等号 “=” 结尾,是一个比较明显的 padding 特征。使用在线 base64 工具解码后,发现它不是一个 UTF-8 字符:
于是将它转换为十六进制 HEX,查看解码出来的结果是啥:
可见,解码出来的信息是 0609,确实是原枚举值数组的 [6, 9]
。
不过,为什么 golang 的 encoding/json
包会选择这样序列化呢?
为了寻找这一问题的根源,我发现此枚举的定义是:
type XXEnum uint8
此枚举是 uint8 的别名!很快啊,我写了几个测试:
func TestUint8(t *testing.T) {
var data uint8 = 32
marshal, err := json.Marshal(data)
if err != nil {
panic(err)
}
fmt.Println(string(marshal)) //32
}
一个 uint8 会被序列化成十进制数字。
func TestSliceUint8(t *testing.T) {
var data = [1]uint8{
32,
}
marshal, err := json.Marshal(data)
if err != nil {
panic(err)
}
fmt.Println(string(marshal)) //"IA=="
}
一个 uint8 切片会被序列化成 base64 编码后的 字符串。
func TestSliceUint8(t *testing.T) {
var data = [1]uint8{
32,
}
marshal, err := json.Marshal(data)
if err != nil {
panic(err)
}
fmt.Println(string(marshal)) //[32]
}
一个 uint8 数组会被序列化成一个 JSON 数组。
func TestSliceUint16(t *testing.T) {
var data = []uint16{
32,
}
marshal, err := json.Marshal(data)
if err != nil {
panic(err)
}
fmt.Println(string(marshal)) //[32]
}
一个 int16 切片会被序列化成一个 JSON 数组。
可见,只有 uint8 数组会被序列化为一个 base64 编码的字符串,这很奇怪,为什么 uint8 是特殊的呢?为什么不把它像 int8 那样用一个真正的 json 数组序列化呢?
事实上,golang 中,对 byte 的定义是这样的:
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is // used, by convention, to distinguish byte values from 8-bit unsigned // integer values. type byte = uint8
在 golang 中,“byte 被认为是 uint8 的一个别名,它们是完全等同的”
对于 byte 切片,人们通常认为它是一个二进制序列,因而,在 go 自带的 JSON 序列化工具中,它被序列化为用 base64 编码的字符串,这在一定程度上具有合理性,而 uint8,倒反天罡,受到了对 byte 切片特殊处理的反作用!
笔者认为这一变换多少有点粗暴,”无符号整数切片“ 和 ”字节切片“ 在表意上还是有较大区别。
再查看 encoding/json
包的源码,发现在选择最终 Encoder 进行序列化时,事实上对 uint8 的切片做了特殊处理:
在 newTypeEncoder
,使用反射实现对不同 type 的 序列化器 选择:
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
//...
switch t.Kind() {
case reflect.Bool:
return boolEncoder
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intEncoder
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintEncoder
case reflect.Float32:
return float32Encoder
case reflect.Float64:
return float64Encoder
case reflect.String:
return stringEncoder
case reflect.Interface:
return interfaceEncoder
case reflect.Struct:
return newStructEncoder(t)
case reflect.Map:
return newMapEncoder(t)
case reflect.Slice:
return newSliceEncoder(t) // Let's go here
case reflect.Array:
return newArrayEncoder(t)
case reflect.Pointer:
return newPtrEncoder(t)
default:
return unsupportedTypeEncoder
}
}
接着,进入 newSliceEncoder
:
func newSliceEncoder(t reflect.Type) encoderFunc {
// Byte slices get special treatment; arrays don't.
if t.Elem().Kind() == reflect.Uint8 {
p := reflect.PointerTo(t.Elem())
if !p.Implements(marshalerType) && !p.Implements(textMarshalerType) {
return encodeByteSlice
}
}
enc := sliceEncoder{newArrayEncoder(t)}
return enc.encode
}
可见,go-lib 在这里进行特殊处理!甚至还贴心地写上注释:Byte slices get special treatment; arrays don't.
在这个特殊的 encodeByteSlice
序列化器内,将会把字节切片/无符号整数切片给序列化为一个 base64 编码的字符串!
原因是找到了,但这合理吗?看着这行注释,我陷入了沉思。。。然后,写出了这样几行代码:
func TestArrayByte(t *testing.T) {
var data = [...]byte{
32,
}
marshal, err := json.Marshal(data)
if err != nil {
panic(err)
}
fmt.Println(string(marshal)) //[32]
}
我有点搞不懂这个包的设计者的用意了,如果是为了 byte 的特殊处理,为什么不把 byte 数组也一起特殊处理呢?只处理切片是出于什么考虑呢?
// to be continued