常见原因是请求体未被完整读取或 Content-Type 不是 application/json,且 http.Request.Body 被重复读取导致为空;应检查 Content-Type、避免多次读取,必要时用 io.NopCloser 重置。

接收并解析 JSON 请求体时,json.Unmarshal 为什么总返回 invalid character
常见原因是请求体未被完整读取,或 Content-Type 不是 application/json,但服务端仍尝试解析。Go 的 http.Request.Body 是单次读取流,若之前被其他中间件(如日志、鉴权)调用过 io.ReadAll 或 r.ParseForm,后续再读就会得到空字节切片,导致 json.Unmarshal 解析空字符串而报错。
- 始终在解析前检查
r.Header.Get("Content-Type")是否包含application/json - 避免重复读取
r.Body;如需多次使用,用io.NopCloser(bytes.NewBuffer(buf))重置 - 用
io.LimitReader(r.Body, 1 限制最大读取 1MB,防止恶意大 payload
结构体字段无法反序列化?注意 json tag 和导出规则
Go 中只有首字母大写的导出字段才能被 json 包访问。若字段名是 userID 但没加 tag,JSON 中的 user_id 就无法绑定——默认映射是严格按 Go 字段名转蛇形,不会自动猜。
- 显式声明
jsontag:UserID int `json:"user_id"` - 忽略空字段用
omitempty:Name string `json:"name,omitempty"` - 嵌套结构体字段也需导出,否则整个嵌套对象为
null - 数字类型建议统一用
int64或float64,避免 JSON 数字解析成float64后再转int丢精度
处理不确定结构的 JSON:用 map[string]interface{} 还是 json.RawMessage
map[string]interface{} 看似灵活,但会把所有数字转成 float64,嵌套深时类型断言极易 panic;而 json.RawMessage 是延迟解析的原始字节切片,适合只提取部分字段或透传。
- 仅需读取一两个字段?定义最小结构体,其余字段用
json.RawMessage占位 - 要校验但不立刻解析?先用
json.RawMessage接收,再对特定字段调用json.Unmarshal - 避免无脑
map[string]interface{},尤其涉及数字计算或数据库写入时
type WebhookPayload struct {
Event string `json:"event"`
Data json.RawMessage `json:"data"`
}
// 后续按 event 类型分发解析
if payload.Event == "order_created" {
var order Order
if err := json.Unmarshal(payload.Data, &order); err != nil {
// handle
}
}
用 Decoder 替代 Unmarshal 提升大请求体性能
当请求体较大(如 >100KB),json.Unmarshal([]byte, ...) 需先将整个 body 读进内存再解析;而 json.NewDecoder(r.Body) 是流式解析,边读边解,内存占用低且支持超时控制。
立即学习“go语言免费学习笔记(深入)”;
- 直接传
r.Body给json.NewDecoder,无需io.ReadAll - 可设置
Decoder.DisallowUnknownFields()拒绝未知字段,增强 API 健壮性 - 注意:
Decoder不能复用,每次请求都应新建一个实例
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
var req CreateUserRequest
if err := decoder.Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
字段名大小写、body 读取时机、结构体 tag、解析方式选择——这些不是“语法细节”,而是 JSON 接口稳定性的实际防线。漏掉任意一项,都可能让错误在生产环境里静默出现。










