Go的秘密生活:包和结构
Source: Dev.to
请提供您希望翻译的正文内容,我会按照要求保留源链接、格式和技术术语,仅翻译文本部分。
Chapter 11: The Architect’s Blueprint
The Friday afternoon sun slanted low through the archive windows, illuminating dust motes dancing in the air. Ethan found Eleanor standing atop a rolling ladder, carefully rearranging a high shelf of leather‑bound ledgers.
Ethan: “你在重新整理吗?”
He set a paper bag on the desk.
Eleanor: “我在纠正。” She slid a heavy volume into place with a satisfied sigh. “前任档案员是按 ‘装订颜色’ 而不是 ‘获取年份’ 来归档的。这样看起来书架很漂亮,但检索非常困难。要找任何东西都得到处找。”
She climbed down, dusting off her hands.
Ethan: “你带了什么?”
“开心果羊角面包。还有一杯cortado。”
Eleanor’s expression softened into a genuine smile.
Eleanor: “你记得cortado。谢谢你,Ethan。” She gestured to the chair opposite her. “请坐。这个下午正适合来杯热饮。”
Ethan opened his laptop.
Ethan: “我已经实现了你建议的库存系统。运行得很顺畅。原子操作快,互斥锁安全。我已经准备好部署了。”
Eleanor peered at the screen, leaning in with interest.
Eleanor: “我们来看看。不只是逻辑本身,还要看看你为它搭建的‘家’。”
She pointed at the file explorer on the left.
/my-project
├── main.go
├── user.go
├── product.go
├── db.go
├── server.go
├── utils.go
└── types.go
Eleanor: “这让我想起我刚开始整理这个档案时的情景。把所有东西放在同一个地方起初感觉很方便,对吧?所有东西都在眼前。”
Ethan: “是的,正是这样。省去了在文件夹里四处搜寻的麻烦。”
Eleanor: “但想象一下,如果这个图书馆没有分区——只有一大堆书堆在地板中央。随着藏书增多,想找到需要的东西就会变得沉重。在 Go 中,文件系统 本身 就是架构。我们必须讨论包(Package)。”
The Encapsulation Boundary
Eleanor: “在很多语言里,文件夹只是用来组织的。而在 Go 中,文件夹 就是 一个 Package。它是一个边界——可以把它想象成一个安全的阅览室。”
She opened a new terminal.
Eleanor: “同一目录下的每个文件必须在顶部声明相同的
package名称。它们共处一室,分享一切。但对外部世界呢?它们只会展示想展示的东西。”
Ethan: “你是说公开和私有吗?”
Eleanor: “在 Go 中我们称之为 Exported(导出)和 Unexported(未导出)。美妙之处在于,没有
public或private之类的关键字。只有 大小写规则。”
She typed a quick example:
package users
// Exported (Public) - Starts with Capital Letter
type User struct {
Name string // Exported field
id int // Unexported field (private)
}
// Exported function
func New(name string) *User {
return &User{
Name: name,
id: generateID(), // We can call unexported functions inside the package
}
}
// Unexported function (Private) - Starts with lowercase
func generateID() int {
return 42 // Simplified
}
Eleanor: “如果以大写字母开头,其他包就能看到它。如果以小写字母开头,则只能在当前包内部可见。简单而优雅。你不需要额外的配置文件来了解 API——只要看大小写就行。”
Ethan nodded.
Ethan: “明白了。那我应该把东西划分到不同的包里。我会创建一个
models包和一个controllers包。”
Eleanor raised a hand, not to stop him, but to guide him.
Eleanor: “小心。这是其他框架的习惯。在 Go 中我们尽量不要按 种类 来分组——把所有模型放在一起或所有控制器放在一起。我们按 职责 来分组。”
She sketched a diagram on her notepad.
BAD ARCHITECTURE (Group by Kind):
/models
- user.go
- product.go
/controllers
- user.go
- product.go
GOOD ARCHITECTURE (Group by Domain):
/users
- user.go
- handler.go
- repositor
> **Source:** ...
y.go
/products
- product.go
- handler.go
Eleanor: “看到了区别吗?如果按业务域来组织,所有关于 Users 的代码都在
package users中。它是自包含的。它讲述的是你的软件 做了什么,而不仅仅是它是如何构建的。”
内部堡垒
Eleanor: “现在你终会遇到需要在自己的包之间共享的代码——辅助函数或核心逻辑——但你不想让外部世界导入它们。比如给用户和商品专用的加密助手。”
Ethan: “我可以建一个
shared包吗?”
Eleanor: “可以。但那样的话,任何在互联网上导入你的库的人都能使用你的
shared包。它会成为你的公开承诺。你再也无法在不破坏他们的情况下修改它。”
她轻点屏幕。
Eleanor: “Go 为我们提供了一个绝佳的工具:
internal。”
她在屏幕上重新排列了 Ethan 的项目结构:
/my-project
├── go.mod
├── main.go
├── internal
│ └── platform
│ └── crypto.go // Only visible to my-project!
├── users
│ ├── service.go // Can import internal/platform
│ └── user.go
└── products
└── product.go
Eleanor: “Go 编译器专门对这点进行强制检查。任何位于名为
internal的目录中的包,只能被根目录下的代码导入。它让你能够编写那些必要但凌乱的细节,而外部项目永远触及不到。”
Ethan: “所以
internal只对我们有效?”
Eleanor: “只对我们自己有效。它让你在以后重构时毫无顾虑。它是一张安全网。”
“Utils” 的疑问
Ethan 注视着屏幕上的新结构,心中渐生秩序感。随后他指着自己的文件列表。
Ethan: “那……
utils.go怎么办?里面有字符串格式化、一些数学函数、随机数生成器。”
Eleanor 微笑,露出同情的神情。
Eleanor: “啊,
utils…(对话继续)*
Source: …
关于包的对话
“我们最终都会创建一个包。这是一种自然的冲动,想要给零碎的东西找个地方。但把它想象成一个杂物抽屉。一旦你开始往里放东西,就停不下来,而且再也找不到它们了。”
“那就删掉它?”
“把它分发,”她温柔地纠正道。“给它们一个合适的归宿。如果一个函数格式化用户名,就放在
users包里。如果它计算税金,就放在billing包里。包名应该描述它提供的功能,而不仅仅是‘装东西’的地方。”
她咬了一口羊角面包,点头表示赞同。
“这真好吃,Ethan。”
她把面包屑从笔记本上刷掉。
“结构不仅仅是对文件进行排序。它是设计系统以防止依赖循环。如果
package A需要package B,而package B又需要package A,Go 会拒绝编译。平铺的目录会隐藏这些循环,而结构化的项目会暴露它们,让你有机会修复。”
Ethan 开始创建 users 目录。
“再也没有杂物抽屉了。”
“万物各得其所,事物各安其位,”Eleanor 热情地赞同道。“并且把导出的标识符大写。你在写一个库,Ethan,而不是日记。让全世界都能读取它。”
第 11 章关键概念
包
- Go 中代码组织的基本单元。
- 每个目录都是一个包。
- 同一目录下的所有文件必须声明相同的
package名称。
导出 vs. 未导出
| 符号 | 可见性 | 示例 |
|---|---|---|
大写(例如 User) | 导出(公共) | 对其他包可见 |
小写(例如 user) | 未导出(私有) | 仅在同一包内部可见 |
领域驱动结构
- 更倾向于按领域(例如
users、orders、billing)组织包,而不是按层次(例如models、controllers、utils)。 - 将相关逻辑放在一起。
internal 目录
- Go 工具链识别的特殊目录。
internal中的代码只能被其父目录及其子目录导入。- 防止实现细节被外部用户使用。
包命名
- 简短、简洁、全小写(例如
net、http、json)。 - 避免使用
utils、common、helpers等通用名称。 - 包名应描述该包提供的功能。
循环依赖
- Go 不允许导入循环(A 导入 B,B 再导入 A)。
- 良好的包结构通过单向依赖来防止这种情况。
go.mod
- 定义模块并管理依赖。
- 位于项目根目录。
下一章:错误处理——Eleanor 将解释为什么 Go 没有异常,Ethan 学会将错误视为值来处理。
Aaron Rose 是一名软件工程师兼技术作家,供稿于 tech-reader.blog,也是《Think Like a Genius》的作者。https://amazon.com/author/aaron.rose