🥰个人主页:会编程的土豆(欢迎来访)
💎作者简介:后端学习者 ❄️个人专栏:数据结构与算法,数据库,leetcode ✨那些你一个人走过的夜路,终将化作照亮未来的光



对应课:搭建服务器、HTTP 协议、处理请求、给客户端响应、读参数(约 5.4 节重点) 代码示例来自 bookstore0612,路径:C:\\Users\\zjl\\Projects\\goweb-bookstore
一、最小可运行服务器(HelloWorld 逐行)
1.1 完整代码
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World")
}
func main() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":8080", nil)
}
保存为 main.go,在项目根执行:
go run main.go
浏览器访问 http://localhost:8080/hello,页面显示 Hello World(后面可能多一个换行,来自 Fprintln)。
1.2 逐行解释
| 1 | package main | 可执行程序必须是 main 包 |
| 4~5 | import "net/http" | 标准库 HTTP 包,路由、服务器、Cookie 都在这 |
| 7~9 | func hello(w, r) | 处理器函数:处理一次请求;固定签名两个参数 |
| 8 | fmt.Fprintln(w, "Hello World") | 往响应体写文本;w 实现了 io.Writer |
| 12 | http.HandleFunc("/hello", hello) | 登记路由:路径 → 函数(注意传的是函数名,不是 hello()) |
| 13 | http.ListenAndServe(":8080", nil) | 阻塞监听 8080;nil = 使用默认路由表 |
1.3 两个核心类型
http.ResponseWriter(常名 w) — 代表「这次请求的响应」:
- 写状态码(默认 200)
- 写响应头(w.Header().Set(…))
- 写响应体(fmt.Fprintln(w,…)、w.Write([]byte(…)))
http.Request(常名 r) — 代表「浏览器发来的这次请求」:
- URL、Method(GET/POST)
- 请求头、Cookie
- 请求体(POST 表单、JSON 等)
1.4 处理器函数 = 课里的「处理请求的函数」
书城项目里每一个 controller.Login、controller.AddBook2Cart 都是这种签名:
func Login(w http.ResponseWriter, r *http.Request) { … }
一次 HTTP 请求 → 调用一次处理器函数。
二、三种挂法:HandleFunc、Handle、ServeHTTP
2.1 方式一:HandleFunc —— 挂普通函数(书城 99% 用法)
http.HandleFunc("/login", controller.Login)
- 第一个参数:URL 路径(精确匹配,课里阶段不做正则)
- 第二个参数:符合 func(w,r) 签名的函数
内部原理(了解即可): HandleFunc 会把你的函数包装成一个实现了 ServeHTTP 的对象,再注册到 Mux。
2.2 方式二:Handle —— 挂「对象」
type MyHandler struct{}
func (m *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "正在通过处理器处理你的请求")
}
func main() {
myHandler := MyHandler{}
http.Handle("/", &myHandler) // 注册的是指针 &myHandler
http.ListenAndServe(":8080", nil)
}
要点:
| 方法名必须叫 ServeHTTP | 这是 http.Handler 接口规定的 |
| 注册时用 &myHandler | 指针接收者,避免拷贝 |
| Handle 不是立刻调用 | 只是登记;请求来了才执行 |
2.3 方式三:ServeHTTP —— 接口本质
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
谁实现了这个接口,谁就可以被 http.Handle(path, handler) 注册。
对比表:
| HandleFunc(path, fn) | 函数 | 书城所有业务处理器 |
| Handle(path, obj) | 实现了 Handler 的对象 | http.FileServer、http.StripPrefix 返回值 |
| 直接写 ServeHTTP | 自定义类型 | 课里演示、标准库内部 |
2.4 书城 main.go 里的真实注册
// 静态资源:Handle + FileServer(FileServer 本身实现了 Handler)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("views/static"))))
// 动态业务:HandleFunc + controller 函数
http.HandleFunc("/login", controller.Login)
http.HandleFunc("/addBook2Cart", controller.AddBook2Cart)
本质都是:URL → 某种处理逻辑。
三、路由表 ServeMux —— 用「电话总机」类比
3.1 是什么
路由表(ServeMux)就是一张 URL → 处理器 的对照表:
/login → controller.Login
/regist → controller.Regist
/main → controller.GetPageBooksByPrice
/addBook2Cart → controller.AddBook2Cart
…
类比:公司总机。客户拨分机号(URL),总机查表转接到对应部门(处理器)。
3.2 DefaultServeMux(默认总机)
http.HandleFunc("/login", controller.Login)
http.ListenAndServe(":8080", nil) // 第二个参数 nil → 用 DefaultServeMux
- Go 在 net/http 包里预置了全局变量 DefaultServeMux
- http.HandleFunc / http.Handle 实际是往 DefaultServeMux 上注册
- 一开始表是空的,所有业务路径都是你自己挂上去的
- 「默认」≠ 内置了 /login;只是「默认用这一张表」
3.3 NewServeMux(自己买一台总机)
mux := http.NewServeMux()
mux.HandleFunc("/myMux", myHandler)
http.ListenAndServe(":8080", mux) // 必须传入 mux,不能 nil
| ListenAndServe(":8080", nil) | DefaultServeMux | 用包级 HandleFunc 注册的都能命中 |
| ListenAndServe(":8080", mux) | 自定义 mux | 只有 mux.HandleFunc 注册的能命中 |
| HandleFunc + ListenAndServe(…, mux) 混用 | 分裂 | 注册在 Default 上的路径在自定义 mux 上 404 |
何时效果不同(易错):
http.HandleFunc("/a", handlerA) // 注册在 DefaultServeMux
mux := http.NewServeMux()
mux.HandleFunc("/b", handlerB)
http.ListenAndServe(":8080", mux) // 只有 /b 有效,/a 会 404!
书城课里为了讲清概念会写 NewServeMux;入门项目用 DefaultServeMux + nil 足够,和自定义 mux 在「只用一个表」时效果几乎一样。
3.4 路径匹配规则(课里常用子集)
- 注册 /static/ 这种带尾部 / 的前缀,会匹配 /static/css/style.css
- 精确路径如 /login 只匹配 /login(不含 query:/login?x=1 仍匹配)
- 根路径 / 在书城用匿名函数单独处理,避免抢走所有 404
四、http.Server 结构体(自定义服务器)
4.1 基本写法
import "time"
server := http.Server{
Addr: ":8080", // 监听地址
Handler: nil, // nil 则内部用 DefaultServeMux
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
server.ListenAndServe()
4.2 常用字段
| Addr | 如 :8080、127.0.0.1:8080 |
| Handler | 根处理器,通常是 nil、*ServeMux 或自定义 Handler |
| ReadTimeout | 读整个请求(含 body)的最长时间 |
| WriteTimeout | 写响应的最长时间 |
| IdleTimeout | HTTP/1.1 keep-alive 空闲连接超时 |
4.3 致命易错点
server := http.Server{
Addr: ":8080",
Handler: myMux,
ReadTimeout: 2 * time.Second,
}
// 错:这样 ListenAndServe 不会用到上面 server 的配置!
http.ListenAndServe(":8080", nil)
// 对:必须调实例方法
server.ListenAndServe()
规则: 配了 http.Server{…} 就必须 server.ListenAndServe(),否则自定义 Handler、超时 全部不生效。
书城完整版为简洁直接用:
http.ListenAndServe(":8080", nil)
五、HTTP 报文解剖(带书城登录实例)
5.1 请求报文(浏览器 → 服务器)
以登录 POST 为例:
POST /login HTTP/1.1 ← 请求行:方法 + 路径 + 协议版本
Host: localhost:8080 ← 请求头开始
User-Agent: Mozilla/5.0 …
Accept: text/html,application/xhtml+xml,…
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
Cookie: user=abc-def-ghi ← 若之前登录过,会带上
username=admin&password=123456 ← 空行后是请求体(POST 表单)
| 请求行 | POST /login HTTP/1.1 | r.Method r.URL.Path |
| 请求头 | Host、Content-Type、Cookie… | r.Header r.Cookie("user") |
| 请求体 | username=admin&password=123456 | ParseForm 后 PostForm / PostFormValue |
GET 示例(首页翻页):
GET /main?pageNo=2&min=10&max=50 HTTP/1.1
Host: localhost:8080
参数在 URL query,不在 body;用 r.FormValue("pageNo") 或 r.URL.Query().Get("pageNo")。
5.2 响应报文(服务器 → 浏览器)
登录成功时大致如下:
HTTP/1.1 200 OK ← 状态行
Content-Type: text/html; charset=utf-8
Set-Cookie: user=c65d2a76-9447-44cc-…; Path=/; HttpOnly
Date: …
<!DOCTYPE html> ← 响应体(HTML)
<html>…</html>
| 200 | OK | 正常返回 HTML / JSON |
| 302 | Found | http.Redirect(w,r,"/main",http.StatusFound) |
| 404 | Not Found | http.NotFound(w,r) |
| 401 | Unauthorized | 改购物车数量未登录时 http.Error(…, 401) |
5.3 处理器里写响应的几种方式
// 1. 纯文本
fmt.Fprintln(w, "Hello")
w.Write([]byte("请先登录!"))
// 2. 跳转
http.Redirect(w, r, "/main", http.StatusFound)
// 3. HTML 模板(书城主流)
t := template.Must(template.ParseFiles("views/pages/user/login.html"))
t.Execute(w, data)
// 4. JSON(加购 Ajax)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "msg": "…"})
注意: 若手动 WriteHeader(404),须在 任何 body 写入之前 调用。
六、读请求参数(课 5.4 重点 + hanzong/admin 实验)
6.1 实验 HTML(课内经典)
<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>
若浏览器地址栏或 form 的 action 同时带 query:
http://localhost:8080/getBody?username=admin
则 同一次 POST 里:
- URL 上有 username=admin
- 表单 body 里有 username=hanzong&password=666666
6.2 四种容器 API
① r.Form — URL + 表单 body 都要
r.ParseForm() // 必须先解析!
fmt.Fprintln(w, r.Form)
// 输出类似:map[username:[hanzong admin] password:[666666]]
顺序规则: 同一 key 多个值时,表单在前,URL query 在后(slice 里 hanzong 在 admin 前面)。
内存示意:
r.Form["username"] → []string{"hanzong", "admin"}
↑ PostForm 来的 ↑ URL 来的
② r.PostForm — 只要 POST body 里的表单
r.ParseForm()
fmt.Fprintln(w, r.PostForm)
// map[username:[hanzong] password:[666666]] ← 没有 admin
③ r.FormValue("username") — 快捷取「第一个值」
// 内部会自动 ParseForm
v := r.FormValue("username") // "hanzong"(表单优先于 URL 的第一个)
④ r.PostFormValue("username") — 只认 body
v := r.PostFormValue("username") // "hanzong"
v := r.PostFormValue("password") // "666666"
书城登录(controller/userhandler.go):
username := r.PostFormValue("username")
password := r.PostFormValue("password")
user, _ := dao.CheckUserNameAndPassword(username, password)
为什么登录用 PostFormValue?密码只应从 POST body 来,避免有人构造 GET /login?password=xxx 混进参数。
6.3 对比总表
| ParseForm() | 前提 | — | error | 用 Form/PostForm 前必调 |
| r.Form | URL + body | 否 | map[string][]string | 调试、多值 |
| r.PostForm | 仅 body | 否 | map[string][]string | 区分 URL/表单 |
| FormValue(k) | URL + body | 是 | 第一个 string | 加购 bookId、删书 id |
| PostFormValue(k) | 仅 body | 是 | string | 登录、注册 |
6.4 Content-Type 限制
上述 Form / PostForm 默认针对:
Content-Type: application/x-www-form-urlencoded
上传文件 时是 multipart/form-data,需:
r.ParseMultipartForm(maxMemory)
r.MultipartForm.File["myfile"]
书城课阶段几乎都是 urlencoded,知道即可。
6.5 Ajax 与表单的一致性
注册页 Ajax 校验(/checkUserName):
$.post("/checkUserName", {"username": username}, function(data){ … });
后端:
username := r.PostFormValue("username")
jQuery 的 $.post 会把对象编成 POST body,和 form 的 name="username" 本质相同。
加购(views/index.html):
$.post("/addBook2Cart", {"bookId": bookId}, …);
后端:
bookID := r.FormValue("bookId")
这里用 FormValue 也行,因为 Ajax 只有 body、没有冲突 URL 参数。
七、静态资源 FileServer(书城 main.go)
7.1 注册代码
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("views/static"))))
http.Handle("/pages/", http.StripPrefix("/pages/", http.FileServer(http.Dir("views/pages"))))
7.2 请求如何映射到磁盘
| /static/css/style.css | css/style.css | views/static/css/style.css |
| /pages/user/login.html | user/login.html | views/pages/user/login.html |
7.3 逐层理解
http.FileServer(http.Dir("views/static"))
→ 实现了 Handler,按 URL 路径读本地文件
http.StripPrefix("/static/", …)
→ 去掉 URL 前缀,否则会把 /static/ 也当成目录名
http.Handle("/static/", …)
→ 挂到 DefaultServeMux
7.4 login.html 如何引用静态资源
<link rel="stylesheet" href="/static/css/style.css">
<script src="/static/script/jquery-1.7.2.js"></script>
浏览器会 再发 GET 请求 拉 css/js,由 FileServer 处理,不经过 controller。
7.5 易错点
| css 404 | go run 工作目录不在项目根,views/static 找不到 |
| 路径多/少一层 | StripPrefix 与 Handle 前缀不一致 |
| 动态页也想走 FileServer | 带 {{.}} 的模板必须用 template.Execute,不能当纯静态 |
八、action 如何连到 handler(前后端闭环)
8.1 三步对应关系
HTML form action="/login"
↕ 路径字符串必须一致
main.go http.HandleFunc("/login", controller.Login)
↕ 函数名
controller/userhandler.go func Login(w,r) { … }
8.2 登录完整闭环
前端(views/pages/user/login.html):
<form action="/login" method="POST">
<input name="username" … />
<input name="password" type="password" … />
<input type="submit" value="登录" />
</form>
路由(main.go):
http.HandleFunc("/login", controller.Login)
后端(controller/userhandler.go):
func Login(w http.ResponseWriter, r *http.Request) {
username := r.PostFormValue("username") // 对应 name="username"
password := r.PostFormValue("password") // 对应 name="password"
// …
}
8.3 对应关系表
| action="/login" | HandleFunc("/login", …) |
| method="POST" | 参数在 body → PostFormValue |
| name="username" | PostFormValue("username") |
| 点击 submit | 浏览器发 HTTP POST,服务器调一次 Login |
关键概念: 表单 HTML 不是 请求体;用户点提交后,浏览器 根据 action/method/name 打包 成 HTTP 请求体。
8.4 与 Ajax 路径一致
$.post("/addBook2Cart", {"bookId": bookId}, …);
http.HandleFunc("/addBook2Cart", controller.AddBook2Cart)
/addBook2Cart 字符串三处(JS、main、注释)必须一致,差一个字母就 404。
九、常见易错点
| 1 | HandleFunc("/login", Login()) 加了括号 | 编译错误或逻辑错乱 | 传 Login 不是 Login() |
| 2 | 用 Form 却忘记 ParseForm | 空 map | 或用 FormValue 自动 Parse |
| 3 | 登录用 FormValue 被 URL 参数污染 | 安全/逻辑问题 | 敏感字段用 PostFormValue |
| 4 | ListenAndServe 与 http.Server 混用 | 超时不生效 | 二选一,配 Server 就 server.ListenAndServe() |
| 5 | DefaultServeMux 与自定义 mux 混注册 | 部分路由 404 | 只用一个表 |
| 6 | FileServer 忘记 StripPrefix | 404 | 前缀成对写 |
| 7 | action 写成 /Login 大小写 | 404 | Go 路径大小写敏感 |
| 8 | 在 Write 之后 SetHeader | 头无效 | 先 Header 再 Write |
十、Mini FAQ
Q1:HandleFunc 和 Handle 选哪个? A:业务逻辑用 HandleFunc;标准库提供的 FileServer 等已是 Handler,用 Handle。
Q2:为什么 ListenAndServe 第二个参数是 nil? A:nil 表示使用 DefaultServeMux,与前面 http.HandleFunc 配套。
Q3:GET 和 POST 参数都用 FormValue 行吗? A:可以,GET 的 query 和 POST 的 body 都能取;但登录密码请用 PostFormValue。
Q4:r.Form["username"][0] 和 FormValue 区别? A:FormValue 取第一个;Form 暴露全部值,适合 debug。
Q5:处理器里能读几次 body 吗? A:body 是流,一般只能读一次;ParseForm 会读并缓存到 Form/PostForm。
Q6:端口被占用怎么办? A:改 :8080 为 :8081,或关闭占用 8080 的进程。
网硕互联帮助中心




评论前必须登录!
注册