net/http标准库:从零构建HTTP服务

📅 2026/8/15 2:02:05
net/http标准库:从零构建HTTP服务
Go Web开发入门用net/http标准库从零构建HTTP服务模块三开篇摘要: 本篇从Go标准库net/http出发演示如何用http.HandleFunc注册路由、处理请求参数、返回JSON响应、托管静态文件并实现优雅关闭。分享http.Server默认无超时配置的踩坑经历对比标准库与Web框架的适用场景开启模块三Go Web开发之旅。开篇故事模块二我们花了十几篇讲并发编程从goroutine到channel到race detector现在把目光转向Web开发。去年我接手一个内部运维平台的项目技术选型时团队争论用不用框架。我决定先从net/http标准库入手把底层搞明白再上框架。结果发现Go的标准库真的很能打一个HTTP服务核心代码就十来行。后来我用Gin的时候才真正理解了框架帮你封装了什么。如果直接学框架很多概念会一知半解。这篇我们从net/http开始搭建一个能处理路由、解析参数、返回JSON的HTTP服务。一、最小HTTP服务十行代码起步Go的net/http库开箱即用。packagemainimport(fmtnet/http)funcmain(){// 注册路由路径 / 的处理函数http.HandleFunc(/,func(w http.ResponseWriter,r*http.Request){// w 写响应r 读请求fmt.Fprintf(w,Hello, Go Web!)})// 启动服务阻塞当前goroutinefmt.Println(server started at :8080)iferr:http.ListenAndServe(:8080,nil);err!nil{fmt.Println(server error:,err)}}http.ResponseWriter是响应写入器*http.Request是请求对象。Go Web编程的本质就是读Request、写Response。二、路由注册与请求处理http.HandleFunc使用的是默认的DefaultServeMux它是一个全局的路由分发器。我们来看更完整的路由处理。packagemainimport(encoding/jsonfmtlognet/http)// User 用户数据结构// json tag 控制序列化后的字段名typeUserstruct{IDintjson:idNamestringjson:nameAgeintjson:age}// 模拟数据库varusers[]User{{ID:1,Name:张三,Age:28},{ID:2,Name:李四,Age:32},}// usersHandler 处理 /users 路由funcusersHandler(w http.ResponseWriter,r*http.Request){w.Header().Set(Content-Type,application/json)// 根据HTTP方法做不同处理switchr.Method{casehttp.MethodGet:// json.Marshal把结构体转成JSONdata,_:json.Marshal(users)w.Write(data)casehttp.MethodPost:// Decode从r.Body读取并解析JSONvaru Useriferr:json.NewDecoder(r.Body).Decode(u);err!nil{w.WriteHeader(http.StatusBadRequest)fmt.Fprintf(w,{error: invalid json})return}u.IDlen(users)1usersappend(users,u)w.WriteHeader(http.StatusCreated)json.NewEncoder(w).Encode(u)default:// 不支持的方法返回405w.WriteHeader(http.StatusMethodNotAllowed)fmt.Fprintf(w,{error: method not allowed})}}funcmain(){http.HandleFunc(/users,usersHandler)// 健康检查接口http.HandleFunc(/health,func(w http.ResponseWriter,r*http.Request){w.Header().Set(Content-Type,application/json)fmt.Fprintf(w,{status: ok})})log.Println(server started at :8080)log.Fatal(http.ListenAndServe(:8080,nil))}三、路径参数与查询参数Web开发中经常需要从URL里取参数。查询参数用r.URL.Query()路径参数需要手动解析。// 查询参数 /search?keywordgopage1funcsearchHandler(w http.ResponseWriter,r*http.Request){w.Header().Set(Content-Type,application/json)// Query() 返回 url.Values本质是 map[string][]stringquery:r.URL.Query()keyword:query.Get(keyword)// 不存在返回空字符串pageStr:query.Get(page)page,_:strconv.Atoi(pageStr)ifpage0{page1// 默认第一页}json.NewEncoder(w).Encode(map[string]any{keyword:keyword,page:page,})}// 路径参数 /users/123// 标准库没有内置路径参数需要手动TrimPrefixfuncuserHandler(w http.ResponseWriter,r*http.Request){w.Header().Set(Content-Type,application/json)// 手动从URL提取 /users/ 后面的部分path:strings.TrimPrefix(r.URL.Path,/users/)id,err:strconv.Atoi(path)iferr!nil{w.WriteHeader(http.StatusBadRequest)json.NewEncoder(w).Encode(map[string]string{error:ID必须是数字})return}json.NewEncoder(w).Encode(map[string]any{id:id})}/users和/users/的匹配规则不同。/users只匹配精确路径/users/会匹配所有以/users/开头的路径。这也是标准库路由的一个坑后来Gin用radix tree解决了这个问题。四、优雅关闭生产环境必备生产环境中服务不能说停就停。优雅关闭的核心是让服务停止接收新请求等已有请求处理完再退出。packagemainimport(contextfmtlognet/httposos/signalsyscalltime)funcmain(){// 自定义ServeMux避免全局状态污染mux:http.NewServeMux()mux.HandleFunc(/,func(w http.ResponseWriter,r*http.Request){time.Sleep(2*time.Second)// 模拟耗时操作fmt.Fprintf(w,done)})// 自定义Server配置超时生产必须做server:http.Server{Addr::8080,Handler:mux,ReadTimeout:10*time.Second,WriteTimeout:15*time.Second,IdleTimeout:60*time.Second,}// goroutine中启动服务主goroutine监听信号gofunc(){log.Println(server started at :8080)iferr:server.ListenAndServe();err!nilerr!http.ErrServerClosed{log.Fatalf(server error: %v,err)}}()// 监听CtrlC和SIGTERMsigChan:make(chanos.Signal,1)signal.Notify(sigChan,syscall.SIGINT,syscall.SIGTERM)sig:-sigChan log.Printf(收到信号 %v开始优雅关闭...\n,sig)// 给已有请求30秒处理时间ctx,cancel:context.WithTimeout(context.Background(),30*time.Second)defercancel()// Shutdown停止接收新连接等已有连接处理完iferr:server.Shutdown(ctx);err!nil{log.Printf(强制关闭: %v\n,err)}log.Println(server已停止)}五、独家踩坑http.Server默认无超时说一个我踩过的大坑。上线第一个版本的时候我直接用http.ListenAndServe(:8080, nil)没配任何超时。结果上线一周后服务CPU飙到100%排查发现是有人用慢速攻击客户端建立连接后慢慢发数据每个连接占一个goroutine最后goroutine堆积到几万个。http.ListenAndServe这个便捷函数内部创建的Server对象ReadTimeout、WriteTimeout、IdleTimeout全是零值也就是永不超时。这在开发环境没问题上了生产就是定时炸弹。// 错误写法生产环境不要用http.ListenAndServe(:8080,nil)// 正确写法自定义Server配置超时见上面优雅关闭代码server:http.Server{Addr::8080,Handler:mux,ReadTimeout:10*time.Second,WriteTimeout:15*time.Second,IdleTimeout:60*time.Second,}server.ListenAndServe()Go 1.22后net/http支持{id}路径参数和METHOD限制但参数绑定、中间件仍需自己写。这就引出了下一篇的Gin框架。六、对比分析与总结维度net/http标准库Gin等框架路由基础匹配1.22后增强radix tree参数路由参数绑定手动解析自动绑定结构体中间件需自己实现内置机制JSON手动调用encoding/json内置序列化适用场景简单服务、学底层中大型Web项目net/http标准库是Go Web开发的根基。理解了Handler、ServeMux、Request和Response的工作机制再学任何Go Web框架都会很轻松。模块三正式开始了。下一篇引入Gin框架看看它如何简化路由注册、参数解析和中间件开发。