云计算百科
云计算领域专业知识百科平台

01. 搭服务器:处理器、路由表、读请求参数(细讲)

🥰个人主页:会编程的土豆(欢迎来访) 💎作者简介:后端学习者 ❄️个人专栏:数据结构与算法,数据库,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) 注册。

对比表:

API注册目标典型场景
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 表单)

部分内容在 Go 里怎么读
请求行 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 对比总表

API数据来源是否自动 Parse返回值书城场景
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 请求如何映射到磁盘

浏览器请求StripPrefix 后磁盘路径
/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 的进程。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 01. 搭服务器:处理器、路由表、读请求参数(细讲)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!