Go 表单解析入门:FormValue、ParseForm 和 PostForm 怎么选

用登录表单示例讲 Go HTTP 表单解析的基本用法,包括 query、x-www-form-urlencoded、multipart、大小限制和校验。

不是所有 Web 请求都是 JSON。登录页、搜索框、后台表单、文件上传仍然会用表单。Go 标准库对表单解析支持很好,但几个 API 容易混:FormValuePostFormValueParseFormParseMultipartForm。本文用登录表单讲清楚基本用法。

GET 查询参数

搜索接口通常用 query:

func search(w http.ResponseWriter, r *http.Request) {
	keyword := r.URL.Query().Get("q")
	page := r.URL.Query().Get("page")
	_ = keyword
	_ = page
}

GET 参数直接从 r.URL.Query() 读取,不需要 ParseForm。它只来自 URL,不会读取 body。

普通 POST 表单

HTML:

<form method="post" action="/login">
  <input name="email">
  <input name="password" type="password">
  <button>登录</button>
</form>

Handler:

func login(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad form", http.StatusBadRequest)
		return
	}
	email := r.PostForm.Get("email")
	password := r.PostForm.Get("password")
	_ = email
	_ = password
}

PostForm 只包含 body 里的表单字段。Form 会合并 query 和 body。登录这种场景更推荐 PostForm,避免 URL query 覆盖或混入同名字段。

FormValue 的便利和代价

email := r.FormValue("email")

FormValue 会自动调用 ParseMultipartFormParseForm,并从 query 和 body 合并后的 Form 里取值。它很方便,但会隐藏错误。对于严肃表单,建议显式 ParseForm,检查错误,再读 PostForm

小型搜索页用 FormValue 问题不大;登录、支付、配置修改这类接口,显式解析更清楚。

限制 body 大小

表单也要限制大小:

r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
if err := r.ParseForm(); err != nil {
	http.Error(w, "form too large", http.StatusBadRequest)
	return
}

不要让客户端提交无限大的 body。即使只是登录表单,也应该有合理上限。

multipart 表单

文件上传用 multipart:

func upload(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
	if err := r.ParseMultipartForm(10 << 20); err != nil {
		http.Error(w, "bad multipart form", http.StatusBadRequest)
		return
	}
	title := r.FormValue("title")
	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "file required", http.StatusBadRequest)
		return
	}
	defer file.Close()
	_ = title
	_ = header
}

ParseMultipartForm 可能使用临时文件。处理完成后可以清理:

defer func() {
	if r.MultipartForm != nil {
		_ = r.MultipartForm.RemoveAll()
	}
}()

校验表单

type LoginForm struct {
	Email    string
	Password string
}

func parseLoginForm(r *http.Request) (LoginForm, error) {
	if err := r.ParseForm(); err != nil {
		return LoginForm{}, err
	}
	form := LoginForm{
		Email:    strings.TrimSpace(r.PostForm.Get("email")),
		Password: r.PostForm.Get("password"),
	}
	if form.Email == "" || form.Password == "" {
		return LoginForm{}, errors.New("email and password are required")
	}
	return form, nil
}

把解析和校验放到函数里,handler 会更干净,测试也更容易写。

测试表单

func TestParseLoginForm(t *testing.T) {
	body := strings.NewReader("email=a%40example.com&password=secret")
	req := httptest.NewRequest(http.MethodPost, "/login", body)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	form, err := parseLoginForm(req)
	if err != nil {
		t.Fatal(err)
	}
	if form.Email != "a@example.com" {
		t.Fatalf("email = %q", form.Email)
	}
}

测试要设置正确 Content-Type,否则解析结果可能不符合预期。

多值字段

复选框和多选输入会提交多个同名字段。不要只用 Get

roles := r.PostForm["roles"]

可以逐个校验:

for _, role := range roles {
	if role != "admin" && role != "editor" && role != "viewer" {
		return errors.New("invalid role")
	}
}

表单字段和 JSON 一样需要白名单校验。前端隐藏字段、下拉选项都不能当作可信输入,用户完全可以自己构造请求。

CSRF 边界

浏览器表单会自动携带 Cookie,所以修改类表单通常还要考虑 CSRF 防护。Go 标准库不内置 CSRF 方案,但你应该知道它属于表单登录态的重要边界。内部工具可以依赖额外认证和 SameSite Cookie,公开站点则需要更完整的 token 机制。

小结

Go 表单解析并不复杂:GET query 用 r.URL.Query(),普通 POST 表单显式 ParseForm 后读 r.PostForm,文件上传用 ParseMultipartFormFormFileFormValue 很方便,但会隐藏解析错误,也会合并 query 和 body。

表单接口同样需要大小限制、字段校验和测试。不要因为它不是 JSON 就放松边界。HTTP 输入都不可信,解析清楚只是第一步。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页