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.Methodr.URLr.Proto
请求头Host、Cookie、Content-Typer.Headerr.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一般是POSTr.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。
  • 真正提交登录看POSTlogin那一条。

五、获取请求参数(重点中的重点)

参数 = 业务字段,如usernamepasswordbookIdpageNo

5.1 参数可能来自两处

  1. URL 查询串/getBody?username=admin
  2. 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、链接上的bookIdFormValueFormValue(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 对上号:

  1. 退出登录
  2. F12 →网络→ 勾保留日志→ 过滤选全部(不要只选 JS)
  3. 打开登录页,填账号,点登录
  4. 找到名称login、方法POST的那条
  5. 载荷 / Payload:就是请求体里的usernamepassword
  6. 请求标头:能看到 Method、Cookie、Content-Type
  7. 响应:状态码、返回的 HTML

常见踩坑

现象原因
只有jquery-1.7.2.js且是 GET点错了静态脚本,不是登录提交
Referer 里有 login只是引用来源,不是登录方法
点完登录列表空了没勾「保留日志」,跳转清记录了
只有 JS 没有 login过滤勾成了 JS,改选「全部」
已显示「欢迎 xxx」却没有 POST已登录,后端走了「已登录直接出首页」,先退出再试

八、书城里还有哪些「处理请求」实例

把同一套模式套到别处,就通了:

功能怎么进来取参响应
注册POST/registPostFormValue 用户名密码邮箱成功页 / 回注册页
Ajax 用户名POST/checkUserNamePostFormValue写一段文字
删书链接 GET/带参FormValue(“bookId”)再渲染管理列表
添加/改书POST/updateOraddBookPostFormValue 各字段GetPageBooks
翻页GET?pageNo=FormValue(“pageNo”)模板首页
加购Ajax POSTFormValue(“bookId”)文本或 JSON

全部都是:HandleFunc → 处理器读 r → 写 w


九、易错点汇总

  1. 用了r.Form/r.PostForm却忘了ParseForm→ 空 map;改用FormValue可自动解析。
  2. 登录用FormValue,URL 被带上?password=→ 可能污染逻辑;密码用PostFormValue
  3. name和 PostFormValue 字符串不一致→ 永远取到空字符串。
  4. action路径和 HandleFunc 不一致→ 404 或进错处理器。
  5. w.WriteHeader().Set/WriteHeader→ 头设不上。
  6. 把「打开登录页」的 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