本文最后更新于:2023年8月17日 凌晨

前言

开发中偶然发现,结构体中声明了[]byte类型的字段,json marshal后,[]byte类型的字段会被base64编码,下面通过一个例子来找找base64编码的原因

排查过程

例子

package main

import (
	"encoding/json"
	"testing"
)

type Reply struct {
	Name    string
	Content []byte
}

func TestJSONMarshal(t *testing.T) {
	var r = Reply{
		Name:    "lam",
		Content: []byte("pongpong"),
	}
	res, _ := json.Marshal(&r)
	t.Log(string(res))
}

输出

=== RUN   TestJSONMarshal
    client_test.go:26: {"Name":"lam","Content":"cG9uZ3Bvbmc="}
--- PASS: TestJSONMarshal (0.00s)
PASS

跟着debug进入encoding/json 包内部,发现注释部分已经说明[]byte类型会被base64编码:

// ...
// Array and slice values encode as JSON arrays, except that
// []byte encodes as a base64-encoded string, and a nil slice
// encodes as the null JSON value.
// ...

继续根据方法调用链找到具体的编码位置:

func (e *encodeState) marshal(v any, opts encOpts) (err error) {
	defer func() {
		if r := recover(); r != nil {
			if je, ok := r.(jsonError); ok {
				err = je.error
			} else {
				panic(r)
			}
		}
	}()
	e.reflectValue(reflect.ValueOf(v), opts)
	return nil
}
func (e *encodeState) reflectValue(v reflect.Value, opts encOpts) {
	valueEncoder(v)(e, v, opts)
}

func valueEncoder(v reflect.Value) encoderFunc {
	if !v.IsValid() {
		return invalidValueEncoder
	}
	return typeEncoder(v.Type())
}
func typeEncoder(t reflect.Type) encoderFunc {
        // ...
	// Compute the real encoder and replace the indirect func with it.
	f = newTypeEncoder(t, true)
	wg.Done()
	encoderCache.Store(t, f)
	return f
}
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
        // ...
	switch t.Kind() {
        // ...
	case reflect.Slice:
		return newSliceEncoder(t)

        // ...
	default:
		return unsupportedTypeEncoder
	}
}
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
}

该方法会根据slice的长度选择效率更高的字符串拼接方式

func encodeByteSlice(e *encodeState, v reflect.Value, _ encOpts) {
	if v.IsNil() {
		e.WriteString("null")
		return
	}
	s := v.Bytes()
	e.WriteByte('"')
	encodedLen := base64.StdEncoding.EncodedLen(len(s))
	if encodedLen <= len(e.scratch) {
		// If the encoded bytes fit in e.scratch, avoid an extra
		// allocation and use the cheaper Encoding.Encode.
		dst := e.scratch[:encodedLen]
		base64.StdEncoding.Encode(dst, s)
		e.Write(dst)
	} else if encodedLen <= 1024 {
		// The encoded bytes are short enough to allocate for, and
		// Encoding.Encode is still cheaper.
		dst := make([]byte, encodedLen)
		base64.StdEncoding.Encode(dst, s)
		e.Write(dst)
	} else {
		// The encoded bytes are too long to cheaply allocate, and
		// Encoding.Encode is no longer noticeably cheaper.
		enc := base64.NewEncoder(base64.StdEncoding, e)
		enc.Write(s)
		enc.Close()
	}
	e.WriteByte('"')
}

总结

至此,[]byte被base64编码的代码已经找着了,思考了下go之所以选择编码[]byte,是为了避免使用utf-8进行编码转换的时候出现非预期的字符,导致“乱码”问题;类似的在AES加密等场景下,也会选择把加密后的二进制数组使用base64或其他方式的编码为字符串进行传输和存储,待使用时进行base64解码,再操作原始的[]byte数据。


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

drpc的代码生成工具的简单改造 上一篇
记一次tcp报文分隔问题处理 下一篇