在Golang开发中,与服务端或客户端交换数据时几乎必然用到JSON序列化与反序列化。标准库encoding/json提供了Unmarshal与Marshal两套基础API,但在真实业务里,外部传入的报文可能残缺、类型错乱甚至编码异常。如果不加区分地忽略error返回值,轻则结构体字段为零值,重则导致后续逻辑 panic。理解json包在解析失败时的错误构造方式,是写出健壮接口的前提。

标准库的错误类型与底层原理
encoding/json在解析出错时并不会抛出类似其他语言的异常,而是返回一个实现了error接口的普通值。底层解析器在扫描输入字节时,如果遇到花括号不匹配、字符串未闭合等情况,会构造一个json.SyntaxError结构,其中包含错误的偏移量Offset,方便调用方定位原始报文中的问题位置。这类错误属于不可恢复的输入格式错误,通常意味着调用者传来的根本不是合法JSON。
当字节流语法正确但字段类型与目标结构体不吻合时,例如JSON里是字符串而Go结构体字段是int,解析器会生成json.UnmarshalTypeError。该错误携带Value(JSON中的实际类型)、Type(Go期望的类型)以及Field(发生错误的字段路径)。与SyntaxError不同,这种类型错误有时可以通过自定义解码逻辑或指针容错来局部处理,而不是直接拒绝整个请求。
除了上述两种,还会遇到json.InvalidUnmarshalError,它一般在传入非指针接收者时出现,比如把结构体值而非指针传给Unmarshal。这属于开发期代码错误,应当在测试阶段暴露并修正,而不是在线上靠捕获来绕过。
基础捕获方式与代码示例
最常规的捕获方式是在调用Unmarshal之后立即判断err。如果err不为nil,可先用errors.As尝试匹配具体子类型,从而进行差异化日志与响应。下面示例展示如何区分语法错误与类型错误:
package main
import (
"encoding/json"
"errors"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func parseUser(data []byte) {
var u User
err := json.Unmarshal(data, &u)
if err != nil {
var syntaxErr *json.SyntaxError
if errors.As(err, &syntaxErr) {
fmt.Printf("JSON语法错误 偏移量:%dn", syntaxErr.Offset)
return
}
var typeErr *json.UnmarshalTypeError
if errors.As(err, &typeErr) {
fmt.Printf("字段%s类型错误 期望:%v 实际:%vn", typeErr.Field, typeErr.Type, typeErr.Value)
return
}
fmt.Println("其他解析错误:", err)
return
}
fmt.Printf("解析成功:%+vn", u)
}
func main() {
parseUser([]byte(`{"id":"abc","name":"tom"}`))
}
上述代码在id字段传入字符串时会触发UnmarshalTypeError,通过Field可以知道是id出了问题。如果传入{"id":1,这种残缺报文,则会命中SyntaxError并打印偏移量。这种细分处理让接口能向调用方返回更友好的错误信息,而不是统一报“解析失败”。
需要注意的是,Go 1.13之后推荐使用errors.As而非类型断言,因为错误可能被包装(wrap)多层。如果项目里用了自定义中间件把err用fmt.Errorf加上上下文,直接类型断言会失败,而errors.As能穿透包装层找到底层的json错误类型。
进阶容错与高精度场景处理
在金融或物联网上报场景中,JSON里的大整数若用默认float64接收会丢失精度。标准库提供json.Decoder并设置UseNumber为true,可将数字解析为json.Number字符串类型,由业务侧自行转换。这样既能捕获转换异常,也能避免uint64溢出被静默截断。
package main
import (
"bytes"
"encoding/json"
"fmt"
)
func safeParse(data string) {
dec := json.NewDecoder(bytes.NewReader([]byte(data)))
dec.UseNumber()
var raw map[string]interface{}
if err := dec.Decode(&raw); err != nil {
fmt.Println("解码失败:", err)
return
}
id, err := raw["id"].(json.Number).Int64()
if err != nil {
fmt.Println("id转int64异常:", err)
return
}
fmt.Println("id=", id)
}
func main() {
safeParse(`{"id":9223372036854775807}`)
}
上例用Decoder替代Unmarshal,在解码阶段不丢失数字精度,后续转换Int64时若超出范围或原值非整数,会返回明确错误,此时捕获并处理即可。对于数组嵌套或动态字段,还可以结合json.RawMessage延迟解析,先捕获整体结构异常,再对子片段单独Unmarshal,做到局部失败不影响主体。
另一个常见误区是依赖recover来捕获JSON解析“异常”。由于encoding/json不主动panic,仅靠recover无法拿到错误详情,反而掩盖了真实故障。正确思路始终是检查error,并在必要处用Decoder的DisallowUnknownFields选项拒绝多余字段,从源头减少因字段歧义导致的隐性错误。