不要像写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。这让我开始思考一个问题我们到底在保护什么你不是在写 JavaGo 的诞生背景很特殊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接口然后让所有消费者去依赖它。这样做的好处是接口粒度由消费者控制——我只声明我需要的那个方法不需要在数据层维护一个“以防万一”的接口依赖方向更清晰——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.Queries或sqlc.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 中当看到一个新的层、一个新的接口时问三个问题现在有两个以上实现吗——不是“将来”是现在。这一层改变了行为还是仅仅转发调用如果删掉这一层今天会出什么具体的 bug如果答案指向某种模糊的“未来可能需要”删掉它。我在一个项目中做过实验删掉了一个“万能”的Service层每个方法都是 repository 的转发减少了约 400 行代码消除了 6 个 mock修复一个 bug 的时间从“定位 4 个文件”变成了“打开 1 个文件”。团队的感受是“代码好像变简单了但功能一个没少。”——这就是最好的证明。不要用 Java 的方式写 GoGo 不是 Java。没有注解没有继承没有 DI 容器。这不是缺陷这是一个设计声明代码应该线性、直接、可读。每一层额外抽象都是你对未来做的一个赌注。而根据我的经验在大部分后端服务中这个赌注赢不了。赢不了的原因是变化从来不发生在你预期的边界上。你以为你要换数据库实际上你改的是缓存策略。你以为你要抽象支付渠道实际上你真正需要的是处理不同的 webhook 格式。当变化真的来临时你的漂亮接口往往不合用——因为它假设了错误的抽象边界。到那时你反而被自己建的“灵活性”困住了。所以保持简单。让代码直白让数据流动可见只在确定的痛点上添加抽象。你的同事和未来的自己会感谢你。

相关新闻

市场对外呼系统的选择标准

市场对外呼系统的选择标准

随着电销行业监管趋严,企业挑选外呼系统不再只看重基础拨号功能,而是围绕合规安全、线路稳定、业务适配、成本与服务五大维度综合筛选,规避封号、业务中断、数据泄露等经营风险。合规资质是市场选型第一底线,也是区分正规服务商与…

2026/10/9 14:35:17 阅读更多 →
FS800DTU直连OnetNET上云实测

FS800DTU直连OnetNET上云实测

本文为「4G 物联网模块实战」系列第 3 篇,记录 FS800DTU 接入中国移动 OneNET 平台、把设备数据送上云的完整过程。 版权声明:本文为原创技术分享文章,转载请注明出处。 目录 前言1 准备工作2 配置步骤:FS800DTU 接入 OneNET 2.1…

2026/10/3 6:01:36 阅读更多 →
Linux PipeWire深度解析之pw_context_new调用流程与实战(二十三)

Linux PipeWire深度解析之pw_context_new调用流程与实战(二十三)

简介: CSDN博客专家、《Android系统多媒体进阶实战》作者 博主新书推荐:《Android系统多媒体进阶实战》🚀 Android Audio工程师专栏地址: Audio工程师进阶系列【原创干货持续更新中……】🚀 Android多媒体专栏地址&a…

2026/10/11 17:51:37 阅读更多 →

最新新闻

行为型设计模式详解:策略、模板方法、观察者、迭代器、责任链

行为型设计模式详解:策略、模板方法、观察者、迭代器、责任链

设计模式系列写到第四篇,终于轮到行为型模式了。前几篇我们把创建型(怎么把对象造出来)和结构型(怎么把对象组合起来)讲完了,这一篇开始讲行为型模式的“上篇”。如果你是跟着这个系列一路看过来的&#xf…

2026/10/13 12:24:19 阅读更多 →
K均值聚类实战:从原理到落地的完整指南

K均值聚类实战:从原理到落地的完整指南

1. 从一个真实需求说起:为什么聚类值得花时间搞明白做数据这行的朋友大概率都遇到过这种场景:手里攥着一份几万行的用户行为表,老板说“把这些人分分类,看看有没有什么规律”,但表里压根没有现成的标签,也没…

2026/10/13 12:24:19 阅读更多 →
图像形成物理:计算机视觉的底层逻辑与工程避坑指南

图像形成物理:计算机视觉的底层逻辑与工程避坑指南

我最早翻到《图像处理、分析与机器视觉(第四版)》的"图像形成物理"这一章时,第一反应是直接跳过。直觉告诉我,前面几章才是重头戏,图像形成不过是点物理常识,无非就是"光打到物体上、镜头收…

2026/10/13 12:24:19 阅读更多 →
AI基础经典试题全解析:归结反演与α-β剪枝手推题攻略

AI基础经典试题全解析:归结反演与α-β剪枝手推题攻略

简介:百度人工智能岗位的经典考试试题文档,覆盖语义网络、知识表示、推理方式、机器学习及自然语言处理等核心模块,并配有完整答案解析。题目按选择题、填空题、简答计算题、应用题四种形式编排,适合正在准备百度或同类互联网公司…

2026/10/13 12:24:19 阅读更多 →
MMDetection网络结构全解析:Backbone、Neck、Head与Loss设计

MMDetection网络结构全解析:Backbone、Neck、Head与Loss设计

1. 整体思路拆解:mmDetection 的网络部分为什么要这么设计我刚开始接触 mmDetection 的时候就一个感觉:模块太多了。一个检测器拆成 backbone、neck、head、loss,到处是 config、registry、builder,看两天源码直接想退坑。但后来切…

2026/10/13 12:24:19 阅读更多 →
Linux安装Oracle19C全攻略:前置检查、静默安装与高频报错排查

Linux安装Oracle19C全攻略:前置检查、静默安装与高频报错排查

简介:对Linux运维工程师和Oracle DBA而言,在CentOS 7与Red Hat 7中安装Oracle 19C,需要提前完成硬件检查、安全策略调整、网络和用户配置等一系列系统级准备,否则安装过程很容易中断。完整安装指南以docx格式提供,共1个…

2026/10/13 12:23:19 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →