go-zero 是怎么启动服务、监听端口、处理 HTTP 请求的?

server.Start() 之后到底发生了什么?端口写在哪?为什么浏览器能直接访问?」—— 这篇把 go-zero 的 HTTP 服务从配置里的端口,到 rest.MustNewServer 创建、Start() 监听、再到一次 HTTP 请求被层层处理的完整链路讲清。

一句话结论 端口从哪来 main 启动五步 Start 内部机制 请求处理链路 优雅关闭 rest vs zrpc 结论速记

端口写在配置文件里,Start() 调底层 Go HTTP Server 监听

go-zero 的 HTTP 服务 = rest.MustNewServer(c.RestConf) 创建 + server.Start() 监听端口。端口不写死在代码里,而是来自 etc/*.yamlHost / Port 字段,经 config.gorest.RestConf 读进程序。

记得住的一句话:端口写在 etc/user-api.yamlPort 字段;main.gorest.MustNewServer(c.RestConf) 把这个端口交给框架;server.Start() 调 Go 标准库的 net/http 监听 Host:Port,开始接 HTTP 请求。go-zero 没有自己造一个 HTTP 内核,它包裹并增强了 Go 原生 HTTP Server

和「初始化」那篇的关系:本篇专注「服务怎么起、端口怎么监听、请求怎么进来」;「连接(DB/RPC/Redis)建在哪」是另一篇 《go-zero 初始化写在哪》 的主题。两篇合起来才是完整的启动全貌。

第一步:端口来自 etc/*.yaml,不是代码

go-zero 是配置驱动的——服务监听哪个地址、哪个端口,全在 YAML 里。

📄 etc/user-api.yaml(你写的)
Name: user-api
Host: 0.0.0.0      # 监听地址
Port: 8888        # 监听端口
Mode: dev
🧱 config.go(goctl 生成的)
package config

type Config struct {
    rest.RestConf   // 内嵌!Host/Port 都在这
}
config.Config 内嵌了 rest.RestConf,所以 c.Hostc.Port 直接可用。

rest.RestConf 里到底有哪些字段?下面是常被用到的一组(具体字段随版本略有差异,以源码 rest.RestConf 为准):

字段含义说明
Host监听地址0.0.0.0 表示监听所有网卡;127.0.0.1 仅本机
Port监听端口HTTP 服务真正监听的端口,如 8888
CertFile / KeyFileHTTPS 证书配了就走 TLS(ListenAndServeTLS),否则明文 HTTP
Verbose详细日志打出路由表、慢请求等
MaxConns最大连接数超过则限流,保护服务
MaxBytes请求体上限防止超大 body 打爆内存
Name / Mode服务名 / 运行模式来自内嵌的 service.ServiceConf,影响日志与监控
CpuThresholdCPU 限流阈值来自 service.ServiceConf,超过则开启自适应熔断( shedding)
etc/user-api.yaml Port: 8888 conf.MustLoad 解析进 c.RestConf rest.MustNewServer 读 c.RestConf :8888 监听 配置 → 解析 → 框架拿到端口 → 后续 Start() 时真正绑定
踩坑提醒:端口不要硬编码在代码里。多环境(dev/test/prod)用不同的 yaml,或用 go run user.go -f etc/user-api.prod.yaml 指定配置;改端口只改 yaml,不用改代码重编。

第二步:main.go 里的五步启动序列

goctl 生成的入口(文件名通常是 user.go)已经把骨架写好了,你基本只补业务逻辑。

package main

import (
    "flag"
    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/rest"
    "your_project/user/internal/config"
    "your_project/user/internal/handler"
    "your_project/user/internal/svc"
)

var configFile = flag.String("f", "etc/user-api.yaml", "the config file")

func main() {
    flag.Parse()                                       // 解析 -f 指定的配置文件路径

    var c config.Config
    conf.MustLoad(*configFile, &c)                     // ① 读配置(含 Host/Port)

    svcCtx := svc.NewServiceContext(c)                   // ② 建依赖(DB/Redis/RPC/...)

    server := rest.MustNewServer(c.RestConf)             // ③ 创建 HTTP 服务,把端口交给框架
    defer server.Stop()                              // ④ 退出时优雅关闭

    handler.RegisterHandlers(server, svcCtx)          // ⑤ 注册所有路由

    server.Start()                                    // ⑥ 监听端口 + 接请求(阻塞)
}
🔹 ① 与 ③:端口的来与交
conf.MustLoad 把 yaml 的 Port 读进 c.RestConfrest.MustNewServer(c.RestConf) 把这个配置交给框架。到这一步还没真正监听,只是「准备好了」。注意 MustNewServer 还支持 rest.WithJwt / rest.WithMiddlewares 等选项。
🔹 ⑤ 与 ⑥:注册与监听
RegisterHandlers 把所有路由(来自 routes.go)挂到 server 上;server.Start() 才是真正监听端口、开始 Accept 连接的地方,它会阻塞当前 goroutine,直到收到关闭信号。
关键点:MustNewServer 只负责「创建 + 配置」,Start() 才负责「监听 + 服务」。Start() 之后的代码(如果有的话)不会执行,因为它一直阻塞在那。

第三步:server.Start() 内部到底做了什么

一句话:打印地址 → 用 Go 原生 net/http 监听 → 进入 Accept 循环。下面拆开看。

① 打印 "Starting server at Host:Port" os.Stdout 输出监听地址 ② net.Listen("tcp", Host:Port) 向内核申请端口,失败则启动即崩 ③ http.Serve(ln, engine) 或 ListenAndServeTLS(配了证书) ④ 阻塞:循环 Accept 连接 → 路由匹配 → 中间件链 → handler engine(go-zero 增强层) radix 路由 + 中间件链 + 指标 + 优雅关闭

核心认知:go-zero 的 HTTP 服务底层就是 Go 标准库的 net/http。它先做 net.Listen 拿到监听器,再调 http.Serve(ln, handler)(或 TLS 版)。所谓「go-zero 的 HTTP 内核」,是架在标准库之上的 engine——负责路由匹配、中间件装配、监控与优雅关闭,不是另起炉灶替换 Go HTTP Server

engine 在「监听」之外还顺手做了什么
  • 路由表:RegisterHandlers 注册的所有路由,编译进一个 radix/patricia 树,请求进来按 path+method 快速匹配。
  • 中间件链:每条路由自动套上 recover → prometheus → 限流(shedding) → 超时 → 日志 → 你自定义的中间件,顺序固定。
  • 监控端点:默认暴露 /metrics(Prometheus)、/ping 健康检查等,由 service.ServiceConf 控制。
  • 优雅关闭钩子:注册 SIGINT/SIGTERM 监听,收到信号后调用 server.Stop()

第四步:一次 HTTP 请求是怎么被处理的

server.Start() 监听着,浏览器发来一个请求后,go-zero 把它一路送到你的 logic

浏览器/客户端 GET /user/1 Listener Accept 连接 Router 匹配路由 中间件链 日志/限流/超时 Handler 薄壳 Logic 业务逻辑 WriteJson 写回响应 handler 只做「解析请求 → 调 logic → 写回 JSON」,重活在 logic
// internal/handler/userhandler.go(goctl 生成的薄壳)
func GetUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req types.GetUserReq
        if err := httpx.Parse(r, &req); err != nil { // 解析 + 校验请求
            httpx.Error(w, err)
            return
        }
        l := logic.NewGetUserLogic(r.Context(), svcCtx)  // 业务在 logic
        resp, err := l.GetUser(&req)
        if err != nil {
            httpx.Error(w, err)
        } else {
            httpx.OkJson(w, resp)                        // 写回 JSON
        }
    }
}
🧰 handler 是薄壳
只负责 httpx.Parse 解析请求、从 JWT/ctx 取用户身份、调 logichttpx.OkJson 写回。不写业务。
⚙️ logic 是主场
真正的业务逻辑在这里,通过 l.svcCtx.DB / l.svcCtx.UserRpc 用启动期建好的依赖。每个请求 new 一个 logic,共享 svc。
为什么浏览器能直接访问:因为监听的是标准 HTTP 端口、用的是标准 HTTP 协议。go-zero 的 rest 服务本质是一个 HTTP Server,任何 HTTP 客户端(浏览器、curl、前端 fetch、Postman)都能直接打 http://Host:Port/path。注意:只有 rest 服务是这样;zrpc(gRPC)服务不能直接用 HTTP/JSON 访问,见下一节。

第五步:服务怎么优雅退出

Start() 阻塞期间,进程收到 Ctrl+C / SIGTERM 时会怎样?

优雅关闭的两道保险
  • 框架内置信号监听:go-zero 在启动时会注册 SIGINT / SIGTERM 监听。收到信号后,框架触发 server.Stop()
  • defer server.Stop()你写在 main 里的这行,保证 main 退出前调用 Stop() 做收尾。
  • Stop 做了什么:调用底层 http.Server.Shutdown——停止接收连接,但会等待在途请求处理完再退出,避免「正在处理的请求被一刀切断」。
别漏了 defer server.Stop()虽然框架有信号钩子,但显式 defer 是双保险,也是 goctl 生成代码的标配。漏写可能导致进程退出时连接未释放、在途请求被中断。

rest 服务 vs zrpc 服务:端口与访问方式不同

go-zero 里「启动一个服务」有两种:对外暴露 HTTP 的 rest,和内部服务间调用的 zrpc

维度rest(API 服务)zrpc(RPC 服务)
监听配置rest.RestConfHost/Portzrpc.RpcConfListenOn(如 0.0.0.0:8080
协议HTTP / JSONgRPC(基于 HTTP/2 + Protobuf)
外部能否直接访问✅ 浏览器/curl/fetch 直接打❌ 需要 gRPC 客户端(或 grpc-gateway 转 HTTP)
服务发现通常前置网关(API 网关收口)向 etcd 注册,客户端自动发现
创建函数rest.MustNewServer(c.RestConf)zrpc.MustNewServer(c.RpcConf)
启动两者都是 .Start() 阻塞监听同左
一句话区分:对外给「人/前端」用的走 rest(HTTP,能直接访问);服务内部互相调用走 zrpc(gRPC,走 etcd 发现)。两者「启动 + 监听端口 + Start 阻塞」的机制一致,差别在协议与端口配置字段。

一张图 + 口诀记住整条链

🧠 记忆口诀

· 端口写在 etc/*.yamlHost/Port,不写死在代码;
· conf.MustLoad 解析进 c.RestConfrest.MustNewServer(c.RestConf) 把端口交给框架;
· handler.RegisterHandlers 挂路由,server.Start()真正监听(底层 net.Listen + http.Serve);
· Start() 阻塞,收到 SIGINT/SIGTERM 走 Stop() 优雅关闭(记得 defer);
· 请求进来:Listener → Router → 中间件链 → Handler(薄壳)→ Logic(业务)→ WriteJson 回去;
· rest = 标准 HTTP,浏览器能直接访问;zrpc = gRPC,内部服务间调用。

延伸阅读:「连接(DB/RPC/Redis)在哪建」看 《go-zero 初始化写在哪》;「路由/接口/handler 怎么定义」看 《go-zero API 定义全流程》;「中间件放哪、怎么写」看 《go-zero middleware 放哪》