扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • 宝典简介

    • Go 语言实战: 编写可维护 Go 语言代码建议
  • 宝典内容

    • 指导原则
    • 标识符
    • 注释
    • 包的设计
    • 项目结构
    • API 设计
    • 错误处理
    • 并发

扫码订阅《 》或入驻星球,即可阅读文章!

项目结构


田浩

我们来谈谈如何将包组合到项目中。 通常一个项目是一个 git 仓库,但在未来 Go 语言开发人员会交替地使用 module 和 project。

就像一个包,每个项目都应该有一个明确的目的。 如果你的项目是一个库,它应该提供一件事,比如 XML 解析或记录。 您应该避免在一个包实现多个目的,这将有助于避免成为 common 库。

贴士: 据我的经验,common 库最终会与其最大的调用者紧密相连,在没有升级该库与最大调用者的情况下是很难修复的,还会带来了许多无关的更改以及API破坏。

如果你的项目是应用程序,如 Web 应用程序,Kubernetes 控制器等,那么项目中可能有一个或多个 main 程序包。 例如,我编写的 Kubernetes 控制器有一个 cmd/contour 包,既可以作为部署到 Kubernetes 集群的服务器,也可以作为调试目的的客户端。

# 5.1. 考虑更少,更大的包

对于从其他语言过渡到 Go 语言的程序员来说,我倾向于在代码审查中提到的一件事是他们会过度使用包。

Go 语言没有提供有关可见性的详细方法; Java有 public、protected、private 以及隐式 default 的访问修饰符。 没有 C++ 的 friend 类概念。

在 Go 语言中,我们只有两个访问修饰符,public 和 private,由标识符的第一个字母的大小写表示。 如果标识符是公共的,则其名称以大写字母开头,该标识符可用于任何其他 Go 语言包的引用。

注意: 你可能会听到人们说 exported 与 not exported, 跟 public 和 private 是同义词。

鉴于包的符号的访问有限控件,Go 程序员应遵循哪些实践来避免创建过于复杂的包层次结构?

贴士: 除 cmd/ 和 internal/ 之外的每个包都应包含一些源代码。

我的建议是选择更少,更大的包。 你应该做的是不创建新的程序包。 这将导致太多类型被公开,为你的包创建一个宽而浅的API。

以下部分将更为详细地探讨这一建议。

贴士: 来自 Java? 如果您来自 Java 或 C#,请考虑这一经验法则 -- Java 包相当于单个 .go 源文件。 - Go 语言包相当于整个 Maven 模块或 .NET 程序集。

# 5.1.1. 通过 import 语句将代码排列到文件中

如果你按照包提供的内容来安排你的程序包,是否需要对 Go 包中的文件也执行相同的操作?什么时候应该将 .go 文件拆分成多个文件?什么时候应该考虑整合 .go 文件?

以下是我的经验法则:

  • 开始时使用一个 .go 文件。为该文件指定与文件夹名称相同的名称。例如: package http 应放在名为 http 的目录中名为 http.go 的文件中。
  • 随着包的增长,您可能决定将各种职责任务拆分为不同的文件。例如:messages.go 包含 Request 和 Response 类型,client.go 包含 Client 类型,server.go包含 Server 类型。
  • 如果你的文件中 import 的声明类似,请考虑将它们组合起来。或者确定 import 集之间的差异并移动它们。
  • 不同的文件应该负责包的不同区域。messages.go 可能负责网络的 HTTP 请求和响应,http.go 可能包含底层网络处理逻辑,client.go 和 server.go 实现 HTTP 业务逻辑请求的实现或路由等等。

贴士: 首选名词为源文件命名。

注意: Go编译器并行编译每个包。 在一个包中,编译器并行编译每个函数(方法只是 Go 语言中函数的另一种写法)。 更改包中代码的布局不会影响编译时间。

# 5.1.2. 优先内部测试再到外部测试

go tool 支持在两个地方编写 testing 包测试。假设你的包名为 http2,您可以编写 http2_test.go 文件并使用包 http2 声明。这样做会编译 http2_test.go 中的代码,就像它是 http2 包的一部分一样。这就是内部测试。

go tool 还支持一个特殊的包声明,以 test 为结尾,即 package http_test。这允许你的测试文件与代码一起存放在同一个包中,但是当编译时这些测试不是包的代码的一部分,它们存在于自己的包中。就像调用另一个包的代码一样来编写测试。这被称为外部测试。

我建议在编写单元测试时使用内部测试。这样你就可以直接测试每个函数或方法,避免外部测试干扰。

但是,你应该将 Example 测试函数放在外部测试文件中。这确保了在 godoc 中查看时,示例具有适当的包名前缀并且可以轻松地进行复制粘贴。

贴士: 避免复杂的包层次结构,抵制应用分类法 Go 语言包的层次结构对于 go tool 没有任何意义除了下一节要说的。 例如,net/http 包不是一个子包或者 net 包的子包。

如果在项目中创建了不包含 .go 文件的中间目录,则可能无法遵循此建议。

# 5.1.3. 使用 internal 包来减少公共API

如果项目包含多个包,可能有一些公共的函数,这些函数旨在供项目中的其他包使用,但不打算成为项目的公共API的一部分。 如果你发现是这种情况,那么 go tool 会识别一个特殊的文件夹名称 - 而非包名称 - internal/ 可用于放置对项目公开的代码,但对其他项目是私有的。

要创建此类包,请将其放在名为 internal/ 的目录中,或者放在名为 internal/ 的目录的子目录中。 当 go 命令在其路径中看到导入包含 internal 的包时,它会验证执行导入的包是否位于 internal 目录。

例如,.../a/b/c/internal/d/e/f 的包只能通过以 .../a/b/c/ 为根目录的代码被导入。 它无法通过 .../a/b/g 或任何其他仓库中的代码导入。[5] (opens new window)

# 5.2. 确保 main 包内容尽可能的少

main 函数和 main 包的内容应尽可能少。 这是因为 main.main 充当单例; 程序中只能有一个 main 函数,包括 tests。

因为 main.main 是一个单例,假设 main 函数中需要执行很多事情,main.main 只会在 main.main 或 main.init 中调用它们并且只调用一次。 这使得为 main.main 编写代码测试变得很困难,因此你应该将所有业务逻辑从 main 函数中移出,最好是从 main 包中移出。

贴士: main 应该做解析 flags,开启数据库连接、开启日志等,然后将执行交给更高一级的对象。

  • 5.1. 考虑更少,更大的包
  • 5.1.1. 通过 import 语句将代码排列到文件中
  • 5.1.2. 优先内部测试再到外部测试
  • 5.1.3. 使用 internal 包来减少公共API
  • 5.2. 确保 main 包内容尽可能的少