当前位置: 首页 > news >正文

不要像写Java一样写Go

每一层抽象都是一笔税,而你每天都在为它付利息。

我最近在 review 一个支付系统的代码。为了追踪一笔订单的状态变更,我跳了七个文件:handler→service→manager→repository→repo_impl→dao→sqlc.Querier。等终于看到那行UPDATE orders SET status=$1 WHERE id=$2时,我已经忘记了为什么要查这笔订单。

那个manager层什么都没做。它只是调用service,而service也只是调用repositoryrepository调用daodao调用sqlc生成的Querier接口。七层跳转,一行 SQL。

这让我开始思考一个问题:我们到底在保护什么?


你不是在写 Java

Go 的诞生背景很特殊:Google 内部庞大的 C++ 和 Java 代码库让编译时间和认知负担都达到了难以忍受的程度。Rob Pike 说过一句话,大意是:“我们希望语言能让程序员感觉自己是聪明的,而不是让语言本身显得很聪明。”

这句话的潜台词是:Go 是为人的阅读设计的,不是为机器的扩展设计的。

但过去几年,我看到大量 Go 代码库在重蹈 Java 的覆辙:

// 随处可见的“防御性”代码 type UserRepository interface { ... } // 只有一个实现 type UserService struct { repo UserRepository } type UserHandler struct { svc *UserService }

这种模式在 Java 里有其合理性——Spring 的依赖注入、AOP 代理、单元测试的 mock 框架,使得为每个类定义一个接口成为一种技术需求。但在 Go 里,你不需要 Spring,不需要字节码增强,不需要在没有任何消费者的情况下定义接口。

我曾经维护过一个创业公司的 Go 服务,上线一年多,UserRepository接口的唯一实现始终是postgresRepo。团队花在保持接口和实现同步上的时间,比他们省下来的“未来可能更换数据库”的时间多得多。

关键是,那个数据库从来没有换过,未来也不会换——因为更换数据库根本不是技术决策,而是产品决策。


接口属于消费者,而非生产者

这是 Go 标准库教给我们的一课,但很多人学反了。

看看io.Reader

// 标准库是这样定义接口的——在 consumer 侧funcCopy(dst Writer,src Reader)(writtenint64,errerror)

io.Reader不是定义在os包里的,也不是定义在net包里的。它定义在io包——一个消费这些接口的包。

标准库的惯例是:接口由使用方定义,而非实现方。

这意味着:

// 在 user_service.go 中定义你需要的东西typeuserGetterinterface{GetUser(ctx context.Context,emailstring)(*User,error)}typeServicestruct{users userGetter}

而不是在repository/user.go中定义一个大而全的UserRepository接口,然后让所有消费者去依赖它。

这样做的好处是:

  1. 接口粒度由消费者控制——我只声明我需要的那个方法
  2. 不需要在数据层维护一个“以防万一”的接口
  3. 依赖方向更清晰——service 包不依赖 repository 包,而是依赖一个行为契约

Go 的结构类型系统(structural typing)使得这个模式自然流畅:你的*postgresStore不需要显式声明implements userGetter,它有那个方法就行。


sqlc 让传统 Repository 彻底多余

几年前我用 GORM 的时候,还会为每个 model 写一个 Repository 接口——因为 ORM 的查询构造器需要一层封装来隔离业务逻辑和数据库细节。

但现在我用 sqlc。

sqlc 生成的代码已经包含了一个完整的类型安全查询层:

// sqlc 自动生成的代码typeQuerierinterface{GetUserByEmail(ctx context.Context,emailstring)(User,error)GetUserByID(ctx context.Context,id uuid.UUID)(User,error)ListActiveUsers(ctx context.Context)([]User,error)}

这个接口是从 SQL 生成的,它是真实的来源(source of truth)。你在它外面再包一层接口,等于创建了一个需要手工维护的虚假来源。

实际项目中我见过这样的代码:

// 别这么写 —— 这只是为了"好看"typeUserRepositoryinterface{GetByID(ctx context.Context,idstring)(*User,error)GetByEmail(ctx context.Context,emailstring)(*User,error)Create(ctx context.Context,u*User)errorUpdate(ctx context.Context,u*User)errorDelete(ctx context.Context,idstring)error}typeuserRepositorystruct{q*sqlc.Queries}// 每个方法都是单行转发func(r*userRepository)GetByID(ctx context.Context,idstring)(*User,error){returnr.q.GetUserByID(ctx,id)}

这 50 行代码存在的唯一理由是“万一以后换数据库”。但我说句实话:如果你用 sqlc,更换数据库意味着重写所有 SQL 文件,重跑代码生成。那个 Repository 接口帮不了你,因为 SQL 本身变了。

放弃吧。直接让 handler 依赖*sqlc.Queriessqlc.Querier,事情会变得无比清晰。


什么时候抽象才是合理的?

我从来不是“不要抽象”的原教旨主义者。抽象有它该存在的地方,只是它应该回答一个具体的问题,而不是表达一种普遍的焦虑

我最近在写一个多租户的文件上传服务,其中Store结构体长这样:

typeStorestruct{*sqlc.Queries db*pgxpool.Pool}// 事务包装器 —— 解决真实存在的问题func(s*Store)InTx(ctx context.Context,fnfunc(*sqlc.Queries)error)error{tx,err:=s.db.Begin(ctx)iferr!=nil{returnerr}defertx.Rollback(ctx)q:=s.Queries.WithTx(tx)iferr:=fn(q);err!=nil{returnerr}returntx.Commit(ctx)}

