G06第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

JSON 与输入校验

JSON 与输入校验 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这课守住服务的入口:把请求体解码成结构体,逐字段校验必需、长度和格式,限制请求体大小,让所有无效输入都得到一致形状的错误响应。

问题场景

G05 的服务只读不写,一加 POST /bookmarks,麻烦全来了:body 不是 JSON、title 是空串、url 缺协议、字段名拼错、body 塞了 2GB。只读接口没有输入所以无所谓,写入接口的每一行都要先过安检。

核心解释

解码和校验收成一个纯函数,handler 只管调用:

type bookmarkInput struct {
	Title string   `json:"title"`
	URL   string   `json:"url"`
	Tags  []string `json:"tags"`
}

func parseBookmark(r *http.Request) (bookmarkInput, error) {
	r.Body = http.MaxBytesReader(nil, r.Body, 1<<20) // 上限 1MB
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	var in bookmarkInput
	if err := dec.Decode(&in); err != nil {
		return in, fmt.Errorf("invalid JSON body: %w", err)
	}
	if strings.TrimSpace(in.Title) == "" {
		return in, errors.New("title is required")
	}
	if utf8.RuneCountInString(in.Title) > 100 {
		return in, errors.New("title too long (max 100 chars)")
	}
	u, err := url.Parse(in.URL)
	if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
		return in, fmt.Errorf("url must be an absolute http(s) link, got %q", in.URL)
	}
	if len(in.Tags) > 10 {
		return in, errors.New("too many tags (max 10)")
	}
	return in, nil
}

四道闸门按成本从低到高排:MaxBytesReader 在读取阶段掐掉超大 body,超限时 Decode 返回错误,不会先把 2GB 读进内存;DisallowUnknownFields 把客户端拼错的字段名(titel)当场拒绝,而不是静默丢弃;必填与长度校验防脏数据入库;url.Parse 配合 scheme 检查挡掉 javascript: 这类链接。结构体字段带 json 标签,解码按标签匹配,字段首字母小写也能从 JSON 接进来。

错误响应统一形状,前端只写一套处理:

func writeError(w http.ResponseWriter, code int, msg string) {
	writeJSON(w, code, map[string]string{"error": msg})
}

mux.HandleFunc("POST /bookmarks", func(w http.ResponseWriter, r *http.Request) {
	in, err := parseBookmark(r)
	if err != nil {
		writeError(w, http.StatusBadRequest, err.Error())
		return
	}
	id, err := store.Add(Bookmark{Title: in.Title, URL: in.URL, Tags: in.Tags})
	if err != nil {
		writeError(w, http.StatusInternalServerError, "internal error")
		return
	}
	writeJSON(w, http.StatusCreated, map[string]int64{"id": id})
})

两条原则:可预期的输入问题一律 400 并把原因写进 error 字段,客户端能改的才配叫 400;内部错误不外泄细节,只说 internal error,err 的内容进日志。创建成功用 201 而不是 200,语义更准。校验顺序也有讲究:先拦结构问题(JSON 能不能解码、字段名对不对),再查语义问题(值合不合法),错误信息按这个顺序返回,客户端修起来最快。

示例拆解

go run .

curl -i -X POST http://127.0.0.1:8080/bookmarks \
  -d '{"title":"Go","url":"https://go.dev","tags":["go"]}'
# 201 Created,{"id":2}

curl -i -X POST http://127.0.0.1:8080/bookmarks -d '{"url":"https://go.dev"}'
# 400,{"error":"title is required"}

curl -i -X POST http://127.0.0.1:8080/bookmarks -d '{"title":"Go","url":"ftp://go.dev"}'
# 400,{"error":"url must be an absolute http(s) link, got \"ftp://go.dev\""}

curl -i -X POST http://127.0.0.1:8080/bookmarks \
  -d '{"title":"Go","titel":"typo","url":"https://go.dev"}'
# 400,invalid JSON body: unknown field "titel"

四条请求覆盖通过、缺字段、坏格式、拼错字段。错误信息里带着「收到的值」,客户端能自己定位问题,这就是校验信息的价值。

动手任务

  1. 在 parseBookmark 里加 tags 清洗:去重并过滤空串,输入 ["go","go",""] 存成 ["go"]。
  2. 把 1MB 上限临时调成 100 字节,发一个大 body,观察 400 的错误内容,再改回来。
  3. 写一个 normalizeURL 小函数(去掉尾部斜杠),在校验前调用,用三种输入验证。

常见错误

  • 校验逻辑直接写在 handler 里越堆越长:解码校验收进 parseXxx 纯函数,handler 只做编排,G13 才好测。
  • MaxBytesReader 漏掉:一个手滑或恶意的大 body 直接把内存吃光。
  • 未知字段静默忽略还觉得宽容:客户端拼错字段名永远得不到反馈,联调时双方都懵。
  • 内部错误也把 err.Error() 原样吐给客户端:泄露 SQL、路径等实现细节,日志归日志、响应归响应。

检查与复盘

完成下方检查题。自查清单:

  • 每个写入接口都过了 MaxBytesReader 和 DisallowUnknownFields
  • 错误响应永远是 {"error": "..."} 一种形状,4xx 表达可修复问题
  • 校验逻辑是可独立测试的纯函数,G13 会直接拿来写表驱动测试

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读代码(Go 1.22+,net/http)。用请求体 {"title": ""} 调 POST /books,响应是什么?

package main

import (
	"encoding/json"
	"net/http"
)

type CreateReq struct {
	Title string `json:"title"`
}

func create(w http.ResponseWriter, r *http.Request) {
	var req CreateReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}
	if req.Title == "" {
		http.Error(w, "title required", http.StatusBadRequest)
		return
	}
	w.WriteHeader(http.StatusCreated)
}
Q2多选中等(多选,全部选对才算正确)

关于 Go net/http 的请求体读取与限制(Go 1.22+),下列说法正确的有哪些?(多选)

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...