场景复现
在 API 实现中返回一个 JSON 结果,其中有一个字段为 URL 链接,客户端拿到该链接后做请求,URL 链接中存在多个使用 &
连接的 querystring 参数。
服务端实现时,通过构造结构体后返回对应的 JSON 序列化结果。
但是请求接口时发现 URL 链接中的 &
符号被自动转义为 \u0026
,导致历史版本的客户端无法解析 URL 中的参数。
一段代码模拟该场景:
package main
import (
"encoding/json"
"fmt"
)
type Data struct {
Link string
}
func main() {
link := `https://mbti.axiaoxin.com/?lang=ko&from=article`
data := Data{
Link: link,
}
b, _ := json.Marshal(data)
fmt.Println("data json:", string(b))
fmt.Println("---")
}
代码执行结果输出:
data json: {"Link":"https://mbti.axiaoxin.com/?lang=ko\u0026from=article"}
---
其中的 &
变成了 \u0026
。
原因分析
Golang 在 json 序列化时,默认会对特殊的 html 字符进行转义处理。
json.Marshal
的源码实现:
func Marshal(v interface{}) ([]byte, error) {
e := newEncodeState()
err := e.marshal(v, encOpts{escapeHTML: true})
if err != nil {
return nil, err
}
buf := append([]byte(nil), e.Bytes()...)
encodeStatePool.Put(e)
return buf, nil
}
其中 e.marshal
通过 escapeHTML: true
指定了 encode 时需要做 HTMLEscape 处理,即对 HTML 特殊字符进行转义。
该方法中通过判断需要序列化的对象类型,来使用对应的 encoderFunc 来做序列化。
这里我们序列化的对象是结构体,因此会调用 structEncoder
的 encode 方法来处理:
可以看到 encode 方法除了会对结构体字段的值做 escape 处理外,对结构体的 json tag 名也会做 escape 处理,实验一下:
package main
import (
"encoding/json"
"fmt"
)
type Data struct {
Link string `json:"Li&nk"`
}
func main() {
link := `https://mbti.axiaoxin.com/?lang=ko&from=article`
data := Data{
Link: link,
}
b, _ := json.Marshal(data)
fmt.Println("data json:", string(b))
fmt.Println("---")
}
运行输出:
data json: {"Li\u0026nk":"https://mbti.axiaoxin.com/?lang=ko\u0026from=article"}
---
对于结构体的每个字段,structEncoder 都会将其转换为 field
对象:
// A field represents a single field found in a struct.
type field struct {
name string
nameBytes []byte // []byte(name)
equalFold func(s, t []byte) bool // bytes.EqualFold or equivalent
nameNonEsc string // `"` + name + `":`
nameEscHTML string // `"` + HTMLEscape(name) + `":`
tag bool
index []int
typ reflect.Type
omitEmpty bool
quoted bool
encoder encoderFunc
}
结构体字段值通过自身类型的 encoderFunc
进行处理,这里我们是 string 类型,因此将调用 stringEncoder
对值进行处理:
stringEncoder 中 调用 string 方法对字符串中 RuneSelf
以下的字符(ASCII)进行 escape 处理,其中就对 &
进行了替换:
htmlSafeSet
是一个 html 特殊符号是否安全的 map,&
符号返回 false, 因此进行了 hex 的运算被替换。
解决方案
解决方法 golang 在源码中已说明白:
// String values encode as JSON strings coerced to valid UTF-8,
// replacing invalid bytes with the Unicode replacement rune.
// So that the JSON will be safe to embed inside HTML <script> tags,
// the string is encoded using HTMLEscape,
// which replaces "<", ">", "&", U+2028, and U+2029 are escaped
// to "\u003c","\u003e", "\u0026", "\u2028", and "\u2029".
// This replacement can be disabled when using an Encoder,
// by calling SetEscapeHTML(false).
就是说,可以通过 json.NewEncoder
创建一个新的 encoder,再对该 encoder 设置 SetEscapeHTML(false)
封装除自定义的 encoder。
// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w: w, escapeHTML: true}
}
可见 NewEncoder 默认也是设置 escapeHTML: true
,但是它提供了 SetEscapeHTML 方法来修改这个设置,然后调用他的 Encode
方法进行序列化。
这里需要注意 NewEncoder 的 Encode 方法会在 JSON 序列化结果后添加一个 \n
:
代码实现:
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type Data struct {
Link string `json:"Li&nk"`
}
func Encoding(t interface{}) ([]byte, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(t)
return buffer.Bytes(), err
}
func main() {
link := `https://mbti.axiaoxin.com/?lang=ko&from=article`
data := Data{
Link: link,
}
b, _ := json.Marshal(data)
fmt.Println("data json:", string(b))
fmt.Println("---")
b, _ = Encoding(data)
fmt.Println("data json:", string(b))
fmt.Println("---")
}
执行结果:
data json: {"Li\u0026nk":"https://mbti.axiaoxin.com/?lang=ko\u0026from=article"}
---
data json: {"Li&nk":"https://mbti.axiaoxin.com/?lang=ko&from=article"}
---
注意,第二个打印多了一个空行。
echo 框架中自定义 JSON 序列化方法
在 web 框架中,比如 echo,其 New 方法实现:
其中的 JSONSerializer 没有设置 escape 为 false,因此使用 echo 做开发,返回的 json 都会出现这种问题:
复制 JSONSerializer 的代码,改为自己的 MyJSONSerializer ,在 Serialize
方法中添加 enc.SetEscapeHTML(false)
来实现不 escape。
在调用 echo 的 New 方法后,再把他的JSONSerializer用我们新的 JSONSerializer 覆盖即可。
代码示例:
e := echo.New()
e.JSONSerializer = &MyJSONSerializer{}