Go mod 好菜系列 - 0x17 kafka-go 这口异步队列菜别只会丢消息
详细聊 kafka-go 在 Go 项目里的常见用法、生产者和消费者各自要关注什么、它适合哪些异步场景,以及为什么“能发能收”离可用还差很远。
很多人第一次用消息队列,最容易产生一种错觉:只要消息发出去了、也收到了,这套链路就算成了。可真到了生产环境里,异步系统最麻烦的地方往往不是“能不能收发”,而是 乱序、重试、幂等、积压、消费失败之后怎么办。而在 Go 里,kafka-go 是一个很常见的入口。
它适合什么场景
- 事件通知
- 异步削峰
- 日志或埋点采集
- 服务解耦
说白了,就是那些“不一定要同步立刻做完,但必须最终被处理”的事。
为什么大家不用原生客户端,偏偏常提 kafka-go
因为它比较贴近 Go 的使用习惯,接口也更顺手。很多团队并不是在追求“最极致的吞吐 benchmark”,而是在追求一个够稳、够直觉、集成成本不高的客户端库。kafka-go 往往就卡在这个平衡点上。
生产者最容易忽略什么
不是怎么把消息发出去,而是发出去以后你怎么确认它真的值得被消费:
- 消息 key 怎么设计
- 是否需要保证同 key 有序
- 失败重试策略是什么
- 消息体有没有版本演进空间
很多系统后面越来越痛,不是 Kafka 的锅,而是消息模型一开始就写得像一封随手便签。
生产和消费的最小示例
writer := &kafka.Writer{
Addr: kafka.TCP("127.0.0.1:9092"),
Topic: "user-events",
Balancer: &kafka.Hash{},
}
err := writer.WriteMessages(context.Background(),
kafka.Message{Key: []byte("user:42"), Value: []byte(`{"event":"created"}`)},
)
if err != nil {
log.Fatal(err)
}
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"127.0.0.1:9092"},
Topic: "user-events",
GroupID: "user-worker",
})
msg, err := reader.ReadMessage(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println(string(msg.Key), string(msg.Value))这段代码很好上手,但真正要上生产时,重点会落到重试、幂等、监控和 offset 提交策略上,而不是这几行 API 本身。
消费者最容易翻车什么
- 处理失败后直接丢
- offset 提交时机不清楚
- 没有幂等,重试一次就把业务写炸
- 积压严重却没有监控和报警
真正的消费者设计,核心不是“收到了”,而是“失败时还能不能优雅地再来一次”。
一个很关键的意识:Kafka 不是任务队列的平替
它当然可以承载异步任务,但它的本质更偏事件流和日志流。你如果把所有业务都当成“扔个消息就算解耦”,最后会得到一堆谁也说不清依赖关系的黑箱订阅者。
kafka-go 的使用心法
- 把 producer 和 consumer 的责任想清楚
- 消息模型尽量稳定,别频繁随意改字段
- 消费者一定做幂等
- 对积压、失败率、消费延迟做监控
这些东西听起来不像代码,但它们往往比那几行连接 Kafka 的代码重要得多。
什么时候值得引入 Kafka
- 流量高峰下同步处理压力太大
- 多个系统都要订阅同一类业务事件
- 你希望业务链路更松耦合
如果只是偶尔做个后台任务,小项目直接上 Kafka 也可能太重。别把“有异步需求”和“必须 Kafka”画等号。
小结
kafka-go 这口菜最该学的,其实不是 API,而是异步思维:
- 它适合事件流、削峰、解耦等异步场景
- 生产者和消费者要分别思考消息模型与失败处理
- 幂等、重试、监控、积压,比“能发能收”更重要
- Kafka 不是万能任务队列,别什么都往里扔