Go语言包结构天然契合最少知识原则(LoD),因包为访问边界、导出规则强制最小暴露、函数参数即契约,且通过接口隔离、封装深层访问、注入抽象依赖等方式落实LoD。

Go 语言里没有“import "github.com/xxx/yyy".Zzz”这种链式调用的惯用写法,不是语法限制,而是最少知识原则(LoD)在工程层面的自然体现——你本就不该知道 Zzz 是谁的字段、谁的方法、谁的返回值。
为什么 Go 的包结构天然适合最少知识原则
Go 不提供类继承、不支持方法链式调用、不鼓励嵌套对象访问(如 a.b.c.Do()),这些“限制”反而是 LoD 的落地支撑:
- 包是访问边界:
internal/下的包无法被外部导入,天然切断了“跨层穿透”的可能 - 导出规则强制最小暴露:
user.Name可导出,user.dbConn默认不可见,避免下游误用内部状态 - 函数参数即契约:所有依赖必须显式传入,没人能偷偷从全局或嵌套结构里捞东西
这意味着,一个 tradeBot 类型若要发通知,不能写成 t.steamClient.api.notify.Send(...),而应接收一个 Notifier 接口作为参数——它只“知道”通知这件事,不知道 Steam、HTTP、还是 Kafka。
如何识别并重构违反最少知识原则的代码
典型症状包括:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 函数体里出现超过一层的点号访问,比如
req.Context().Value("user").(*User).Profile.AvatarURL - 测试时不得不 mock 整条依赖链(
mockDB.MockTx().MockStmt().Rows()) - 改一个配置字段,导致三个不相关包的测试全挂(说明它们都直接读了同一 config struct)
重构动作很简单:
- 把深层字段访问封装成方法:
u.AvatarURL()替代u.Profile.AvatarURL - 把嵌套调用拆成参数注入:
func (t *tradeBot) sendTrade(n Notifier)替代t.client.Notifier.Send() - 用接口隔离变化点:
type DBExecutor interface { Exec(query string, args ...any) (sql.Result, error) },而不是直接依赖*sql.DB
internal/ 与 pkg/ 的分工本质是 LoD 的分水岭
internal/ 不是“放私有代码的地方”,而是“LoD 的物理围栏”:
-
internal/tradebot/可以自由使用internal/steamapi/,因为它们属于同一信任域,共享上下文 -
pkg/logger/对外提供Logger接口,但绝不暴露其内部用的是zerolog还是zap——使用者不该、也不能依赖这个细节 - 一旦发现
cmd/里直接 new 了一个internal/xxx/yyy的 struct,说明 LoD 已破:入口不该知道内部实现,应该通过构造函数或选项传入抽象依赖
真正的难点不在结构命名,而在每次加一行 . 时,多问一句:“这个点号之后的东西,是我这个函数‘直接的朋友’吗?”

















