--- tags: [gRPC, Go, Server, Beginner] create time: 2026-05-13 23:15 --- # Hello World — 最小可运行 Server ## 概述 这是你搭建 gRPC 服务器的第一行代码。从零开始,用最少的步骤跑通一个能接收请求的 gRPC 服务——不需要 TLS、不需要优雅关闭、不需要拦截器。先把"能跑起来"搞定,再一步步往生产环境逼近。 > [!tip] 学习路径建议 > 本文是「Server 搭建」系列的**第一课**。后续我们会在这个最小示例上逐步叠加配置,直到达到生产可用。不要跳过——每一篇解决一个实际问题。 ## 三段式模板 不管多复杂的 gRPC 服务器,本质上都是三步: | 步骤 | 做了什么 | 类比 | |------|---------|------| | 1. 监听端口 | 告诉操作系统"我准备在这里接客了" | 餐厅挂出"营业中"牌子 | | 2. 注册服务 | 告诉 server "谁来接待客人" | 安排服务员站好位置 | | 3. 启动 Serve | 正式开始接客 | 开门迎客 | ### 生命周期时序 ```mermaid sequenceDiagram participant M as main() participant G as grpc.Server participant O as OS participant C as Client M->>O: net.Listen("tcp", ":50051") Note over O: "操作系统开始监听端口" M->>G: grpc.NewServer() M->>G: RegisterUserServiceServer() G-->>M: "服务已注册" M->>O: Serve(lis) Note over M,G: "main 阻塞在此" loop 持续监听 C->>O: "gRPC 连接请求" O->>G: "转发请求" G->>G: "路由到对应 RPC 方法" G-->>C: "返回响应" end ``` **一句话理解**:`main()` 是总指挥,先让操作系统"就位"(Listen),再给 server "派活"(Register),最后自己站在门口等活上门(Serve)。 ### 完整代码 ```go // cmd/server/main.go package main import ( "context" "log" "net" "go.uber.org/zap" // 日志库 "google.golang.org/grpc" // gRPC 框架 "google.golang.org/grpc/reflection" pb "your/proto/gen/go" // protoc 生成的代码 ) func main() { // ====== 初始化日志 ====== logged, _ := zap.NewProduction() zap.ReplaceGlobals(logged) defer logged.Sync() // ====== 第一步:监听端口 ====== lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("监听失败: %v", err) } // ====== 第二步:创建 server + 注册服务 ====== s := grpc.NewServer() // 创建空 server pb.RegisterUserServiceServer(s, &userService{}) // 注册 UserService // Reflection 方便开发时调试(生产环境关掉) reflection.Register(s) zap.L().Sugar().Infow("server 启动", "addr", lis.Addr()) // ====== 第三步:开始接客 ====== if err := s.Serve(lis); err != nil { log.Fatalf("serve 出错: %v", err) } } // userService 实现 pb.UserServiceServer interface type userService struct { // UnimplementedUserServiceServer 提供了所有方法的默认行为(返回 Unimplemented) // 新增 proto 方法时,编译器会因缺少实现而报错 → 提前发现问题 pb.UnimplementedUserServiceServer } // 你只需要实现实际需要提供的方法 func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { // ... 业务逻辑 return &pb.CreateUserResponse{Id: "123"}, nil } ``` 逐段解释: ### 第 1 段:监听端口 ```go lis, err := net.Listen("tcp", ":50051") ``` `net.Listen` 让操作系统在 `50051` 端口等待连接。**注意这里还没有启动任何 gRPC 逻辑**——它只是一个普通的 TCP 监听器。gRPC 框架只是在 TCP 之上加了一层协议而已。 > [!warning] 常见坑:端口冲突 > 如果端口已被占用,`Listen` 会立即返回错误。开发时可以选 `90051`、`150051` 这种高端口避开系统服务;生产环境则通过配置管理端口号。 > [!tip] 进阶:网络地址格式 > `":50051"` 等价于 `"0.0.0.0:50051"`(监听所有网卡)。如果只暴露给本机调试,可以改为 `"127.0.0.1:50051"`;如果需要 IPv6,用 `"[::]:50051"`。 这个值后续会抽取到配置文件里,不再硬编码。 ### 第 2 段:创建 server 并注册 ```go s := grpc.NewServer() pb.RegisterUserServiceServer(s, &userService{}) ``` - `grpc.NewServer()` 创建一个**空的** gRPC server——它什么都不做,只是容器。它此时没有 TLS、没有拦截器、没有任何中间件配置。 - `RegisterUserServiceServer` 把你的实现塞进去,告诉 server 哪些 RPC 方法可以响应。 > [!question] 一个 server 能注册多个 service 吗? > 当然可以。proto 文件里的每个 `service` 都会生成一个对应的 `RegisterXxxServer` 函数,全部挂到同一个 server 上即可。这正是微服务中"多 proto 共享端口"的基础。 后续会通过 `grpc.NewServer(opts ...grpc.ServerOption)` 传入各种选项来增强这个 server(TLS、流控、keepalive……),目前先用零配置的默认版本。 ### 第 3 段:启动 Serve ```go s.Serve(lis) ``` 这一步之后,main 函数会**阻塞在这里**,持续监听并处理请求——直到进程被杀死或遇到错误。 这意味着:**Serve 之后的代码永远不会执行**。需要在这之前完成所有初始化工作(日志、数据库连接、协程启动等)。这也是为什么大多数项目最终会把 `main()` 封装成一个更结构化的启动函数——纯裸跑只适合入门示例。 ## 为什么需要 UnimplementedXxxServer? 很多新手会疑惑:为什么要嵌入一个 `UnimplementedUserServiceServer`? 原因很简单:**Proto 新增方法时,编译期会自动提醒你。** 假设你的 proto 文件新增了一个 `DeleteUser` 方法: ```proto service UserService { rpc CreateUser(...) returns (...); rpc GetUser(...) returns (...); rpc DeleteUser(...) returns (...); // ← 新加的 } ``` 重新 `protoc` 后,如果你没有嵌入 `UnimplementedUserServiceServer`,编译器会报错: ``` cannot use &userService{} as UserServiceServer: missing method DeleteUser in receiver type userService ``` 从"线上出 Bug 才发现问题"变成了"提交代码前就发现"——把风险挡在编译期。 > [!question] 那如果我不嵌入会怎样? > 你可以手动实现所有方法,但每次 proto 变动都要手动检查一遍有没有遗漏。人总会忘,编译器不会。 详细的零-Stub 模式机制参见 [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]]。 ## 运行验证 启动 server 后,用 `grpcurl` 快速验证是否工作: ```bash # 列出可用的服务和方法 grpcurl -plaintext localhost:50051 list # 查看某个 service 的详细接口定义 grpcurl -plaintext localhost:50051 describe UserService ``` 如果看到你的 `UserService` 和相关方法,说明一切正常。 > [!note] 这里的 `-plaintext` 表示不使用 TLS。生产环境必须加上 TLS,那是后面的话题。 ## 下一步 现在你有了一个能跑的 server,但离生产还有距离: - 没有限制消息大小 → 大请求可能撑爆内存 —→ [[hhs/gRPC/3. 服务端实现/02-ServerOption 生产级配置速查]] - 没有 TLS → 数据明文传输 —→ [[hhs/gRPC/3. 服务端实现/02-ServerOption 生产级配置速查]] - 没有 keepalive → 中间的负载均衡器可能切断空闲连接 —→ [[hhs/gRPC/3. 服务端实现/02-ServerOption 生产级配置速查]] - 没有优雅关闭 → 直接 kill 会导致正在处理的请求丢失 —→ [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭]] 接下来的文章会逐个解决这些问题。 ## 关联笔记 - [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]] - [[hhs/gRPC/3. 服务端实现/02-ServerOption 生产级配置速查]] - [[hhs/gRPC/2. gRPC 核心篇/02-gRPC 核心术语]]