这是一份关于Go的编码/json包的清单,多年来,当我第一次遇到这些东西时,不是感到困惑就是感到惊讶。
如果你仔细阅读官方软件包的文档,其中许多事情都有提及,所以理论上它们不应该是一个惊喜。但是有一些在文档中根本没有提到--或者至少没有明确指出--而且值得注意的是
地图条目是按字母顺序排列的
当把Go地图编码为JSON时,条目将根据地图键按字母顺序排序。例如,下面的地图:
m := map[string]int{
"z": 123,
"0": 123,
"a": 123,
"_": 123,
}
将被编码到JSON中:
{"0":123,"_":123,"a":123,"z":123}
字节片被编码为base-64字符串
任何[]byte 片段在编码到JSON时将被转换为base64编码的字符串。base64字符串使用填充和标准编码字符,如RFC 4648中定义的。例如,下面的地图:
m := map[string][]byte{
"foo": []byte("bar baz"),
}
将被编码到JSON中:
{"foo":"YmFyIGJheg=="}
无片和空片的编码方式不同
Go中的无片将被编码为null JSON值。相反,空(但不是nil)片将被编码为一个空的JSON数组。比如说:
var nilSlice []string
emptySlice := []string{}
m := map[string][]string{
"nilSlice": nilSlice,
"emptySlice": emptySlice,
}
将被编码为JSON:
{"emptySlice":[],"nilSlice":null}
整数,time.Time和net.IP值可以作为映射键使用
可以对一个有整数值作为地图键的地图进行编码。这些整数将在生成的JSON中自动转换为字符串(因为JSON对象中的键必须总是字符串)。比如说:
m := map[int]string{
123: "foo",
456_000: "bar",
}
将被编码到JSON中:
{"123":"foo","456000":"bar"}
此外,Go允许你用实现了encoding.TextMarshaler接口的键对地图进行编码。这意味着你也可以使用time.Time 和net.IP 值作为地图的键,开箱即用。比如说:
t1 := time.Now()
t2 := t1.Add(24 * time.Hour)
m := map[time.Time]string{
t1: "foo",
t2: "bar",
}
将被编码为JSON:
{"2009-11-10T23:00:00Z":"foo","2009-11-11T23:00:00Z":"bar"}
请注意,试图用任何其他类型的键对地图进行编码都会导致一个 json.UnsupportedTypeError错误。
字符串中的角括号和安培数被转义了
如果一个字符串包含角括号<> ,这些角括号将被转义为JSON输出中的\u003c 和\u003e 。同样,& 字符也将被转义为\u0026 。这是为了防止一些网络浏览器意外地将JSON解释为HTML。比如说:
s := []string{
"<foo>",
"bar & baz",
}
将被编码到JSON中:
["\u003cfoo\u003e","bar \u0026 baz"]
如果你需要防止这些字符被转义,你应该使用一个json.Encoder 实例并调用 SetEscapeHTML(false).这里有一个例子。
浮点数的尾部零点被去除
当对一个小数部分以零结尾的浮点数进行编码时,任何尾部的零都不会出现在JSON中。比如说:
s := []float64{
123.0,
456.100,
789.990,
}
将会被编码到JSON中:
[123,456.1,789.99]
在零值结构上使用省略号是不可行的
omitempty 指令从不认为结构类型是空的--即使所有的结构字段都有其零值,并且你也在这些字段上使用了omitempty 。在编码后的JSON中,它将始终作为一个对象出现。比如说:
s := struct {
Foo struct {
Bar string `json:",omitempty"`
} `json:",omitempty"`
}{}
将会被编码到JSON中:
{"Foo":{}}
有一个长期的建议,讨论改变这种行为,但Go 1的兼容性承诺意味着它不可能很快发生。相反,你可以通过使字段成为指向结构体的指针来解决这个问题,这是因为omitempty 认为nil 指针是空的。比如说:
s := struct {
Foo *struct {
Bar string `json:",omitempty"`
} `json:",omitempty"`
}{}
在一个零值的time.Time上使用省略号是行不通的
在一个零值的time.Time 字段上使用omitempty ,不会在编码后的JSON中隐藏它。这是因为time.Time 类型在幕后是一个结构,如上所述,omitempty 从来不认为一个结构类型是空的。相反,字符串"0001-01-01T00:00:00Z" 将出现在JSON中(这是通过对零值的 方法调用返回的值)。 MarshalJSON()方法返回的值time.Time 。比如说:
s := struct {
Foo time.Time `json:",omitempty"`
}{}
将被编码到JSON中:
{"Foo":"0001-01-01T00:00:00Z"}
有一个'string'结构标签
Go提供了一个string 结构标签指令,该指令可以强制将单个字段中的数据在生成的JSON中编码为字符串。例如,如果你想强制将一个整数表示为字符串而不是JSON数字,你可以这样使用string 指令:
s := struct {
Foo int `json:",string"`
}{
Foo: 123,
}
而这将被编码到JSON中:
{"Foo":"123"}
请注意,string 结构标签指令只对包含浮点、整数或bool 类型的字段起作用。对于任何其他类型的字段,它将没有任何作用。
结构标签中不支持非ASCII标点符号
当使用结构标签来改变JSON中的键名时,任何包含非ASCII标点符号的标签将被忽略。值得注意的是,这意味着你不能在结构标签中使用en或em破折号,或大多数货币符号。比如说:
s := struct {
CostUSD string `json:"cost $"` // OK
CostEUR string `json:"cost €"` // Contains the non-ASCII punctuation character €. Will be ignored.
}{
CostUSD: "100.00",
CostEUR: "100.00",
}
将被编码为以下JSON(注意结构标签重命名的CostEUR 字段被忽略了):
{"cost $":"100.00","CostEUR":"100.00"}
同样,在将JSON对象的值解码成结构时,任何包含非ASCII标点符号的结构标签都将被忽略,结构字段将保留其零值。例如,下面的代码:
js := []byte(`{"cost $":"100.00","cost €":"100.00"}`)
s := struct {
CostUSD string `json:"cost $"`
CostEUR string `json:"cost €"`
}{}
err := json.Unmarshal(js, &s)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v", s)
将打印出来:
{CostUSD:100.00 CostEUR:}
当你需要对JSON对象进行解码时,如果该对象的键包含非ASCII字符,而你又不能改变JSON,这种情况就会很烦人。为了解决这个限制,你可以把解码到map作为一个中间步骤,然后把数据从map复制到结构中。例如,下面的代码。
js := []byte(`{"cost $":"100.00","cost €":"100.00"}`)
var aux map[string]string
err := json.Unmarshal([]byte(js), &aux)
if err != nil {
log.Fatal(err)
}
s := struct {
CostUSD string `json:"cost $"`
CostEUR string `json:"cost €"`
}{
CostUSD: aux["cost $"],
CostEUR: aux["cost €"],
}
fmt.Printf("%+v", s)
将打印出来:
{CostUSD:100.00 CostEUR:100.00}
将一个JSON数字解码为一个接口{},得到一个float64
当将JSON数字解码成interface{} ,该值将具有底层类型float64 ,即使它在原始JSON中是一个整数。
如果你想获得一个整数的值(而不是一个float64 ),最稳健的方法是使用一个json.Decoder 实例对JSON进行解码,并对其设置了 UseNumber()方法对其进行解码。这将把所有的JSON数字解码为底层类型json.Number ,而不是float64 ,然后你可以使用它的Int64() 方法把数字作为一个整数访问。比如说:
js := `{"foo": 123, "bar": true}`
var m map[string]interface{}
dec := json.NewDecoder(strings.NewReader(js))
dec.UseNumber()
err := dec.Decode(&m)
if err != nil {
log.Fatal(err)
}
i, err := m["foo"].(json.Number).Int64()
if err != nil {
log.Fatal(err)
}
fmt.Printf("foo: %d", i)
会打印:
foo: 123
不要使用More()来检查流中是否有剩余的JSON对象
当用json.Decoder 处理一个JSON对象流时,不要使用More() 方法来检查流中是否有剩余的对象。尽管它的名字叫More() ,但它并不是为这个目的而设计的†,而且试图以这种方式使用它可能会导致一些微妙的问题。
† More() 方法旨在与Token() 结合使用,专门用于检查当前正在解析的数组或对象中是否有其他元素。
例如,如果你在解码一个无效的JSON流时使用它,如{"name": "alice"}{"name": "bob"}] (注意结尾处的额外方括号),它不会导致错误(当它应该!)。像这样:
js := `{"name": "alice"}{"name": "bob"}]`
dec := json.NewDecoder(strings.NewReader(js))
for {
var user map[string]string
err := dec.Decode(&user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v\n", user)
// Don't do this!
if !dec.More() {
break
}
}
这段代码将无错误地运行并输出:
map[name:alice]
map[name:bob]
看一个流是否包含另一个JSON对象的正确技术是检查io.EOF 错误,当流中没有更多的对象可以处理时,就会返回这个错误。像这样:
js := `{"name": "alice"}{"name": "bob"}]`
dec := json.NewDecoder(strings.NewReader(js))
for {
var user map[string]string
err := dec.Decode(&user)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
log.Fatal(err)
}
fmt.Printf("%v\n", user)
}
运行这个将正确地导致一个错误,正如我们所期望的那样,鉴于无效的输入:
map[name:alice]
map[name:bob]
2009/11/10 23:00:00 invalid character ']' looking for beginning of value
由自定义MarshalJSON()方法返回的字符串值必须加引号
如果你正在创建一个返回字符串值的自定义MarshalJSON() 方法,你必须在返回之前用双引号包裹字符串,否则它不会被解释为一个JSON字符串,并会导致运行时错误。比如说:
type Age int
func (age Age) MarshalJSON() ([]byte, error) {
encodedAge := fmt.Sprintf("%d years", age)
encodedAge = strconv.Quote(encodedAge) // Wrap the string in quotes before returning.
return []byte(encodedAge), nil
}
func main() {
users := map[string]Age{
"alice": 21,
"bob": 84,
}
js, err := json.Marshal(users)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", js)
}
将导致以下JSON被打印出来:
{"alice":"21 years","bob":"84 years"}
如果在上面的代码中,你没有引用MarshalJSON() 的返回值,你会得到这个错误:
2009/11/10 23:00:00 json: error calling MarshalJSON for type main.Age: invalid character 'y' after top-level value