🥰个人主页:会编程的土豆(欢迎来访)
💎作者简介:后端学习者
✨那些你一个人走过的夜路,终将化作照亮未 来的光



〇、这篇要解决什么问题
很多同学听到「处理请求」会懵:到底在处理什么?
一句话:
浏览器发来一次 HTTP 请求之后,你在处理器函数里把请求拆开看清楚,办完事,再把结果写回浏览器
拆开看 = 读 *http.Request(常写作 r)
写回去 = 用 http.ResponseWriter(常写作 w)
整门书城课(登录、加购、删书......)都是这一套的反复使用
一、请求是怎么进到你代码里的
1.1 先挂路由
http.HandleFunc("/login", controller.Login)
http.ListenAndServe(":8080", nil)
含义:
- 访问路径
/login时,调用Login这个函数 nil表示使用默认路由表
1.2 处理器函数长什么样
func Login(w http.ResponseWriter, r *http.Request) {
// 这里就是「处理请求」的现场
}
| 参数 | 角色 |
|---|---|
r |
这次请求的全部信息:方法、URL、头、Cookie、表单...... |
w |
往浏览器写响应的出口:HTML、纯文本、重定向、Set-Cookie...... |
所以课里说的「处理请求的函数」,就是这种签名的函数
1.3 一张时序图
浏览器点击「登录」
│
▼
打包 HTTP:POST /login + 请求头 + 请求体(username&password)
│
▼
Go 服务器 ListenAndServe 收到字节流,解析成 http.Request
│
▼
DefaultServeMux 按路径找到 Login
│
▼
Login(w, r) 执行:读 r → 查库/写 Session → 写 w
│
▼
浏览器收到 HTML / 跳转,用户看到结果
二、一次 HTTP 请求里有什么(协议基础)
课里把请求拆成三块,务必背下来:
① 请求行 POST /login HTTP/1.1
② 请求头 Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
Cookie: user=xxxx-xxxx-...
③ 请求体 username=admin&password=123456
对应关系:
| 报文部分 | 例子 | Go 怎么读 |
|---|---|---|
| 请求行 | 方法、路径、协议 | r.Method、r.URL、r.Proto |
| 请求头 | Host、Cookie、Content-Type | r.Header、r.Cookie |
| 请求体 | 表单字段 | ParseForm 后的 PostForm / PostFormValue |
GET 和 POST 差在哪
| GET(如翻页) | POST(如登录) | |
|---|---|---|
| 参数常在哪 | URL:?pageNo=2&min=10 |
请求体 |
| 请求体 | 常规没有 | 有表单数据 |
| 书城例子 | /getPageBooks?pageNo=2 |
/login 提交账号密码 |
课里常说:「GET 没有请求体,POST 才有请求体」------指常规用法。
三、获取请求行
请求行告诉你:用什么方法、访问哪个路径。
func showRequestLine(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "方法:", r.Method) // GET / POST / ...
fmt.Fprintln(w, "路径:", r.URL.Path) // /login
fmt.Fprintln(w, "查询串:", r.URL.RawQuery) // pageNo=2&min=10(不含 ?)
fmt.Fprintln(w, "完整 URL:", r.URL.String())
fmt.Fprintln(w, "协议:", r.Proto) // HTTP/1.1
}
实际用途
- 同一个路径既要「打开表单」又要「提交表单」时,可用
r.Method == "POST"分支 - 调试时先打印 Method/Path,确认请求有没有打到你以为的处理器
书城登录页表单是:
<form action="/login" method="POST">
因此命中 Login 时,r.Method 一般是 POST,r.URL.Path 是 /login。
四、获取请求头
请求头是一堆元信息键值对。
func showHeaders(w http.ResponseWriter, r *http.Request) {
// 取单个头(大小写不敏感)
ua := r.Header.Get("User-Agent")
ct := r.Header.Get("Content-Type")
ref := r.Header.Get("Referer") // 从哪个页面跳过来的
fmt.Fprintln(w, "UA:", ua)
fmt.Fprintln(w, "Content-Type:", ct)
fmt.Fprintln(w, "Referer:", ref)
// 遍历全部头(学习用)
for k, vals := range r.Header {
fmt.Fprintf(w, "%s = %v\n", k, vals)
}
}
Cookie 也算「头」,但有专用 API
登录成功后浏览器会带:
Cookie: user=会话UUID
读法:
cookie, err := r.Cookie("user")
if err == nil {
sessionID := cookie.Value
}
这是后面 Session 课的基础:用 Cookie 里的 id 去数据库找登录用户。
易错点
- Network 里看 jquery 请求时,Referer 里有
/login,只表示「从登录页加载的脚本」,不是说登录方法是 GET。 - 真正提交登录看 POST
login那一条。
五、获取请求参数(重点中的重点)
参数 = 业务字段,如 username、password、bookId、pageNo。
5.1 参数可能来自两处
- URL 查询串 :
/getBody?username=admin - POST 表单正文 :
username=hanzong&password=666666
课里示例表单:
<form action="http://localhost:8080/getBody" method="POST">
用户名:<input type="text" name="username" value="hanzong"><br/>
密码:<input type="password" name="password" value="666666"><br/>
<input type="submit">
</form>
若地址还写成 /getBody?username=admin,同一次请求里 username 就有两个来源。
5.2 四种取法对比
① r.Form ------ URL + 表单都要
r.ParseForm() // 必须先解析!否则 Form 是空的
fmt.Fprintln(w, r.Form)
// 可能输出:
// map[username:[hanzong admin] password:[666666]]
// 注意:表单值在前,URL 值在后
r.Form 的类型本质是 map[string][]string:一个 key 可以对应多个值。
② r.PostForm ------ 只要表单
r.ParseForm()
fmt.Fprintln(w, r.PostForm)
// map[username:[hanzong] password:[666666]]
// 没有 URL 里的 admin
适用默认编码 application/x-www-form-urlencoded(普通 <form>)。
上传文件用 multipart/form-data 时,要用 MultipartForm,不能指望普通 PostForm 拿文件。
③ r.FormValue("username") ------ 快捷,URL+表单
v := r.FormValue("username") // 自动 ParseForm,取第一个
// 上面例子里第一个是 hanzong(表单优先)
④ r.PostFormValue("username") ------ 快捷,只要表单
v := r.PostFormValue("username") // 自动 Parse,只要 body
5.3 怎么选(书城实用口诀)
| 场景 | 推荐 |
|---|---|
| 登录、注册密码 | PostFormValue(避免 URL 偷带参数) |
翻页 pageNo、链接上的 bookId |
FormValue 或 FormValue(GET 也行) |
Ajax $.post(..., {bookId}) |
FormValue / PostFormValue 都可以 |
| 调试想看全部来源 | ParseForm + 打印 r.Form |
5.4 和前端 HTML 的对齐关系(一定要对上)
<form action="/login" method="POST">
<input name="username" ...>
<input name="password" type="password" ...>
<input type="submit" value="登录">
</form>
username := r.PostFormValue("username") // 必须等于 name
password := r.PostFormValue("password")
| HTML | 作用 |
|---|---|
action="/login" |
发到哪个 URL → 对应哪个 HandleFunc |
method="POST" |
参数进请求体 |
name="username" |
请求体里的字段名 |
表单本身不是请求体 ;点提交后,浏览器把 name=值 打包成请求体发给后端。
Ajax 同理:
$.post("/checkUserName", {"username": username}, function(res){ ... });
jQuery 把对象打成 POST 体;后端仍用 PostFormValue("username")。
5.5 书城登录里的真实写法
func Login(w http.ResponseWriter, r *http.Request) {
flag, _ := dao.IsLogin(r)
if flag {
GetPageBooksByPrice(w, r)
return
}
// ↓↓↓ 处理请求参数 ↓↓↓
username := r.PostFormValue("username")
password := r.PostFormValue("password")
// ↓↓↓ 业务:查库、Session、Cookie ↓↓↓
user, _ := dao.CheckUserNameAndPassword(username, password)
if user.ID > 0 {
// ... AddSession + SetCookie ...
// 给客户端响应:成功页
} else {
// 给客户端响应:登录页 + 错误提示
}
}
读参数只是第一步;后面查库、写 Session 属于业务,但都挂在「处理这次请求」的同一个函数里。
六、给客户端响应
读完 r、办完事,必须通过 w 给浏览器一个交代,否则页面一直转圈或空白。
6.1 写纯文本 / HTML 片段
w.Write([]byte("用户名已存在!"))
// 或
fmt.Fprintln(w, "请先登录!")
Ajax 校验用户名常用这种方式:返回一小段文字或带颜色的 HTML。
6.2 写完整页面(模板)
t := template.Must(template.ParseFiles("views/pages/user/login.html"))
t.Execute(w, "用户名或密码不正确!")
Execute 的第二个参数会进模板的 {``{.}}。
6.3 重定向
http.Redirect(w, r, "/main", http.StatusFound) // 302
注销后回首页、未登录访问购物车去登录页,都常用重定向。
6.4 设置响应头 / Cookie
w.Header().Set("Content-Type", "application/json; charset=utf-8")
http.SetCookie(w, &http.Cookie{
Name: "user",
Value: uuid,
Path: "/",
HttpOnly: true,
})
注意: 若手动 WriteHeader(状态码),必须在写入 body 之前调用;写了 body 再改头往往无效。
6.5 响应和请求是一对
请求:浏览器 → 服务器(你读 r)
响应:服务器 → 浏览器(你写 w)
「处理请求」完整闭环 = 解析请求 → 执行业务 → 写响应。
七、用浏览器 Network 验证(强烈建议亲手做)
理论看完,用 Network 对上号:
- 退出登录
- F12 → 网络 → 勾 保留日志 → 过滤选 全部(不要只选 JS)
- 打开登录页,填账号,点登录
- 找到名称
login、方法 POST 的那条 - 看 载荷 / Payload :就是请求体里的
username、password - 看 请求标头:能看到 Method、Cookie、Content-Type
- 看 响应:状态码、返回的 HTML
常见踩坑
| 现象 | 原因 |
|---|---|
只有 jquery-1.7.2.js 且是 GET |
点错了静态脚本,不是登录提交 |
| Referer 里有 login | 只是引用来源,不是登录方法 |
| 点完登录列表空了 | 没勾「保留日志」,跳转清记录了 |
| 只有 JS 没有 login | 过滤勾成了 JS,改选「全部」 |
| 已显示「欢迎 xxx」却没有 POST | 已登录,后端走了「已登录直接出首页」,先退出再试 |
八、书城里还有哪些「处理请求」实例
把同一套模式套到别处,就通了:
| 功能 | 怎么进来 | 取参 | 响应 |
|---|---|---|---|
| 注册 | POST /regist |
PostFormValue 用户名密码邮箱 | 成功页 / 回注册页 |
| Ajax 用户名 | POST /checkUserName |
PostFormValue | 写一段文字 |
| 删书 | 链接 GET/带参 | FormValue("bookId") | 再渲染管理列表 |
| 添加/改书 | POST /updateOraddBook |
PostFormValue 各字段 | GetPageBooks |
| 翻页 | GET ?pageNo= |
FormValue("pageNo") | 模板首页 |
| 加购 | Ajax POST | FormValue("bookId") | 文本或 JSON |
全部都是:HandleFunc → 处理器读 r → 写 w。
九、易错点汇总
- 用了
r.Form/r.PostForm却忘了ParseForm→ 空 map;改用FormValue可自动解析。 - 登录用
FormValue,URL 被带上?password=→ 可能污染逻辑;密码用PostFormValue。 name和 PostFormValue 字符串不一致 → 永远取到空字符串。action路径和 HandleFunc 不一致 → 404 或进错处理器。- 先
w.Write再Header().Set/WriteHeader→ 头设不上。 - 把「打开登录页」的 GET 和「提交登录」的 POST 搞混 → Network 里看 Method 区分。
十、Mini FAQ
Q1:处理请求和处理响应是两件事吗?
A:课里常分集讲,但代码里在同一个处理器函数中完成:先处理请求(读 r),再给客户端响应(写 w)。
Q2:处理器函数和 DAO 有什么区别?
A:处理器面向 HTTP(参数、Cookie、模板);DAO 面向数据库(SQL)。处理器里调用 DAO,不要在 DAO 里解析 http.Request。
Q3:为什么有的用 FormValue,有的用 PostFormValue?
A:看参数是否必须来自 POST 正文。敏感、只应表单提交的用 PostFormValue;链接/查询串用 FormValue 更方便。
Q4:ParseForm 会读请求体吗?
A:会解析并缓存表单数据;FormValue 内部也会按需解析。一般对同一请求解析一次即可。
Q5:给客户端响应一定要 HTML 吗?
A:不必。可以是纯文本、JSON、重定向、文件下载等,取决于你怎么写 w