GoWeb 处理请求详解:请求行、请求头、请求参数与给客户端响应
🥰个人主页:会编程的土豆(欢迎来访)
💎作者简介:后端学习者
❄️个人专栏:数据结构与算法,数据库,leetcode
✨那些你一个人走过的夜路,终将化作照亮未来的光
〇、这篇要解决什么问题
很多同学听到「处理请求」会懵:到底在处理什么?
一句话:
浏览器发来一次 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,只要 body5.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
