在实际开发中,Golang 常用于构建高性能 API 接口。在返回 JSON 结果时,特别是涉及包含特殊字符的 URL 字段,开发者可能会遇到字符被自动转义的情况。常见的是 &
符号被转义为 \u0026
,导致客户端无法正常解析该链接的参数,进而影响请求的正确性。
复现问题
在 API 实现中返回一个 JSON 结果,其中有一个字段为 URL 链接,客户端拿到该链接后做请求,URL 链接中存在多个使用 &
连接的 querystring 参数。
服务端实现时,通过构造结构体后返回对应的 JSON 序列化结果。
但是请求接口时发现 URL 链接中的 &
符号被自动转义为 \u0026
,导致历史版本的客户端无法解析 URL 中的参数。
一段代码模拟该场景:
package main
import (
"encoding/json"
"fmt"
)
type Data struct {
Link string
}
func main() {
link := `https://blog.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://blog.axiaoxin.com/?lang=ko\u0026from=article"}
---
可以看到,URL 中的 &
符号被替换为了 \u0026
,这正是由于 Go 在 JSON 序列化过程中对 HTML 特殊字符进行转义所致。
转义原因分析:为什么 &
符号会替换为 \u0026
Golang 在使用 json.Marshal
序列化时,默认会对一些 HTML 特殊字符(如 &
, <
, >
等)进行转义。这种行为是为了确保 JSON 能够安全地嵌入 HTML 页面,防止潜在的 XSS 攻击。在 json.Marshal
的源码中,可以看到默认开启了 escapeHTML: true
参数:
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
}
escapeHTML 参数的设置直接影响到字符的处理方式,比如 &
会被转义为 \u0026
,影响 URL 的传递。
其中 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://blog.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://blog.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 的运算被替换。
解决方案
针对这一问题,最简单的方法是使用 json.NewEncoder
,并通过 SetEscapeHTML(false)
来取消默认的 HTML 特殊字符转义。
解决方法 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://blog.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("---")
}
运行后,结果将正确保留 URL 中的 &
符号:
data json: {"Li\u0026nk":"https://blog.axiaoxin.com/?lang=ko\u0026from=article"}
---
data json: {"Li&nk":"https://blog.axiaoxin.com/?lang=ko&from=article"}
---
注意,第二个打印多了一个空行。
通过这种方法,能够避免 &
被错误转义成 \u0026
,确保客户端可以正确解析 URL 参数。
在 Golang Web 框架 echo 中的应用:自定义 JSON 序列化方法
很多开发者在使用 Go 进行 Web 开发时,常常使用像 Echo 这样的框架。在 Echo 中,JSON 序列化是默认启用了 escapeHTML: true 的,因此开发者需要手动修改。比如 echo,其 New 方法实现:
其中的 JSONSerializer 没有设置 escape 为 false,因此使用 echo 做开发,返回的 json 都会出现这种问题:
可以通过自定义 JSON 序列化方法来解决该问题。在 Echo 框架中实现自定义 JSON 序列化器,重写其中的 Serialize 方法,并添加 SetEscapeHTML(false)
,可以有效避免 HTML 特殊字符的转义问题。
复制 JSONSerializer 的代码,改为自己的 MyJSONSerializer ,在 Serialize
方法中添加 enc.SetEscapeHTML(false)
来实现不 escape。
在调用 echo 的 New 方法后,再把他的 JSONSerializer 用我们新的 JSONSerializer 覆盖即可。
代码示例:
e := echo.New()
e.JSONSerializer = &MyJSONSerializer{}
FAQ
Q: 为什么 JSON 序列化时会自动转义 &
?
A: Go 默认会对一些 HTML 特殊字符进行转义,以防止潜在的 XSS 攻击,& 被转义为 \u0026 是这种行为的表现。
Q: 如何关闭 Golang 的 JSON 序列化字符转义? A: 可以使用 json.NewEncoder 并调用 SetEscapeHTML(false) 方法来取消默认的转义行为。
Q: Echo 框架中如何自定义 JSON 序列化方式? A: 在 Echo 框架中,可以通过自定义 JSONSerializer 并覆盖 SetEscapeHTML(false) 来解决。