不能用 req.parseform() 解析 json 请求体,因为它专为 application/x-www-form-urlencoded 和 multipart/form-data 设计;对 json 请求会将整个 {"name":"alice"} 当作键存入 req.form,值为空切片,导致需手动遍历键再反序列化,低效且易错。

Go 标准库的 encoding/json 足够好,但“能跑”不等于“没坑”——多数线上问题出在 Body 重复读取、Content-Type 漏设、状态码写晚了,或结构体字段不可导出。
解析 JSON 请求体时为什么不能用 req.ParseForm()
它专为 application/x-www-form-urlencoded 和 multipart/form-data 设计。对 JSON 请求调用 req.ParseForm(),会把整个 {"name":"alice"} 当作一个表单键塞进 req.Form,值为空切片。你得手动遍历键、再反序列化,既低效又易错。
- 永远检查
r.Header.Get("Content-Type")是否包含"application/json",否则直接返回http.StatusUnsupportedMediaType - 用
json.NewDecoder(r.Body).Decode(&v)直接流式解码,不缓存全部 body 到内存 -
r.Body是单次读取流:一旦被Decode消费完,再次读就是io.EOF;别在中间加io.ReadAll或req.ParseForm() - 结构体字段必须首字母大写(可导出),且带
json:"field_name"标签,否则解码后字段始终为空
json.NewDecoder 和 json.Unmarshal 选哪个
优先用 json.NewDecoder,尤其在 HTTP handler 中。它接受 io.Reader,天然适配 r.Body,还能配合 http.MaxBytesReader 做 payload 限制。
-
json.Unmarshal([]byte, &v)需要先读完整个 body(如io.ReadAll(r.Body)),对大请求内存压力大 -
json.NewDecoder支持流式解析,边读边解,失败时也只消耗已读部分 - 若需复用 body(比如日志记录 + 解码),必须用
io.TeeReader或提前bytes.Buffer缓存,但会牺牲流式优势
返回 JSON 响应时为什么不能用 fmt.Fprintf(w, "%s", b)
它会把字节切片 b 当作字符串打印,输出类似 [123 34 110 97 109 101 34 ...] 的字节数组表示,客户端收到的是非法 JSON。
- 必须调用
w.Header().Set("Content-Type", "application/json; charset=utf-8"),缺一不可 - 非 200 响应(如 400/500)必须在
json.NewEncoder(w).Encode()之前调用w.WriteHeader(statusCode),否则默认发 200 - 用
json.NewEncoder(w).Encode(v),它自动处理转义、UTF-8 编码和末尾换行,且写入失败可捕获 error - 避免手拼 JSON 字符串或用
w.Write(b)—— 虽然能用,但绕过了Encoder的健壮性校验(如 nil map slice 处理)
结构体初始化常见语法错误
报错 too many arguments to conversion 通常是因为把结构体字面量写成了函数调用形式,比如 User("alice", "a@b.c"),而正确写法是 User{Name: "alice", Email: "a@b.c"}。
-
User{}是结构体字面量初始化,字段名可选,支持标签映射 -
User()是类型转换或函数调用语法,在结构体上非法 - 嵌套结构体字段若为指针(如
*time.Time),注意判空再解引用,否则 panic - 可选字段用
omitempty标签(如Age int `json:"age,omitempty"`),但要注意零值(0、""、nil)也会被忽略
最常被跳过的一步是:没做 Content-Type 检查就直接解码,结果非 JSON 请求进来就 panic;或者把 w.WriteHeader(400) 写在 Encode() 之后,导致客户端收到 200 + 错误 JSON。这两个点,线上服务一碰就倒。











