Go 后端面试复盘:语言、并发与服务实践 · 23. 工程化与项目设计至24. 微服务、Kratos 与 gRPC
前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。
23. 工程化与项目设计
23.1 Go 项目目录应该怎么设计?
没有唯一标准。常见思路:
text
project/
├── api/ # Proto/OpenAPI 等协议
├── cmd/
│ └── server/ # 程序入口
├── configs/ # 配置模板
├── internal/
│ ├── biz/ # 领域/业务逻辑
│ ├── data/ # 数据访问实现
│ ├── service/ # transport service 实现
│ ├── server/ # HTTP/gRPC server 组装
│ └── conf/ # 配置生成代码
├── pkg/ # 确实要供外部项目复用的包
├── test/ # 集成测试资源
├── go.mod
└── Makefile原则:
- 按变化原因和依赖方向组织。
internal防止外部包误用。- 不要创建没有边界价值的 util 大杂烩。
- 避免过早拆成大量小包造成循环依赖和导航困难。
23.2 package 设计原则是什么?
- 名称简短、明确,避免
util、common、base。 - 包围绕单一概念组织。
- API 尽量小。
- 减少全局状态。
- 避免 import cycle。Modules 的
require声明模块依赖,不会自动拆解包循环导入;调整职责与依赖方向,见模块实践。 - 错误和接口放在最能表达所有权的位置。
23.3 Go Modules 解决什么?
- 模块版本管理。
- 最小版本选择。
- 校验依赖内容。
- 可重复构建。
- 支持 replace、exclude 等开发能力。
常用命令:
bash
go mod init example.com/project
go mod tidy
go mod download
go list -m all
go mod graph
go mod why module/pathgo mod tidy 会根据所有构建标签相关包和测试依赖调整 go.mod/go.sum,提交前应检查 diff。
23.4 go.sum 是什么?
记录模块内容和 go.mod 文件的哈希,用于验证下载内容的一致性。它不是“当前真正使用所有依赖”的简单列表,也不应随意删除。
23.5 依赖注入有哪些方式?
- 构造函数手动注入:最直观,推荐默认选择。
- 代码生成式依赖注入:大型项目减少装配代码。
- 运行时容器:灵活但类型和调用链可能更隐晦。
go
func NewUserService(repo UserRepo, logger *slog.Logger) *UserService {
return &UserService{repo: repo, logger: logger}
}避免服务内部随意读取全局单例,这会降低测试性。
23.6 配置管理要注意什么?
- 配置与代码分离。
- 密钥不要进入仓库或普通日志。
- 启动时校验必填字段和范围。
- 明确默认值。
- 动态配置使用不可变快照和原子发布。
- 配置变更应有版本、审计和回滚。
- 不要在业务热路径反复解析配置。
23.7 日志应该如何设计?
结构化字段示例:
- timestamp。
- level。
- service、instance、environment。
- trace_id、request_id。
- operation。
- user_id(注意隐私)。
- duration。
- error code。
原则:
- 不记录密码、token、完整银行卡号等。
- 错误尽量在边界记录一次。
- 高频路径避免打印巨大对象。
- 使用日志采样和级别控制。
- 日志不是指标的替代品。
23.8 API 版本如何设计?
- 兼容新增字段。
- 不改变已有字段语义。
- 删除字段先弃用,再经过迁移期。
- 重大不兼容变化使用新版本。
- 服务内部与外部 API 版本策略可以不同。
- 数据库字段、领域模型和 API 模型不要强绑定。
23.9 如何做优雅启动和关闭?
启动:
- 加载并校验配置。
- 初始化日志、指标、追踪。
- 连接必要依赖。
- 注册服务。
- 启动监听。
- 上报 readiness。
关闭:
- 关闭 readiness/服务发现。
- 停止接收新请求。
- 等待在途请求。
- 停止后台任务。
- flush 数据、日志和 trace。
- 关闭连接。
- 在总超时内退出。
23.10 什么是 readiness 和 liveness?
- readiness:实例是否准备好接收流量。依赖未就绪、正在关闭时应失败。
- liveness:进程是否需要被重启。不要因短暂下游故障就让 liveness 失败,否则可能引发重启风暴。
24. 微服务、Kratos 与 gRPC
24.1 微服务的优缺点是什么?
优点:
- 独立部署和扩缩容。
- 团队边界清晰。
- 故障隔离潜力。
- 可按业务选择技术和资源。
代价:
- 网络不可靠。
- 分布式事务和数据一致性复杂。
- 部署、监控、追踪和治理成本高。
- 接口演进与跨团队协调复杂。
- 本地调试和测试链路更长。
不是系统越复杂越应该拆;合理的模块化单体往往是更好的起点。
24.2 Kratos 常见分层如何理解?
常见结构:
service:实现由 Proto 生成的服务接口,负责传输层参数转换。biz:业务用例、领域逻辑和仓储接口。data:仓储接口实现,访问 DB、Redis、外部服务。server:组装 HTTP/gRPC server 和 middleware。conf:配置结构。
依赖方向大致为:
查看图表代码
关键是 biz 不依赖具体数据库实现,data 实现由 biz 定义的行为接口。
24.3 Proto 字段设计要注意什么?
- 字段号发布后不要复用。
- enum 0 值用
UNSPECIFIED。 - 时间优先使用明确时间类型或约定。
- 金额不要使用 float,使用最小货币单位整数或 decimal 表达。
- ID 类型跨前端时注意 int64 精度。
- 列表接口定义分页信息。
- 错误通过统一错误模型表达。
- 不要把所有字段都设计成 string。
24.4 gRPC 的四种调用模式是什么?
- Unary:一请求一响应。
- Server Streaming:一请求多响应。
- Client Streaming:多请求一响应。
- Bidirectional Streaming:双向流。
流式调用需要更认真处理:
- 背压。
- 心跳和超时。
- 半关闭。
- 并发读写约束。
- 消息大小。
- 重连后的业务语义。
24.5 gRPC interceptor 有什么用途?
类似 HTTP middleware,可统一处理:
- 日志。
- tracing。
- metrics。
- 认证鉴权。
- 限流。
- panic recover。
- 超时。
- 错误转换。
顺序很重要。例如 recover 应覆盖后续链路,trace ID 应在日志前注入。
24.6 gRPC deadline 为什么重要?
没有 deadline 的调用可能无限等待并占用连接、goroutine 和下游资源。服务应:
- 客户端传 deadline。
- 服务端把 context 继续传给 DB 和下游。
- 为不同操作设置符合 SLO 的预算。
- 保留上游处理和网络返回余量。
不要每一层都机械设置相同固定超时,可能导致总链路预算失控。
24.7 重试应该放在哪一层?
通常由最了解调用语义的一层控制。客户端超时时服务端可能已经成功,重试非幂等操作会重复扣款、创建订单或发奖。要求:
- 操作幂等,或有幂等键。
- 仅重试明确的瞬时错误。
- 指数退避加随机抖动。
- 设置最大次数和总时间预算。
- 避免多层同时重试产生乘法放大。
24.8 服务发现和负载均衡是什么关系?
- 服务发现:找到当前可用实例地址。
- 负载均衡:从地址集合中选择一个实例。
客户端需要处理实例增删、健康状态、连接复用和调用失败。常见算法包括轮询、加权、最少请求、一致性哈希等。
24.9 Kratos middleware 顺序怎么设计?
示意顺序:
- recovery。
- request ID / tracing。
- logging。
- metrics。
- authentication。
- authorization。
- rate limit。
- validation。
- business handler。
没有绝对固定顺序。例如希望记录鉴权失败就必须让 logging/metrics 覆盖鉴权中间件。应明确每层输入、输出和异常是否能被外层观察。
24.10 gRPC 与 HTTP 如何共同维护一套接口?
使用 Proto 作为接口源:
- 定义 request/response 和 RPC。
- 通过 HTTP annotation 映射 REST 路由。
- 生成 gRPC 和 HTTP 相关代码。
- service 实现一套业务入口。
- 内部 transport 共享 biz 层。
要避免让 HTTP 特有语义污染领域层,也要对错误码、分页和字段 presence 做统一约定。
继续阅读
- 排错
- 排错
- 排错