资讯动态

Go 后端开发实战(3):用标准库写 HTTP 服务

发布时间:2026/9/2 17:40:39 来源:尧图企业网站定制
上一篇用 context 管理并发任务的取消。本篇把它落到网络入口只用net/http构造可校验输入、有超时、能优雅关闭的服务。理解标准库的请求生命周期后下一篇选择框架时才知道框架究竟替我们做了什么。一、从 Handler 契约理解请求生命周期HTTP 服务的核心接口只有ServeHTTP(ResponseWriter, *Request)。http.HandlerFunc让普通函数满足它ServeMux根据方法和路径分发。Go 1.22 起模式可以写成GET /users/{id}并用r.PathValue(id)读取路径参数。路由应把资源名放在路径、筛选放在查询串动词由 HTTP 方法表达。请求体是流读取后不会自动回到开头。先用http.MaxBytesReader限制大小再用 JSON Decoder 解析开启DisallowUnknownFields能尽早发现客户端拼错字段。一次成功 Decode 后还要确认流已结束否则{} {}可能被当成合法请求。内容类型、长度和业务字段是不同层次的校验应分别返回稳定错误码。响应头必须在WriteHeader或首次Write前设置。统一 JSON 错误结构客户端才能根据机器可读 code 分支而不是解析中文 message。生产环境不要把数据库错误原文返回客户端它可能暴露表名和内部地址服务端日志保留包装后的错误链对外映射成有限类别。以下程序使用httptest在内存中发请求代码独立可执行同时避免示例占用真实端口。packagemainimport(encoding/jsonfmtnet/httpnet/http/httpteststrings)typeCreateUserstruct{Namestringjson:name}funcusers(w http.ResponseWriter,r*http.Request){w.Header().Set(Content-Type,application/json)r.Bodyhttp.MaxBytesReader(w,r.Body,1024)decoder:json.NewDecoder(r.Body)decoder.DisallowUnknownFields()varinput CreateUseriferr:decoder.Decode(input);err!nil||strings.TrimSpace(input.Name){w.WriteHeader(http.StatusBadRequest)json.NewEncoder(w).Encode(map[string]string{code:invalid_input})return}w.WriteHeader(http.StatusCreated)json.NewEncoder(w).Encode(map[string]any{id:1,name:input.Name})}funcmain(){mux:http.NewServeMux()mux.HandleFunc(POST /users,users)request:httptest.NewRequest(http.MethodPost,/users,strings.NewReader({name:Lin}))response:httptest.NewRecorder()mux.ServeHTTP(response,request)fmt.Printf(status%d type%s body%s,response.Code,response.Header().Get(Content-Type),response.Body.String())}运行输出status201 typeapplication/json body{id:1,name:Lin}二、超时、连接与优雅关闭必须一起设计不要直接使用http.ListenAndServe的默认配置启动公网服务因为默认 Server 没有显式读写超时。ReadHeaderTimeout限制慢速请求头ReadTimeout限制读取整体请求WriteTimeout约束响应写入IdleTimeout管理 keep-alive 空闲连接。具体值要结合上传、流式响应和代理超时选择对流式接口设置过短 WriteTimeout 会误杀正常连接。客户端同样需要超时。默认http.Client没有整体 deadline复制创建大量 Transport 又会失去连接复用。进程内共享配置好的 Client/Transport为单次业务调用再用 context 设置更短截止时间。读完并关闭响应体连接才有机会复用限制错误响应体大小防止下游返回无限数据。关停时Server.Shutdown(ctx)停止接受新连接并等待活跃请求完成等待时间由 context 限制。它不会自动停止自建后台 goroutine后者需要上一篇的取消和 WaitGroup。ListenAndServe在正常 Shutdown 后返回http.ErrServerClosed不应记录成故障。容器平台还应先让 readiness 失败留出负载均衡摘流时间再发起 Shutdown。下面示例建立真实的本地服务等待一个请求成功后执行优雅关闭。端口使用:0由系统分配避免冲突。packagemainimport(contextfmtionetnet/httptime)funcmain(){mux:http.NewServeMux()mux.HandleFunc(GET /healthz,func(w http.ResponseWriter,r*http.Request){w.WriteHeader(http.StatusOK)w.Write([]byte(ok))})server:http.Server{Handler:mux,ReadHeaderTimeout:2*time.Second,IdleTimeout:30*time.Second,}listener,err:net.Listen(tcp,127.0.0.1:0)iferr!nil{panic(err)}done:make(chanerror,1)gofunc(){done-server.Serve(listener)}()client:http.Client{Timeout:time.Second}response,err:client.Get(http://listener.Addr().String()/healthz)iferr!nil{panic(err)}body,_:io.ReadAll(response.Body)response.Body.Close()fmt.Printf(health%s status%d\n,body,response.StatusCode)ctx,cancel:context.WithTimeout(context.Background(),time.Second)defercancel()iferr:server.Shutdown(ctx);err!nil{panic(err)}err-done fmt.Printf(closed%t\n,errhttp.ErrServerClosed)}运行输出healthok status200 closedtrue三、把协议细节变成稳定边界中间件本质是接收 Handler 并返回 Handler 的函数。恢复 panic、请求 ID、访问日志和认证都可以组合但顺序会改变语义恢复器要包在足够外层请求 ID 要在日志之前认证失败也应被访问日志记录。不要在中间件中偷偷读取并耗尽请求体也不要把业务实体塞进无类型 context value。健康检查分两类liveness 只回答进程是否需要重启readiness 表示当前实例能否接流。把数据库短暂抖动直接变成 liveness 失败会制造重启风暴readiness 可以检查关键依赖但要有短超时和缓存策略。指标端点与 pprof 不应直接暴露公网。协议层还要处理幂等性。GET、PUT、DELETE 的语义与 POST 不同但网络重试仍可能重复到达。创建支付或任务时可接收幂等键在数据库中以唯一约束保存键与结果仅在内存 map 去重无法跨实例也无法承受重启。状态码要准确创建用 201异步受理用 202无响应体成功可用 204输入错误与服务故障不能都返回 200。标准库已经足以构建清晰、可靠的小服务。选择第三方框架的理由应是团队生产率、生态中间件或绑定能力而不是回避 HTTP 基础。下一篇会用 Gin 与 Echo 风格的关键能力做取舍并保持业务层不被框架类型污染。参考来源Go 官方文档net/httpGo 博客HTTP Server 路由增强OWASP REST Security Cheat SheetMDNHTTP 状态码 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 本文属于《Go 后端开发实战》系列持续更新关注不迷路。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价