先给结论
端口写在配置文件里,Start() 调底层 Go HTTP Server 监听
go-zero 的 HTTP 服务 = rest.MustNewServer(c.RestConf) 创建 + server.Start() 监听端口。端口不写死在代码里,而是来自 etc/*.yaml 的 Host / Port 字段,经 config.go 的 rest.RestConf 读进程序。
记得住的一句话:端口写在 etc/user-api.yaml 的 Port 字段;main.go 里 rest.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.Host、
c.Port 直接可用。
那 rest.RestConf 里到底有哪些字段?下面是常被用到的一组(具体字段随版本略有差异,以源码 rest.RestConf 为准):
| 字段 | 含义 | 说明 |
Host | 监听地址 | 0.0.0.0 表示监听所有网卡;127.0.0.1 仅本机 |
Port | 监听端口 | HTTP 服务真正监听的端口,如 8888 |
CertFile / KeyFile | HTTPS 证书 | 配了就走 TLS(ListenAndServeTLS),否则明文 HTTP |
Verbose | 详细日志 | 打出路由表、慢请求等 |
MaxConns | 最大连接数 | 超过则限流,保护服务 |
MaxBytes | 请求体上限 | 防止超大 body 打爆内存 |
Name / Mode | 服务名 / 运行模式 | 来自内嵌的 service.ServiceConf,影响日志与监控 |
CpuThreshold | CPU 限流阈值 | 来自 service.ServiceConf,超过则开启自适应熔断( shedding) |
踩坑提醒:端口不要硬编码在代码里。多环境(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.RestConf;rest.MustNewServer(c.RestConf) 把这个配置交给框架。到这一步还没真正监听,只是「准备好了」。注意 MustNewServer 还支持 rest.WithJwt / rest.WithMiddlewares 等选项。
🔹 ⑤ 与 ⑥:注册与监听
RegisterHandlers 把所有路由(来自 routes.go)挂到 server 上;server.Start() 才是真正监听端口、开始 Accept 连接的地方,它会阻塞当前 goroutine,直到收到关闭信号。
关键点:MustNewServer 只负责「创建 + 配置」,Start() 才负责「监听 + 服务」。Start() 之后的代码(如果有的话)不会执行,因为它一直阻塞在那。
请求链路
第四步:一次 HTTP 请求是怎么被处理的
server.Start() 监听着,浏览器发来一个请求后,go-zero 把它一路送到你的 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 取用户身份、调 logic、httpx.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.RestConf 的 Host/Port | zrpc.RpcConf 的 ListenOn(如 0.0.0.0:8080) |
| 协议 | HTTP / JSON | gRPC(基于 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 阻塞」的机制一致,差别在协议与端口配置字段。