Golang 与 PHP 的 json 序列化问题

Intro

最近在做 Golang 与 PHP 的 RPC 实现。因 PHP 业务端已上线稳定,Golang 方面则需要完全兼容。其中使用了 json 序列化,发现区别还是很大的,见下面代码。

1
2
3
$ php -a
php > echo json_encode("<test我爱中国>");
"<test\u6211\u7231\u4e2d\u56fd>"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"encoding/json"
"fmt"
)

func main() {

st := "<test我爱中国>"
res, err := json.Marshal(st)
if err != nil {
fmt.Println("json err:", err.Error())
}
fmt.Printf("json is: %s", string(res)) // json is: "\u003ctest我爱中国\u003e"
}

PHP 默认的 json_encode() 函数会把多字节字符转成 \uXXXX 当然通过设置 JSON_UNESCAPED_UNICODE 可以解决这个问题。这里不动 PHP 代码。
Golang 这里用 json 包的 Marshal 方法实现序列化,对多字节字符是不进行处理的。但是这个方法出于安全考虑会将”<”, “>”, “&”这三个字符转成 \uXXXX 形式。这还不是最魔幻的,这个方法没有可选参数进行设置。

取消转义特殊字符

看官网包说明通过 json.NewEncoder() 可以关闭转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"encoding/json"
"fmt"
"bytes"
)

func main() {

st := "<test我爱中国>"
jsonBuf := bytes.NewBuffer([]byte{})
jsonEncode := json.NewEncoder(jsonBuf)
jsonEncode.SetEscapeHTML(false)
if err := jsonEncode.Encode(st); err != nil {
fmt.Println("json err:", err.Error())
}
fmt.Printf("json is: %s", jsonBuf.String()) // json is: "<test我爱中国>"
}

自定义序列化方法

对多字节字符转义,只能通过自定义序列化方法,官网也有包说明非常人性化。

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
package main

import (
"encoding/json"
"fmt"
"bytes"
"strconv"
)

type cusString string

func (cs cusString) MarshalJSON() ([]byte, error) {
return []byte(strconv.QuoteToASCII(string(cs))), nil
}

func main() {

st := cusString("<test我爱中国>")
jsonBuf := bytes.NewBuffer([]byte{})
jsonEncode := json.NewEncoder(jsonBuf)
jsonEncode.SetEscapeHTML(false)
if err := jsonEncode.Encode(st); err != nil {
fmt.Println("json err:", err.Error())
}
fmt.Printf("json is: %s", jsonBuf.String()) // json is: "<test\u6211\u7231\u4e2d\u56fd>"
}

这样 json 序列化后的结果就和 PHP 下的一样了。分别计算下 MD5 。

1
2
3
4
5
6
7
8
9
$ php -a
Interactive shell

php > echo json_encode("<test我爱中国>");
"<test\u6211\u7231\u4e2d\u56fd>"
php > echo md5(json_encode("<test我爱中国>"));
5ec1bfc0a2db38f985cdae47b2012ca5
php >
php >
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
package main

import (
"encoding/json"
"fmt"
"bytes"
"strconv"
"crypto/md5"
)

type cusString string

func (cs cusString) MarshalJSON() ([]byte, error) {
return []byte(strconv.QuoteToASCII(string(cs))), nil
}

func main() {

st := cusString("<test我爱中国>")
jsonBuf := bytes.NewBuffer([]byte{})
jsonEncode := json.NewEncoder(jsonBuf)
jsonEncode.SetEscapeHTML(false)
if err := jsonEncode.Encode(st); err != nil {
fmt.Println("json err:", err.Error())
}
fmt.Printf("json is: %s", jsonBuf.String()) // json is: "<test\u6211\u7231\u4e2d\u56fd>"
fmt.Printf("md5 is: %x", md5.Sum(jsonBuf.Bytes())) // md5 is: 5543e9185c4bde6311dc9c7605ca92b8
}

MD5 值不一样,这就很魔幻了。开始大胆假设,小心求证。

特殊处理

MD5 算法有问题,太扯了这是不可能的,试了别的 hash 算法也是一样。
json 序列化有问题,转 string 后确实没看出问题,直接输出 bytes 格式。发现是这样的。

1
34 60 116 101 115 116 92 117 54 50 49 49 92 117 55 50 51 49 92 117 52 101 50 100 92 117 53 54 102 100 62 34 10

最后面是个10,10在 Ascii 表里是 LR new line 换行符。看来这个 json 序列化果然有问题啊,看看序列化源码怎么写的:

源码在这里 json stream

至于为什么会添加一个换行符,注释是这么说的:

1
2
3
4
5
Terminate each value with a newline.
This makes the output look a little nicer
when debugging, and some kind of space
is required if the encoded value was a number,
so that the reader knows there aren't more

说白了 json.NewEncoder() 处理的是流式数据,多个数据间为了分隔加了 \n

最终代码如下,Go Playground

如果你看到这里说明你可能遇到了相似的问题,如果没有解决你的问题,从文档或源码中找找答案吧。

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
36
37
package main

import (
"encoding/json"
"fmt"
"bytes"
"strconv"
"crypto/md5"
)

type cusString string

func (cs cusString) MarshalJSON() ([]byte, error) {
return []byte(strconv.QuoteToASCII(string(cs))), nil
}

func main() {

st := cusString("<test我爱中国>")
jsonBuf := bytes.NewBuffer([]byte{})
jsonEncode := json.NewEncoder(jsonBuf)
jsonEncode.SetEscapeHTML(false)
if err := jsonEncode.Encode(st); err != nil {
fmt.Println("json err:", err.Error())
}

jsonBytes := jsonBuf.Bytes()
resultJson := make([]byte, 0)
if jsonBytes[len(jsonBytes)-1] == '\n' {
resultJson = jsonBytes[:len(jsonBytes)-1]
} else {
resultJson = jsonBytes
}

fmt.Printf("json is: %s \r\n", string(resultJson)) // json is: "<test\u6211\u7231\u4e2d\u56fd>"
fmt.Printf("md5 is: %x \r\n", md5.Sum(resultJson)) // md5 is: 5ec1bfc0a2db38f985cdae47b2012ca5
}