因为微服务边界清晰、接口简单、QPS不高,标准库net/http已足够满足需求,且避免了gin/echo等框架的额外抽象开销与依赖复杂度。

为什么不用 gin/echo,而选标准库封装
因为微服务边界清晰、接口简单、QPS 不高(net/http 足够支撑:注册中心对接、健康检查、请求透传、基础日志等场景,且无第三方依赖,构建体积小(
容易踩的坑:http.Server 默认不设置 ReadTimeout/WriteTimeout,长连接堆积会耗尽文件描述符;http.DefaultServeMux 是全局变量,多服务共存时路由冲突风险高——必须用独立 http.ServeMux 实例。
如何封装一个可复用的 HTTP 服务启动器
核心是把监听、路由、超时、日志、信号处理收拢到一个结构体里,避免每次写 http.ListenAndServe 硬编码。
- 定义
Server结构体,内嵌*http.Server,并持有*http.ServeMux和自定义 logger -
Server.ListenAndServe()内部调用server.Shutdown()处理 SIGTERM/SIGINT,确保 graceful shutdown - 健康检查路由统一注册为
/healthz,返回200 OK+ JSON{"status":"ok"},不走业务中间件 - 所有 handler 接收
context.Context,便于注入 traceID 或 cancel 控制
示例关键片段:
立即学习“go语言免费学习笔记(深入)”;
type Server struct {
*http.Server
mux *http.ServeMux
}
func (s *Server) RegisterHandler(pattern string, h http.Handler) {
s.mux.Handle(pattern, h)
}
func (s *Server) Run(addr string) error {
ln, err := net.Listen("tcp", addr)
if err != nil { return err }
return s.Serve(ln)
}
如何实现轻量级服务发现集成(Consul 版本)
不引入完整 SDK,只用 net/http + encoding/json 对接 Consul HTTP API,注册/注销逻辑控制在 50 行以内。
关键点:
- 注册时 POST 到
http://consul:8500/v1/agent/service/register,body 包含服务名、地址、端口、健康检查路径(如/healthz)和 TTL(建议 30s) - 使用
time.Ticker每 15s 发一次PUT /v1/agent/check/pass/service:<name></name>续租,失败三次后主动注销 - 进程退出前调用
DELETE /v1/agent/service/deregister/<id></id>,避免僵尸服务残留 - 不要在
init()中自动注册——服务可能还没完全 ready,应在Server.Run()成功监听后再触发
怎么让标准库支持结构化日志和 traceID 注入
log 包本身不支持字段,但可以用 fmt.Sprintf 拼接,或更稳妥地封装一个 Logger 接口,底层仍用 log.New。
traceID 注入靠中间件实现,注意两点:
- 从请求头(如
X-Request-ID)读取,若不存在则生成 UUID v4,通过context.WithValue透传 - 所有 handler 入口第一行应提取 context 中的 traceID,并写入日志前缀,例如:
[traceID=abc123] GET /api/user/123 - 避免在日志中直接打印
req.Header或req.URL,它们不是线程安全的;应提前拷贝req.URL.String()和req.Method
真正难的是跨 goroutine 日志上下文传递——比如你在 handler 里启了个 go func() 去发异步通知,必须显式传入带 traceID 的 context,否则日志就断了。


















