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 逐行解释行代码含义1package main可执行程序必须是main包4~5import net/http标准库 HTTP 包路由、服务器、Cookie 都在这7~9func hello(w, r)处理器函数处理一次请求固定签名两个参数8fmt.Fprintln(w, Hello World)往响应体写文本w实现了io.Writer12http.HandleFunc(/hello, hello)登记路由路径 → 函数注意传的是函数名不是hello()13http.ListenAndServe(:8080, nil)阻塞监听 8080nil 使用默认路由表1.3 两个核心类型http.ResponseWriter常名w— 代表「这次请求的响应」写状态码默认 200写响应头w.Header().Set(...)写响应体fmt.Fprintln(w,...)、w.Write([]byte(...))http.Request常名r— 代表「浏览器发来的这次请求」URL、MethodGET/POST请求头、Cookie请求体POST 表单、JSON 等1.4 处理器函数 课里的「处理请求的函数」书城项目里每一个controller.Login、controller.AddBook2Cart都是这种签名func Login(w http.ResponseWriter, r *http.Request) { ... }一次 HTTP 请求 → 调用一次处理器函数。二、三种挂法HandleFunc、Handle、ServeHTTP2.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 FileServerFileServer 本身实现了 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 → 用 DefaultServeMuxGo 在net/http包里预置了全局变量DefaultServeMuxhttp.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注册的能命中HandleFuncListenAndServe(..., 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?x1仍匹配根路径/在书城用匿名函数单独处理避免抢走所有 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:8080Handler根处理器通常是nil、*ServeMux或自定义HandlerReadTimeout读整个请求含 body的最长时间WriteTimeout写响应的最长时间IdleTimeoutHTTP/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/xhtmlxml,... Content-Type: application/x-www-form-urlencoded Content-Length: 32 Cookie: userabc-def-ghi ← 若之前登录过会带上 usernameadminpassword123456 ← 空行后是请求体POST 表单部分内容在 Go 里怎么读请求行POST /login HTTP/1.1r.Methodr.URL.Path请求头Host、Content-Type、Cookie…r.Headerr.Cookie(user)请求体usernameadminpassword123456ParseForm后PostForm/PostFormValueGET 示例首页翻页GET /main?pageNo2min10max50 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; charsetutf-8 Set-Cookie: userc65d2a76-9447-44cc-...; Path/; HttpOnly Date: ... !DOCTYPE html ← 响应体HTML html.../html状态码含义书城用法200OK正常返回 HTML / JSON302Foundhttp.Redirect(w,r,/main,http.StatusFound)404Not Foundhttp.NotFound(w,r)401Unauthorized改购物车数量未登录时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; charsetutf-8) json.NewEncoder(w).Encode(map[string]interface{}{ok: true, msg: ...})注意若手动WriteHeader(404)须在任何 body 写入之前调用。六、读请求参数课 5.4 重点 hanzong/admin 实验6.1 实验 HTML课内经典form actionhttp://localhost:8080/getBody methodPOST 用户名input typetext nameusername valuehanzongbr/ 密码input typepassword namepassword value666666br/ input typesubmit /form若浏览器地址栏或 form 的 action 同时带 queryhttp://localhost:8080/getBody?usernameadmin则同一次 POST里URL 上有usernameadmin表单 body 里有usernamehanzongpassword6666666.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)— 只认 bodyv : r.PostFormValue(username) // hanzong v : r.PostFormValue(password) // 666666书城登录controller/userhandler.gousername : r.PostFormValue(username) password : r.PostFormValue(password) user, _ : dao.CheckUserNameAndPassword(username, password)为什么登录用PostFormValue密码只应从 POST body 来避免有人构造GET /login?passwordxxx混进参数。6.3 对比总表API数据来源是否自动 Parse返回值书城场景ParseForm()前提—error用 Form/PostForm 前必调r.FormURL body否map[string][]string调试、多值r.PostForm仅 body否map[string][]string区分 URL/表单FormValue(k)URL body是第一个 string加购bookId、删书idPostFormValue(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 的nameusername本质相同。加购views/index.html$.post(/addBook2Cart, {bookId: bookId}, ...);后端bookID : r.FormValue(bookId)这里用FormValue也行因为 Ajax 只有 body、没有冲突 URL 参数。七、静态资源 FileServer书城 main.go7.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.csscss/style.cssviews/static/css/style.css/pages/user/login.htmluser/login.htmlviews/pages/user/login.html7.3 逐层理解http.FileServer(http.Dir(views/static)) → 实现了 Handler按 URL 路径读本地文件 http.StripPrefix(/static/, ...) → 去掉 URL 前缀否则会把 /static/ 也当成目录名 http.Handle(/static/, ...) → 挂到 DefaultServeMux7.4 login.html 如何引用静态资源link relstylesheet href/static/css/style.css script src/static/script/jquery-1.7.2.js/script浏览器会再发 GET 请求拉 css/js由 FileServer 处理不经过controller。7.5 易错点问题原因css 404go 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.htmlform action/login methodPOST input nameusername ... / input namepassword typepassword ... / input typesubmit value登录 / /form路由main.gohttp.HandleFunc(/login, controller.Login)后端controller/userhandler.gofunc Login(w http.ResponseWriter, r *http.Request) { username : r.PostFormValue(username) // 对应 nameusername password : r.PostFormValue(password) // 对应 namepassword // ... }8.3 对应关系表前端后端action/loginHandleFunc(/login, ...)methodPOST参数在 body →PostFormValuenameusernamePostFormValue(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。九、常见易错点#易错点后果正确做法1HandleFunc(/login, Login())加了括号编译错误或逻辑错乱传Login不是Login()2用 Form 却忘记ParseForm空 map或用FormValue自动 Parse3登录用FormValue被 URL 参数污染安全/逻辑问题敏感字段用PostFormValue4ListenAndServe与http.Server混用超时不生效二选一配 Server 就server.ListenAndServe()5DefaultServeMux 与自定义 mux 混注册部分路由 404只用一个表6FileServer 忘记 StripPrefix404前缀成对写7action 写成/Login大小写404Go 路径大小写敏感8在 Write 之后 SetHeader头无效先 Header 再 Write十、Mini FAQQ1HandleFunc和Handle选哪个A业务逻辑用HandleFunc标准库提供的FileServer等已是Handler用Handle。Q2为什么 ListenAndServe 第二个参数是 nilAnil 表示使用DefaultServeMux与前面http.HandleFunc配套。Q3GET 和 POST 参数都用 FormValue 行吗A可以GET 的 query 和 POST 的 body 都能取但登录密码请用PostFormValue。Q4r.Form[username][0]和 FormValue 区别AFormValue 取第一个Form 暴露全部值适合 debug。Q5处理器里能读几次 body 吗Abody 是流一般只能读一次ParseForm会读并缓存到 Form/PostForm。Q6端口被占用怎么办A改:8080为:8081或关闭占用 8080 的进程。