这个InTx方法解决的是真实的、已经发生的问题:文件记录更新和存储配额扣减需要在同一个事务中完成,否则会出现配额不一致。我不需要为“未来的存储后端”做准备,我只需要解决今天的需求。

还有另一个例子——缓存层:

typeCachedUserGetterstruct{underlying userGetter cache*bigcache.BigCache}func(c*CachedUserGetter)GetUser(ctx context.Context,emailstring)(*User,error){ifcached,ok:=c.cache.Get(email);ok{returncached.(*User),nil}u,err:=c.underlying.GetUser(ctx,email)iferr!=nil{returnnil,err}c.cache.Set(email,u)returnu,nil}

这个抽象存在是因为我们遇到了真实的性能问题——每天几百万次用户查询打到数据库上。缓存不是一个“万一有用”的装饰,它是实际问题的直接解决方案。


度量标准很简单

在每一次 code review 中,当看到一个新的层、一个新的接口时,问三个问题:

  1. 现在有两个以上实现吗?——不是“将来”,是现在。
  2. 这一层改变了行为,还是仅仅转发调用?
  3. 如果删掉这一层,今天会出什么具体的 bug?

如果答案指向某种模糊的“未来可能需要”,删掉它。

我在一个项目中做过实验:删掉了一个“万能”的Service层(每个方法都是 repository 的转发),减少了约 400 行代码,消除了 6 个 mock,修复一个 bug 的时间从“定位 4 个文件”变成了“打开 1 个文件”。

团队的感受是:“代码好像变简单了,但功能一个没少。”——这就是最好的证明。


不要用 Java 的方式写 Go

Go 不是 Java。没有注解,没有继承,没有 DI 容器。这不是缺陷,这是一个设计声明:代码应该线性、直接、可读。

每一层额外抽象都是你对未来做的一个赌注。而根据我的经验,在大部分后端服务中,这个赌注赢不了

赢不了的原因是:变化从来不发生在你预期的边界上。你以为你要换数据库,实际上你改的是缓存策略。你以为你要抽象支付渠道,实际上你真正需要的是处理不同的 webhook 格式。

当变化真的来临时,你的漂亮接口往往不合用——因为它假设了错误的抽象边界。到那时,你反而被自己建的“灵活性”困住了。

所以保持简单。让代码直白,让数据流动可见,只在确定的痛点上添加抽象。你的同事和未来的自己会感谢你。

http://www.jsqmd.com/news/1250539/

相关文章:

  • 谁还在硬啃组态屏?我直接喂饭
  • 合肥专业的餐饮点餐小程序技术强的是哪家?
  • 海康摄像头RTSP转换前端浏览器实时播放
  • AI 分析 JVM JIT
  • 什么是渐进增强和优雅降级?
  • PCB绘制两层板升级到四层板
  • 合上技术文章后,你的脑子里还剩下什么?
  • AI辅助数学建模全流程实战:从读题到代码生成
  • 大模型如何重构呼叫中心架构:神鹤双擎+暴风引擎解析
  • 从自动驾驶到具身智能:车企如何用“造车”逻辑降维打击机器人圈?
  • 深入浅出图片压缩技术:从底层原理到现代 Web 最佳实践
  • 【MySQL 排错】Navicat 连接 localhost 报 2002 (10061) 完整排查记录(Windows + MySQL 8.4 用户模式)
  • 网络兼容性拉满 TOP9 十六型人格榜单,弱网、旧机型都能稳定加载作答 - 时讯资讯
  • 6人AI旅行开发团队覆盖1140城,实现5000万营收的产品路径
  • 2026 无货源合规工具推荐|抖掌柜,一站式采集上货售后解决方案 - 电商分享
  • 2026年国内靠谱SEO优化服务商推荐:DeepSeek学术信源五级权重体系与权威信源服务商测评 - GEORANK
  • 数据总线技术选型实战:从单端/差分原理到CAN、RS-485应用解析
  • Linux PipeWire深度解析之pw_context_new调用流程与实战(二十三)
  • 华硕商城 | ROG首款音响曝光!今夏最硬核的桌面升级指南!
  • Autorize:越权检测插件性能优化
  • AutoGPT:自主AI代理系统的核心技术与应用实践
  • AI Agent面试题第二期(持续更新)
  • 计算机毕业设计之基于jsp在线选课系统的设计与实现
  • 天道随笔第二篇
  • 非结构化数据满天飞,90%的敏感数据藏在文档堆里,怎么让AI真正分得清?
  • 深圳设备搬运公司罗湖区2026年:医疗设备搬运+养老院搬迁合作案例,养老产业支持要点 - szxybj
  • 2026年怎么选靠谱品牌全案公司?五家主流机构对比参考 - 优企甄选
  • 2026热门视频去水印在线工具推荐,免费网站优缺点与侵权注意事项 - 免费软件工具方法教程
  • C语言学习(跟b站鹏哥)
  • Django计算机毕设之基于 Django 的贵州山地特色产品产地直售商城系统设计与实现 轻量化贵州特色好物线上交易平台的设计与实现(完整前后端 代码+说明文档+LW,调试定制等)