表单
1. HTML 表单基础知识
常见表单结构:
go
<form method="POST" action="/submit">
<input type="text" name="username">
<input type="password" name="password">
<input type="file" name="file">
<button type="submit">提交</button>
</form>
重点属性:
| 属性 | 说明 |
|---|---|
method |
GET (参数放在 URL 中(?a=1&b=2)) / POST(参数放在 Request Body 中) |
action |
提交地址,决定表单提交到哪里 |
enctype |
编码方式(上传文件必须 multipart/form-data) |
常见 enctype:
| enctype | 用途 |
|---|---|
application/x-www-form-urlencoded |
默认,普通表单, 在发送前编码所有字符 |
multipart/form-data |
文件上传, 不对字符编码 |
text/plain |
基本不用, 空格转换为 "+" 加号,但不对特殊字符编码 |
name/value机制
每个字段必须有 name 属性,否则不会被提交到服务器
2. Go 中表单解析机制
| 特性 | r.ParseForm() |
r.ParseMultipartForm(maxMemory) |
|---|---|---|
| 表单类型 | application/x-www-form-urlencoded |
multipart/form-data |
| 文件支持 | 不支持文件上传 | 支持文件上传 |
| 内存使用 | 全部加载到内存 | 可控制内存使用量 |
| 使用场景 | 普通文本表单 | 文件上传表单 |
**r.ParseForm()**解析后数据存入:
r.FormURL查询参数 + POST表单数据r.PostForm仅POST表单数据
r.ParseMultipartForm(maxMemory)
解析后:
- 字段 →
r.MultipartForm.Value - 文件 →
r.MultipartForm.File
r.Form / r.PostForm / r.FormValue 区别
| 方法 | 区别 |
|---|---|
r.Form |
包含:GET 参数 ,POST 参数(如果有),本质是 map[string][]string |
r.PostForm |
仅包含 POST 表单(不含 GET) |
r.FormValue(),r.PostFormValue() |
自动调用:ParseForm,返回第一个值,如果不存在 → "" ,推荐在大多数场景使用 |
r.FormFile() |
自动调用: r.ParseMultipartForm(32 << 20) |
3. GET / POST 处理流程
典型结构:
func handler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// 渲染表单或返回资源
case http.MethodPost:
// 解析 body、验证、处理
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
4. 表单字段类型处理
| HTML 类型 | Go 在接收时类型 |
|---|---|
<input type="text"> |
string |
<input type="number"> |
string → 转 int/float |
<input type="checkbox"> |
多值 → []string |
<input type="radio"> |
单值 string |
<select> |
单/多值 |
<textarea> |
string |
<input type="file"> |
文件 → multipart.File |
示例:
ageStr := r.FormValue("age")
age, _ := strconv.Atoi(ageStr)
多值字段(checkbox、multi-select)
HTML:
<input type="checkbox" name="hobby" value="run">
<input type="checkbox" name="hobby" value="read">
后端:
hobbies := r.Form["hobby"] // []string
r.FormValue("hobby") 只能取第一个!
5. 文件上传
HTML:
<form enctype="multipart/form-data" method="post">
<input type="file" name="avatar">
</form>
Go:
r.ParseMultipartForm(20 << 20)
file, header, err := r.FormFile("avatar")
defer file.Close()
dst, _ := os.Create("uploads/" + header.Filename)
defer dst.Close()
io.Copy(dst, file)
👉 推荐使用 io.Copy(性能最佳)
👉 不要使用 io.ReadAll(占内存)
限制上传大小(必做)
防止 DoS 或大文件攻击:
go
// HTTP请求体级别,限制整个请求体大小,读取请求体时出发
r.Body = http.MaxBytesReader(w, r.Body, 20<<20) // 20MB
// Multipart表单解析级别,限制单个文件的内存使用量,解析multipart数据时触发
r.ParseMultipartForm(20 << 20)
6. 表单验证
表单验证要保证三件事:
| 验证类型 | 示例 | 为什么重要 |
|---|---|---|
| 格式验证 | 邮箱是否合法?数字是否数字?长度? | 防止非法输入带来错误或漏洞 |
| 安全验证 | XSS、SQL 注入、路径穿越、MIME 伪造 | 防止攻击 |
| 业务验证 | 用户名是否已存在?日期必须大于今天? | 保证业务正确性 |
文本字段
go
// 必填字段
name := r.FormValue("name")
if name == "" {
http.Error(w, "name is required", http.StatusBadRequest)
return
}
// 长度限制
if len(name) < 2 || len(name) > 30 {
http.Error(w, "name length must be 2~30", http.StatusBadRequest)
return
}
数字字段
go
// 类型验证 (int)
ageStr := r.FormValue("age")
age, err := strconv.Atoi(ageStr)
if err != nil {
http.Error(w, "invalid age", http.StatusBadRequest)
return
}
// 范围验证
if age < 1 || age > 120 {
http.Error(w, "age must be 1~120", http.StatusBadRequest)
return
}
邮箱/手机号验证
go
var phoneReg = regexp.MustCompile(`^1[3-9]\d{9}$`)// 邮箱正则
var emailReg = regexp.MustCompile(`^[\w+.-]+@[\w.-]+\.[a-zA-Z]{2,}$`)
email := r.FormValue("email")
if !emailReg.MatchString(email) {
http.Error(w, "invalid email", http.StatusBadRequest)
return
}
// 手机号正则
var phoneReg = regexp.MustCompile(`^1[3-9]\d{9}$`)
密码验证
go
// 至少八位,包含数字和字母
password := r.FormValue("password")
if len(password) < 8 {
http.Error(w, "password too short", http.StatusBadRequest)
return
}
hasNum := strings.ContainsAny(password, "0123456789")
hasLetter := strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
if !hasNum || !hasLetter {
http.Error(w, "password must contain letters & numbers", http.StatusBadRequest)
return
}
文件验证
go
// 文件类型
fh, header, _ := r.FormFile("file")
defer fh.Close()
buf := make([]byte, 512)
fh.Read(buf)
mime := http.DetectContentType(buf)
allowed := map[string]bool{
"image/jpeg": true,
"image/png": true,
}
if !allowed[mime] {
http.Error(w, "invalid file type", http.StatusBadRequest)
return
}
fh.Seek(0, io.SeekStart)
// 文件拓展名
ext := strings.ToLower(filepath.Ext(header.Filename))
if ext != ".jpg" && ext != ".png" {
http.Error(w, "invalid extension", http.StatusBadRequest)
return
}
// 文件名安全(防路径穿越)
filename := filepath.Base(header.Filename)
安全性验证
go
// 防XSS-使用html/template
t.Execute(w, "<script>alert('you have been pwned')</script>")
// 防 CSRF
// 生成 token
func GenerateRandomToken(n int) (string, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
token, _ := GenerateRandomToken(32) // 32 字节 = 64 hex 字符
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: token,
})
// 提交时验证
formToken := r.FormValue("csrf_token")
cookie, _ := r.Cookie("csrf_token")
if cookie == nil || cookie.Value != formToken {
http.Error(w, "CSRF validation failed", http.StatusForbidden)
return
}
业务验证
go
// 确认密码
pwd := r.FormValue("password")
confirm := r.FormValue("confirm")
if pwd != confirm {
http.Error(w, "password not match", http.StatusBadRequest)
return
}
// 日期验证,起始时间必须<结束时间
start := r.FormValue("start")
end := r.FormValue("end")
startTime, _ := time.Parse("2006-01-02", start)
endTime, _ := time.Parse("2006-01-02", end)
if !startTime.Before(endTime) {
http.Error(w, "start must be before end", http.StatusBadRequest)
return
}
7. 安全性
| 风险 | 解决方案 |
|---|---|
| XSS | 使用 html/template 自动转义 |
| CSRF | 使用 token(随机值) |
| 路径穿越攻击 | 使用 filepath.Base(filename) |
| 文件覆盖 | 使用 UUID / 时间戳重命名 |
| MIME 伪造 | 检测 http.DetectContentType() |
8. 常见坑与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
FormValue 取不到值 |
忘记 name= 属性 |
确保表单字段有 name |
| 上传文件 always nil | 忘记 enctype="multipart/form-data" |
加 enctype |
| 大文件导致崩溃 | 使用 io.ReadAll |
换 io.Copy,加 MaxBytesReader |
| 多值表单只拿到一个值 | 用 FormValue |
用 r.Form["key"] |
| GET 参数取不到 | 表单 method=GET | 用 r.URL.Query() |