28_JSON与序列化
用 encoding/json 编解码结构体,区分无效 JSON、缺失字段与错误类型。
Go 基础专题 28:JSON 编解码与字段校验
学习目标:用
encoding/json编解码结构体,区分无效 JSON、缺失字段与错误类型。
核心知识
JSON 是跨语言交换数据的文本格式。Go 的 encoding/json 可把结构体编码成 JSON,也可把 JSON 解码到结构体。只有导出字段,即首字母大写的字段,才会被标准库正常访问;json:"title" 标签指定 JSON 中的键名。字段未出现时通常保留 Go 零值,所以业务上需要区分“未提供”和“提供了零值”。
package main
import (
"encoding/json"
"fmt"
)
type Note struct {
ID int64 `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
func main() {
source := Note{ID: 1, Title: "学习 Go"}
data, err := json.Marshal(source)
if err != nil {
fmt.Println("编码失败:", err)
return
}
var decoded Note
if err := json.Unmarshal(data, &decoded); err != nil {
fmt.Println("解码失败:", err)
return
}
fmt.Println(string(data), decoded.Title)
}
json.Marshal 返回 []byte 和 error,json.Unmarshal 需要目标变量的指针。把 &decoded 误写成 decoded 会失败。真实服务应向上返回错误或在入口转换成稳定提示。若输入中 title 缺失,解码通常不会报错,而得到空字符串;必须另行校验。
校验输入和控制未知字段
简单 json.Unmarshal 默认忽略结构体中没有的 JSON 字段。对需要严格输入契约的 API,可用 json.Decoder 的 DisallowUnknownFields();解码后还要检查是否有第二个 JSON 值,避免接受一个合法对象后面跟额外数据。校验应覆盖空标题、长度、数字范围和必填字段。
package main
import (
"encoding/json"
"fmt"
"strings"
)
type CreateNote struct {
Title string `json:"title"`
}
func parse(raw string) (CreateNote, error) {
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.DisallowUnknownFields()
var request CreateNote
if err := decoder.Decode(&request); err != nil {
return CreateNote{}, fmt.Errorf("解析 JSON: %w", err)
}
if strings.TrimSpace(request.Title) == "" {
return CreateNote{}, fmt.Errorf("title 不能为空")
}
return request, nil
}
func main() {
request, err := parse(`{"title":"学习 Go"}`)
fmt.Println(request.Title, err)
}
这个示例完成了未知字段限制和标题检查,但尚未检查第二个 JSON 值。正式 HTTP 服务可在第一次 Decode 后再解码到临时变量,要求结果是 io.EOF。HTTP 请求体还需在解码前限制最大字节数,防止大输入耗尽资源。
逐步理解
null、缺失字段和具体零值在结构体解码后可能难区分。若必须判断字段是否出现,可使用指针字段、json.RawMessage 或专门的输入 DTO,并在校验时明确允许的状态。不要直接把数据库实体作为外部请求结构体,避免暴露或接受本不该由客户端控制的字段。输出 JSON 也要检查错误,特别是待编码值含不支持的类型时。
动手练习
练习:分别调用 parse 处理合法标题、空标题、未知字段和格式错误的 JSON。答案要点:仅合法输入返回无错误;其余输入根据解析阶段或业务校验阶段返回错误。再尝试输入两个相连的 JSON 对象,补上第二次解码检查。