写 Go 服务的时候,业务代码只是一部分。数据存在哪里,配置怎么读,出问题之后去哪找日志,这些也得提前想好。这里把这些基础组件放到一条请求里看,方便后面做项目时对照。
基础设施是什么?
简单理解,就是业务背后那些反复会用到的支撑能力。比如做一个任务清单,创建任务是业务;保存任务的数据库、登录状态、日志、部署和备份,则是在支撑业务运行。
基础设施和微服务框架也不是一回事。Kratos、go-zero 主要组织服务代码和接入组件;MySQL、Redis、消息队列是独立的系统。用了框架,还是得理解这些组件各自负责什么。
一个请求会经过哪些地方?
以“新增任务”为例:
浏览器 → HTTP 接口 → 参数校验 → 业务逻辑 → 数据库 → 响应
↓
日志、指标、链路记录
最小版本可以只有一个 Go 进程和一个数据库。先把参数校验、错误返回、超时和数据保存做对。缓存、队列、服务发现,要等出现明确需求再接。
常见组件怎么分工?
| 组件 | 解决什么问题 | 接入时先看什么 |
|---|---|---|
| 配置 | 区分本地、测试和生产环境 | 默认值、必填项、敏感配置来源 |
| 数据库 | 保存需要长期保留的数据 | 约束、索引、事务、连接池 |
| 缓存 | 减少重复查询,保存可丢失的临时结果 | 过期时间、失效时机、数据一致性 |
| 消息队列 | 把较慢的工作从请求中拆出去 | 消费重试、重复消息、积压 |
| 服务发现 | 根据服务名找到可用实例 | 健康状态、地址变化、调用超时 |
| 可观测性 | 看清服务是否正常,以及哪里出了问题 | 日志、指标、链路之间的关联 |
数据库:先把正确性做出来
任务标题不能是空的,任务 ID 不能重复,这类约束尽量也放到数据库里。一次业务操作涉及多次写入时,要考虑能否用事务保持一致。MySQL 的事务与锁可以从 InnoDB 文档开始看。
另外,数据库连接不是开得越多越好。连接池上限需要结合数据库容量和请求耗时设置,查询也要有超时。Go 中可以把请求的 context.Context 传给 QueryContext、ExecContext,具体用法见 取消数据库操作。
缓存:别把它当成第二份随便写的数据库
例如任务详情采用“先读缓存,未命中再查数据库”的方式,那么更新任务之后,旧缓存就要处理。先把缓存失效的规则写清楚,再考虑性能。
Redis 的 Go 客户端可以看 go-redis 官方用法。客户端应按应用生命周期复用,别每个 HTTP 请求都重新创建一个。
消息队列:异步以后,问题并没有消失
发通知、生成报告之类的工作,可以先记录任务,再交给后台消费。这里最容易漏掉的是重复执行:同一条消息因为重试被消费两次,业务还能不能正确?一般需要业务 ID、状态约束或其他幂等措施。
如果只是一个低频的小工具,同步执行或者一个简单的后台任务可能就够了。多引入一个组件,也就多一份部署、监控和故障处理工作。
日志、指标和链路,各看什么?
- 日志:这次请求发生了什么,错误在哪里。
- 指标:最近错误率有没有上升,耗时和队列长度有没有异常。
- 链路:一次请求跨了哪些调用,时间主要花在哪里。
这几类信息可以用 request ID 或 trace ID 联系起来。它们的用途可以参考 OpenTelemetry 的信号说明。日志里记录必要的上下文,不要直接把密码、令牌或完整用户数据打出来。
在项目里怎么组织?
一个小项目可以先这样分:
cmd/server/ 程序入口,组装依赖,启动和退出
internal/service/ 接口层,转换请求和响应
internal/biz/ 业务规则
internal/data/ 数据库、缓存等访问实现
configs/ 可以公开的配置示例
这是一种组织方式,不是所有项目都必须照着建。只有一个简单接口时,也不需要为每个函数都造一层接口。重点是业务规则不要被某个数据库客户端绑死,资源的创建和关闭也要能找到。
上线之前过一遍
我更倾向于按实际请求去检查:错误输入有没有被拦住,数据库慢下来之后有没有超时,程序退出时有没有停止接收请求,备份能不能恢复,出了问题能不能从日志定位。
组件名字认识得再多,如果这些没有做,服务还是不稳。先把一个小服务做完整,再逐步加组件,更容易理解每一步为什么要这么做。