diff --git a/Chapter 11.md b/Chapter 11.md new file mode 100644 index 0000000000000000000000000000000000000000..571dfc82e5996647e4c4a7fb02fbd0ec189ae8f1 --- /dev/null +++ b/Chapter 11.md @@ -0,0 +1,1004 @@ +# Chapter 11: Packages and imports + + +![Packages and imports](https://www.practical-go-lessons.com/img/package-imports.b209ceba.jpg) + +------ + + + +# 1 在本章中你将学到什么? + +- 什么是包? +- 源文件是如何分组的? +- main.go 文件存储在哪里? +- 如何创建你的 GO 项目? +- 什么是导入路径? 什么是导入声明? +- go.mod文件是什么? +- 什么是模块化编程? +- 如何使用Go构建模块化应用? +- 什么是内部目录,为什么要使用内部目录? + +# 2 涵盖的技术概念 + +- 包 +- 源文件 +- 导入 +- 模块 +- 模块化编程 + +# 3 程序、软件包、源文件 + +Go程序是软件包的组合(见图1)。 + +![A Go program[fig:A-Go-program]](https://www.practical-go-lessons.com/img/go_program_package.acdaaa3b.png) + +一个GO程序[图:一个GO程序] + +包由一个或多个**源文件**组成。在这些源文件中,Go程序员声明: + +- 常量 +- 变量 +- 函数 +- 类型和方法 + +软件包主程序**通常**由单个文件组成。main函数是程序的入口**。在GO程序中,您还将找到一个名为**go.mod**的文件**。**以下各节将详细介绍所有这些组成部分。 + +# 4 & 源文件 + +在图[2](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fig:A-source-file)中,您可以看到源文件的原理图版本。我们将详细说明源文件的每个部分。 + +![源文件[图:A-source-file]](https://www.practical-go-lessons.com/img/source_file.0a4d14ff.png) + +源文件[图:一个源文件] + +以下代码片段是一个软件包**占用**的源文件示例**:** + +``` +// package-imports/occupancy/occupancy.go +package occupancy + +const highLimit = 70.0 +const mediumLimit = 20.0 + +// retrieve occupancyLevel from an occupancyRate +// From 0% to 30% occupancy rate return Low +// From 30% to 60% occupancy rate return Medium +// From 60% to 100% occupancy rate return High +func level(occupancyRate float32) string { + if occupancyRate > highLimit { + return "High" + } else if occupancyRate > mediumLimit { + return "Medium" + } else { + return "Low" + } +} + +// compute the hotel occupancy rate +// return a percentage +// ex : 14,43 => 14,43% +func rate(roomsOccupied int, totalRooms int) float32 { + return (float32(roomsOccupied) / float32(totalRooms)) * 100 +} +``` + +## 4.1 包子句 + +在源文件的顶部,我们在示例中找到**包子句**,它是: + +``` +package occupancy +``` + +**包子句**是每个源文件的第一行。它定义了当前包的名称。 + +## 4.2 导入声明 + +接着是一组导入声明。在源文件的这一部分中,我们定义了希望在这个包中使用的所有其他包。包占用不会导入其他包。让我们举另一个例子:这是来自包room的源文件 : + +``` +// package-imports/import-declaration/room/room.go +package room + +import "fmt" + +// display information about a room +func printDetails(roomNumber, size, nights int) { + fmt.Println(roomNumber, ":", size, "people /", nights, " nights ") +} + +import "fmt" +``` + +这里我们导入一个包: + +- `fmt`这是标准库函数的一部分 + +## 4.3 源代码 + +导入声明后,我们找到最重要的部分,包的源代码。在这里,您可以声明变量、常量、函数、类型和方法。 + +# 5 组织文件 + +我们必须将包的源文件分组到一个目录中。目录必须与软件包同名。例如,**baz**包的源文件必须存储到**baz**文件夹中(见图[3](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fig:Location-of-package's)) + +![源文件[图:A-source-file]](https://www.practical-go-lessons.com/img/package_storage.f7358f82.png) + + + +[[图:软件包的位置]] + + + +# 6 主函数包 + +Go程序首先初始化“main”包,然后从该包运行函数“main”。主函数包是您的程序开始执行,构建它所要做的事情的地方。 + +下面是一个主包的例子: + +``` +// package-imports/main-package/main.go +package main + +import "fmt" + +func init() { + fmt.Println("launch initialization") +} + +func main() { + fmt.Println("launch the program !") +} +``` + +此程序具有`init`(初始化)功能。此函数可以保存程序正确运行所需的所有初始化任务(有关此函数的更多信息,请参阅专用章节)。 + +该程序还定义了一个“main”函数。这两个函数都没有返回类型(不像C, main函数必须返回一个整数)。 + +main函数将在所有初始化任务完成后执行。在这个函数中,您通常调用其他包并实现您的程序逻辑。 + +上一次程序输出: + +``` +launch initialization +launch the program! +``` + +启动`init`函数,然后是主函数。 + +#### 6.0.0.1 每个项目一个主函数包? + +情况并不总是这样,但一个项目可以有几个主函数包,因此有几个主要功能。通常,大型项目中存在不同的主函数包。以下是一些常见的例子: + +- 启动应用程序Web服务器的主要软件包 +- 另一个运行计划数据库维护的主要软件包 +- 另一个是为特定守时干预而开发的...... + +例如,Kubernetes(最受关注的围棋项目之一),在同一时间有20个主函数包。 + +#### 6.0.0.2 我应该命名保存主函数包为 main.go 的文件吗? + +不,这不是一项义务。如果您的项目只有一个主要功能,您可以这样做。但通常,给它起一个名称是很好的做法。 + +例如,在项目Kubernetes中,您可以找到以下文件,它们保存了主函数包: + +- check_cli_conventions.go +- gen_kubectl_docs.go + +我们只需查看程序的名称,就可以推断出程序的作用:第一个将检查是否遵守了CLI规定,第二个将生成文档。 + +请遵守这个惯例。它允许其他开发人员通过查看文件树来了解程序的作用。 + +#### 6.0.0.3 我们存储在特定目录中的主要函数包 + +同样,Go规范中没有关于应该包含主函数包的文件夹。主函数包仍然有很高的使用率,应该住在存储库根部的**cmd/**文件夹中。 + +# 7 go.mod文件 + +以下是go.mod文件的示例: + +``` +module maximilien-andile.com/myProg + +go 1.13 +``` + +## 7.1 模块路径 + +第一行由单词**模块**和模块路径组成。 + +我们在这里介绍**模块**的概念。它们并不总是存在于 GO 中;它们大约在2019年3月提出。我们将在另一部分中详细说明模块是什么以及如何使用它们。目前,请记住模块是“存储在文件树中的Go包的集合,其根部是go.mod文件”[1](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fn1)。当我们向模块添加主函数包时,我们使它成为一个可以编译和执行的程序。 + +我们需要为我们的模块(我们的程序)定义一个模块路径。路径是模块的唯一位置。以下是两个著名的 GO 程序的一些路径示例。 + +- Hashicorp Vault,[2](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fn2): + +``` +module github.com/hashicorp/vault +``` + +- Kubernetes,[3](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fn3): + +``` +module k8s.io/kubernetes +``` + +`"github.com/hashicorp/vault"`是 Github 存储库的 URL。Github存储库是一个包含项目源代码的文件夹。此文件夹可以公开访问(即由任何拥有链接的人访问)或私有。在后一种情况下,代码只能被授权开发人员访问。如果您可以访问内部有go.mod的存储库,您可以在项目中导入它。我们稍后会看看这是如何运作的。 + +请注意,如果您选择远程路径,您的程序不会被Go自动共享或公开!为了测试的目的,您可以选择非URL的路径: + +``` +module thisIsATest + +go 1.13 +``` + +## 7.2 预设的 GO 版本 + +go.mod文件的下一行声明Go的预设版本。在这个程序中,go的1.13版本是预设的。通过设置,我们向其他开发人员说,我们的项目是使用这个特定版本的围棋开发的。10年后,GO 将不断发展,我们现在在程序中使用的一些东西可能将不再存在。未来的程序员会知道如何编译它。 + +# 8 一个示例程序 + +![go程序文件夹/文件组织示例[图:Go-程序-示例]](https://www.practical-go-lessons.com/img/tree_go_program_example.a3426c1b.png) + +go程序文件夹/文件组织示例[图:Go-程序-示例] + +程序由2个目录(粗体字)组成: + +- occupancy +- room + +每个目录由一个go文件组成。文件有目录的名称。每个文件夹代表一个包。 + +#### 8.0.0.1 room.go + +``` +package room + +const roomText = "%d : %d people / %d nights" +``` + +#### 8.0.0.2 occupancy.go + +``` +package occupancy + +const highLimit = 70.0 +``` + +在程序文件夹的根目录下,我们可以找到带有main.go**文件的**go.mod文件。 + +#### 8.0.0.3 go.mod + +``` +module thisIsATest + +go 1.13 +``` + +#### 8.0.0.4 main.go(入口) + +``` +package main + +import "fmt" + +func main() { + fmt.Println("program started") +} +``` + +让我们尝试构建并执行它: + +``` +$ go build main.go +$ ./main +program started +``` + +程序除了输出字符串“program started(程序启动)”外,什么都不做。在两个包中,我们定义了一个常量。这两个常数从未使用过。 + +# 9 实际应用1:给现有程序添加一个包 + +## 9.1 问题 + +在之前的 Go 程序中添加名为**booking**的软件包。在这个包中: + +- 定义一个名为`vatRate`的常量,值为20.0 +- 定义一个名为`printVatRate`的函数,该函数将打印到标准输出的常量`vatRate`,后跟百分比符号。 + +## 9.2 解决方案 + +包由一个目录和至少一个源文件组成。如果我们想要创建一个package ' booking ',我们应该: + +1. 创建名为“预订”的目录 +2. 创建一个文件到这个目录中。文件名称将为**booking.go** + +这是源文件booking.go: + +``` +// package-imports/application-add-package/booking/booking.go +package booking + +import "fmt" + +const vatRate = 20.0 + +func printVatRate() { + fmt.Printf("%.2f %%", vatRate) +} +``` + +# 10 模块化编程 + +我们可以在单个main.go文件中定义一个程序。这就是我们以前所做的。这是合法的;你的代码将编译。这是合法的,但不是最好的解决方案。理解为什么我们需要定义“模块化编程”的概念。 + +这些问题对于软件工程来说并不新鲜,以前被很多程序员问过。在60年代,软件开发人员一直在努力维护代码库。软件被设计成单一的庞然大物,大量使用GOTO语句[[@davis2011understanding\]](https://www.practical-go-lessons.com/chap-11-packages-and-imports#davis2011understanding)。由于这种设计,一些项目的代码库变得难以理解。 + +社区需要更好的发展方式。这是对模块化编程反思的开始。 + +## 10.1 什么是模块? + +为了定义模块,我们将使用Gauthier的标准定义。[[@gauthier1970designing\]](https://www.practical-go-lessons.com/chap-11-packages-and-imports#gauthier1970designing)。模块是代码块: + +1. 执行特定任务 +2. 输入和输出定义良好 +3. 可以独立测试 + +一个需要执行几项任务的大型项目可以分为不同的任务,每个任务都生活在具有定义良好的API(2)的模块中,并且可以独立于其他系统进行测试。 + +## 10.2 模块化编程的预期收益 + +我们可以确定三个主要好处[[@parnas1972criteria\]](https://www.practical-go-lessons.com/chap-11-packages-and-imports#parnas1972criteria): + +如果你通过使用模块来构建你的系统,你可以让你的团队独立地在不同的模块上工作,这增加了团队成员的预期**生产力**。例如,您将两个开发人员定位在模块A上,另外两个开发人员定位在模块B上。如果明确定义了两个模块的 API,这两个组就不会相互阻塞。模块即使尚未完成,其他人也可以实现。 + +模块增加了开发的**灵活性**。您可以将独立的功能发送到模块中,而无需对系统的现有代码库进行重大更改。 + +开发人员可以轻松地理解模块中组织的代码。一个有很多结构、接口和文件的大型系统很难理解。加入一个拥有数千个文件的项目很难。由模块组成的系统需要更少的能量来理解,模块可以迭代研究。模块化方便开发人员(尤其是团队新手)对系统**的理解** + +## 10.3 如何将程序分解为模块 + +将系统分解为模块可能很困难。这个问题没有唯一的答案。以下是一些基于经验和阅读的建议: + +- 不要为系统的每个任务创建一个模块;您最终会得到太多大小太小的模块。 +- 相反,创建模块,将功能分组,例如,用于处理数据库查询的模块,用于处理日志记录的模块。 +- 在模块内部,代码通常**紧密耦合**,这意味着模块的不同部分是紧密链接的。 +- 在两个模块之间,您应该强制**松耦合**。每个模块都被视为组件,每个组件不需要另一个组件才能工作。模块应该是独立的。它允许您在不接触其他模块的情况下将一个模块替换为另一个模块。 + +# 11模块化编程/GO模块/GO包 + +我们引入了三个概念: + +- 模块化编程 +- GO 软件包 +- Go模块 + +这三个概念是联系在一起的。模块化编程是一种编写程序的方式。开发人员应该创建可测试的代码块,这些代码块使用定义明确的输出和输出执行特定任务。这个“方法”可以应用于C、Java、C++、Go... + +- **Go软件包**是可用于编写**模块化程序**的工具。在GO软件包中: + - 我们可以将源文件与特定功能相关的函数(或方法)分组: + - 例如:处理酒店预订的套餐预订:创建预订,确认预订,取消预订... + - 例:会将酒店客房相关功能分组的套餐房:显示房间信息,查看其当前入住... + - 我们可以编写独立于代码其余部分运行的测试。(我们将在单元测试章节中查看如何操作) +- **Go模块**是将软件包分组形成应用程序或库的一种方式。 + +# 12 软件包命名惯例 + +为包找到一个好名字很重要。其他开发人员会在源代码中使用软件包名称,因此,它必须是信息丰富的,但也必须是简短的。关于这个主题,写了很多博客文章。在本节中,我将为您提供一些明智选择软件包名称的提示: + +你选择的名字必须很小。在我看来,长度不超过十个字母。以标准库为例。包装名称非常有简短,通常由一个单词组成。 + +这个名字必须带来基本信息。一个好的测试是询问某人你的软件包做什么,而没有向他展示源代码。一个糟糕的答案应该会让你第二次想到这个名字。 + +软件包名称可以用蛇壳(my_name)编写,规范中没有写任何内容,但GO社区的用法是使用驼峰写法(myPackageName) + +#### 12.0.0.1 软件包名称的唯一性 + +这种小名字经常吓到初学者,因为他们害怕潜在的名字冲突。冲突风险存在,但包名级别不存在: + +- 导入路径**和**包名必须是唯一的。 + +另一个开发人员可能将foo作为软件包名称并不妨碍我选择它,因为我的软件包没有相同的导入路径。 + +- 请注意,如果有人与您具有相同的导入路径和相同的软件包名称,您仍然可以编译程序 + +# 13 连接包:试错方法。 + +在上一章中,我们制作的代码全部在主包里。我们知道如何创建包含函数、变量、常量的新包......如何将包里的函数的功能用到主函数包中? + +这是我们的room包: + +``` +// package-imports/trial-error/room/room.go +package room + +import "fmt" + +// display information about a room +func printDetails(roomNumber, size, nights int) { + fmt.Println(roomNumber, ":", size, "people /", nights, " nights ") +} +``` + +如何在主函数包(我们程序的入口)中调用函数`printDetails`?我们试着直接调用函数: + +``` +// package-imports/trial-error/main.go +package main + +func main() { + printDetails(112, 3, 2) +} +``` + +如果我们编译这个程序,我们得到这个错误: + +``` +./main.go:4:2: undefined: printDetails +``` + +我们试图调用的函数`printDetails`没有在主函数包中定义。但是,该功能是在包房中定义的。我们想把包room导入到主函数包中。为了使导入工作,我们需要说我们的程序是一个Go模块。 + +为此,我们需要在项目根部创建一个go.mod文件: + +``` +module thisIsATest2 + +go 1.13 +``` + +模块路径设置为 `thisIsATest2`。你可以选择另一个 + +然后我们可以将房间包导入到我们的主要功能中: + +``` +// package-imports/trial-error-2/main.go +package main + +import "thisIsATest2/room" + +func main() { + printDetails(112, 3, 2) +} +``` + +让我们尝试构建我们的程序。我们收到一条错误消息: + +``` +./main.go:3:8: imported and not used: "thisIsATest2/room" +./main.go:6:2: undefined: printDetails +``` + +此错误说明: + +- 我们无需使用即可导入包。 +- 函数`printDetails`仍未定义。 + +Go不知道我们想使用房间包中的`printDetails`。为了通知它,我们可以使用这个符号: + +``` +room.printDetails(112, 3, 2) +``` + +It means : “call the function `printDetails` from package room”. Let’s try it : + +``` +// package-imports/trial-error-3/main.go +package main + +import "thisIsATest2/room" + +func main() { + room.printDetails(112, 3, 2) +} +``` + +如果我们编译,我们又遇到了一个错误: + +``` +./main.go:6:2: cannot refer to unexported name room.printDetails +./main.go:6:2: undefined: room.printDetails +``` + +它给了我们一个指示:我们无法调用该函数,因为它尚未导出...我们需要导出功能,让别人看得见!要在GO中做到这一点,您必须在函数的第一个字母上使用大写字母。 + +这是新版本的room包源文件: + +``` +// room/room.go +package room + +import "fmt" + +// display information about a room +func PrintDetails(roomNumber, size, nights int) { + fmt.Println(roomNumber, ":", size, "people /", nights, " nights ") +} +``` + +printDetails已重命名为PrintDetails。 + +这是我们的主要功能: + +``` +// package-imports/trial-error-4/main.go +package main + +import "thisIsATest2/room" + +func main() { + room.PrintDetails(112, 3, 2) +} +``` + +函数调用已更改,以反映方法的新名称(`room.PrintDetails`)。现在我们可以启动程序的编译: + +``` +$ go build main.go +``` + +处决: + +``` +$ ./main +112 : 3 people / 2 nights +``` + +万岁!它起作用了。我们已经将另一个包导出的功能用于主包。 + +## 13.1 关键的取用模式 + +- 函数、变量和常量如果不**导出**,则**私有**到定义它的包中 +- 要**导出某物**,只需将其标识符的第一个字母转换为**大写**字母 + +``` +// package-imports/package-example/price/price.go +package price + +// this function is not exported +// we cannot use it in another package +func compute(){ + // +} + +// this function is exported +// it can be used in another package +func Update(){ + //... +} +``` + +## 13.2 词汇精确度 + +- 您可能会听到其他开发人员使用“**public**或**private**”一词。这些条款相当于导出/未导出。其他语言使用它们来指代相同的东西。这不是官方的GO术语。 + - 示例用法:“嗯,你为什么创造了**public**(公共的)这个函数,它甚至没有在你的软件包之外使用?” + - 示例用法:“鲍勃,你应该考虑将此方法**private**(私有化)” +- “可见性”一词是指一种方法的导出/未导出质量。 + - 示例用法:“这个常数的可见性是多少?这是**private**(私有)的” + +# 14 导入路径&导入声明 + +要将一个包导入另一个包,您需要知道它的导入路径。我们将在第一部分中了解如何确定它。 + +当你知道导入路径时,你必须写一个导入声明。这是下一节的对象。 + +## 14.1 导入路径 + +Go规范没有严格指定导入路径。导入路径是一个字符串,它必须在所有现有模块中唯一标识模块。构建系统将使用它们获取导入包的源代码并构建程序。今天,Go构建系统(包中的代码**go/build**依赖于不同类型的路径): + +- 标准库函数路径 + - 我们将用于构建您程序的源文件默认位于**/usr/local/go/src**(适用于Linux和Mac用户)和用于Windows用户的**C:\Go\src**中 + - Example : `fmt`, `time` ... +- 指向代码共享网站的URL。 + - Go为以下使用Git作为版本控制系统(VCS)的代码共享网站提供开箱即用的支持:Github、Gitlab、Bitbucket。 + - URL可以在互联网上公开(例如,托管在Github上的开源项目) + - 示例:`gitlab.com/loir402/foo` + - 它也只能在您的本地/公司网络中公开 + - 示例:`acme-corp.company.gitlab.com/loir402/foo` +- 本地路径或相对路径 + - 当Go引入了模块时,即使Go构建系统支持它,这种类型的导入路径也不再是常态。 + +下载软件包时,go将检查HTTP响应中使用的VCS系统类型(在元标签内)。如果没有提供,您应该将使用的VCS系统类型添加到导入路径中。下面是 Go 官方文档中的一个例子: + +``` +example.org/repo.git/foo/bar +``` + +## 14.2 什么是VCS?什么是Git,Github,Gitlab? + +VCS的意思是版本控制系统;它是一个旨在跟踪项目源文件上操作的更改的程序。VCS系统还具有允许多个开发人员同时在同一程序上工作,而不会互相骚扰。 + +市场上有几个VCS程序可供选择: + +- Git +- Mercurial +- Bazaar +- 。[...4](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fn4) + +Git是开源的。它非常有名,今天被广泛使用。(在我的专业经验中,我只使用Git)。 + +- 它由Linus Torvalds[5](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fn5)创建于2005年。 + +这是一个可以在终端上使用的程序,但也可以使用图形界面。 + +Go源代码的更改使用Git进行管理。 + +Git是一个**分布式VCS。**个人开发人员将通过创建“本地存储库”获取源代码的副本。当开发人员对其更改感到满意时,他可以将它们推送到远程存储库。 + +- **Github、Gitlab、Bitbucket**:是允许开发人员通过初始化托管在服务器上的存储库来托管源代码的网站。 +- 这些网站提供了其他功能,但代码托管是它们的基本活动。 + +这些网站是与他人展示和分享您作品的绝佳方式。它们也是开源社区的基本块。我强烈建议您在那个网站上创建一个个人资料,以显示您可以做什么和帮助他人。 + +## 14.3 远程包如何下载代码 + +你需要在你的终端中使用 `go get` 命令来下载包。 + +例如,如果您想从 https://github.com/graphql-go/graphql 导入代码,请打开一个终端,然后键入: + +``` +go get github.com/graphql-go/graphql +``` + +我们将在专门介绍依赖管理系统的一章中详细介绍这一点。 + +## 14.4 导入声明 + +导入声明声明我们将在当前包中使用一些外部代码。 + +导入声明可以有不同的形式: + +##### 14.4.0.0.1标准进口 + +``` +import "gitlab.com/loir402/foo" +``` + +然后在文件中,您可以使用语法访问导入包的导出元素: + +``` +foo.MyFunction() +``` + +在这里,我们使用foo包中的MyFunction函数。 + +##### 14.4.0.0.2 多个软件包导入 + +你很少只有一个导入。通常情况下,您有几个软件包需要导入: + +``` +import ( + "fmt" + "maximilien-andile.com/application2/foo" + "maximilien-andile.com/application2/bar" +) +``` + +在这里我们导入三个包。以下是他们的导入路径: + +- `fmt` +- `maximilien-andile.com/application2/foo` +- `maximilien-andile.com/application2/bar` + +##### 14.4.0.0.3 带有明确包名(别名)的导入 + +您还可以给软件包一个别名(软件包的某种姓氏),以使用此别名调用其导出元素。例如: + +``` +import bar "gitlab.com/loir402/foo" +``` + +这里我们说这个包裹有姓氏**栏**。您可以使用软件包foo的导出类型、函数、变量和常量(导入路径为“gitlab.com/loir402”),限定符**栏**如下所示: + +``` +bar.MyFunction() +``` + +##### 14.4.0.0.4 带点导入 + +``` +import ."gitlab.com/loir402/foo" +``` + +使用此语法,所有函数、类型、变量和常量都将在当前包中声明。因此,包的所有导出标识符都可以在没有任何限定符的情况下使用。我不建议使用这个,因为它可能会变得令人困惑。 + +##### 14.4.0.0.5 空白导入 + +通过空白导入,只需调用包的init函数。可以使用以下语法指定空白导入: + +``` +import _ "gitlab.com/loir402/foo" +``` + +# 15 内部目录 + +当您创建软件包时,当您的程序在代码共享站点(如Github、Gitlab...)上可用时,其他开发人员将可以使用您的软件包。 + +这是你的重大责任。其他程序将依赖于您的代码。这意味着,当您更改函数或导出标识符的名称时,其他代码可能会中断。 + +要禁止导入包,您可以将它们放入一个名为“内部”的目录中。 + +![内部目录的示例用法](https://www.practical-go-lessons.com/img/internal_directory.c05acbe1.png) + +内部目录的示例用法 + +在图中,可以看到一个带有内部目录的目录结构示例: + +- cmd/main.go包含主包和主功能 +- internal/booking是booking的导入目录。 +- internal/booking/booking.go是包booking的源文件。 +- 包booking内的任何导出标识符都可以访问到当前程序中(即我们可以将一个函数从预订包调用到包主) +- 但是,其他开发人员将无法在程序中使用它。 + + + +# 16 实际应用二:重构源代码 + +## 16.1 问题 + +您的一位同事创建了一个由唯一的main.go文件组成的程序: + +``` +// package-imports/application-refactor/problem/main.go +package main + +import "fmt" + +func main() { + + // first reservation + customerName := "Doe" + customerEmail := "john.doe@example.com" + var nights uint = 12 + emailContents := getEmailContents("M", customerName, nights) + sendEmail(emailContents, customerEmail) + createAndSaveInvoice(customerName, nights, 145.32) +} + +// send an email +func sendEmail(contents string, to string) { + // ... + // ... +} + +// prepare email template +func getEmailContents(title string, name string, nights uint) string { + text := "Dear %s %s,\n your room reservation for %d night(s) is confirmed. Have a nice day !" + return fmt.Sprintf(text, + title, + name, + nights) +} + +// create the invoice for the reservation +func createAndSaveInvoice(name string, nights uint, price float32) { + // ... +} +``` + +您被要求重构代码以提高其可维护性。提出新的代码组织: + +- 您应该创建哪些软件包? +- 您应该创建新目录吗? + +## 16.2 解决方案 + +### 16.2.1 go.mod文件 + +初始任务是创建一个新目录并创建一个go.mod文件。您可以手动完成,但让我向您展示如何使用终端: + +``` +$ mkdir application2Test +$ go mod init maximilien-andile.com/packages/application2 +go: creating new go.mod: module maximilien-andile.com/packages/application2 +``` + +此命令将自动初始化go.mod文件: + +``` +module maximilien-andile.com/packages/application2 + +go 1.13 +``` + +模块路径设置为“maximilien-andile.com/packages/application2”。你可以选择任何你想要的。 + +### 16.2.2 创建哪些软件包? + +主包定义了三个功能: + +- 发送电子邮件 +- getEmailContents +- createAndSaveInvoice + +两个功能与电子邮件有关。与发票相关的一个功能。我们可以创建一个电子邮件包和一个发票包。规则很简单:**将分组到与同一主题相关的包结构中**。 + +让我们为我们的软件包找到名称: + +- email +- invoice + +名字必须保持简短的信息和简单。不要寻找复杂性;保持简单易懂。下一步是创建两个目录来保存我们的源文件。 + +![MacOS查找器中项目文件夹的屏幕截图](https://www.practical-go-lessons.com/img/folder_solution_package_app_2.645f58b2.png) + +MacOS查找器中项目文件夹的屏幕截图 + +### 16.2.3 email包 + +在目录email中,我们将创建一个名为email.go的文件: + +``` +// package-imports/application-refactor/solution/email/email.go +package email + +// send an email +func Send(contents string, to string) { + // ... + // ... +} + +// prepare email template +func Contents(title string, name string, nights uint) string { + // ... + // TO IMPLEMENT + return "" +} +``` + +请注意,我们更改了函数的名称。 + +- sendEmail已被发送取代 +- getEmailContents已被Contents取代 + +首先需要注意的是这两个函数是导出的。名称已被修改。因为我们在包email中,所以我们不需要调用SendEmail方法。以下是两个调用示例: + +``` +// version 1 +email.SendEmail("test","john.doe@test.com") +// version 2 +email.Send("test","john.doe@test.com") +``` + +希望大家更喜欢版本2...在版本1中,我们使用了两次电子邮件一词,这很管用,但并不优雅。 + +我们还更改了第二个函数(getEmailContents)的名称。我们改变了两件事: + +1. 在其他语言中,以“get”一词开头的方法非常常见。在GO中,这是罕见的。为什么?因为当你查看函数签名时,输出是一个字符串。您知道您将以字符串的形式获取电子邮件内容。添加“获取”不会给来电者带来更多信息。我们移除了它。 +2. 我们删除了电子邮件引用,因为我们在邮件包中。没有必要重复自己。 + +### 16.2.4 包invoice + +以下是包invoice的源代码: + +``` +// package-imports/application-refactor/solution/invoice/invoice.go +package invoice + +// create the invoice for the reservation and +// save it to database +func Create(name string, nights uint, price float32) { + // ... +} +``` + +函数名已重新制作:我们导出它,并删除了名称中对发票的引用。以下是调用函数Create的示例: + +``` +invoice.Create() +``` + +如果我们不更改函数的名称,我们可以有这样的东西: + +``` +invoice.CreateInvoice() +``` + +这种语法不必要地重复“invoice”一词。 + +### 16.2.5 主函数包 + +``` +// package-imports/application-refactor/solution/main.go +package main + +import ( + "maximilien-andile.com/packages/application2/email" + "maximilien-andile.com/packages/application2/invoice" +) + +func main() { + // first reservation + customerName := "Doe" + customerEmail := "john.doe@example.com" + var nights uint = 12 + emailContents := email.Contents("M", customerName, nights) + email.Send(emailContents, customerEmail) + invoice.Create(customerName, nights, 145.32) +} +``` + +首先,请注意,我们导入了两个由其导入路径标识的软件包: + +- `maximilien-andile.com/package/application2/email` +- `maximilien-andile.com/package/application2/invoice` + +主要功能是我们程序的入口。定义了三个变量。 + +- `customerName`(字符串) +- `customerEmail`(字符串) +- `nights`(无符号整数) + +由于我们公开了`Contents`和`Send`函数,我们可以将它们自由调用到主包中: + +``` +emailContents := email.Contents("M", customerName, nights) +email.Send(emailContents, customerEmail) +``` + +然后我们调用函数`Create`(也公开): + +``` +invoice.Create(customerName, nights, 145.32) +``` + +### 16.2.6 项目树 + +![应用解决方案项目树](https://www.practical-go-lessons.com/img/tree_solution_package_application_2.28246ab9.png)应用解决方案项目树 + +# 17 自我测试 + +## 17.1问题 + +1. 如果你把 :`import _ "github.com/go-sql-driver/mysql"` 加入到程序的导包部分, 会发生什么? +2. 别名“**goq**”导入软件包“**github.com/PuerkitoBio/goquery**”的语法是什么? +3. 怎么才能与他人分享自己的GO代码? +4. 如何在包中发现导出的标识符? + +## 17.2 答案 + +1. 以下语句可以实现什么:`import _ "github.com/go-sql-driver/mysql"` ? + 1. 这是一个空白的导入声明。 + 2. 据说是空白的,因为下划线字符`_`。 + 3. **github.com/go-sql-driver/mysql**软件包的所有init函数都将运行 +2. 导入别名包的语法是什么? + 1. `import goq "github.com/PuerkitoBio/goquery"` +3. 怎么才能与他人分享自己的GO代码? + 1. 在代码托管网站上创建一个git存储库(如Github、GitLab、bitbucket...) + 1. 假设您创建了存储库gitlab.com/loir402/foo + 2. Initialize your module (with`go mod init gitlab.com/loir402/foo`) + 1. 它将在项目的根部创建一个go.mod文件。 + 3. 将您的代码推送到托管网站。 + 4. 将它分享给您的同事和朋友,他们将进口它。 + 5. 请注意,您也可以通过电子邮件或实体邮件发送代码,但可能不是最佳的:) +4. 如何在包中发现导出的标识符? + 1. 他们的第一个字母大写 + 2. 相反,未导出的标识符没有大写的第一个字母 + 1. ex:`const FontSize = 12` is an exported constant identifier + 2. ex:`const emailLengthLimit = 58` is an unexported constant identifier + +# 18个关键取用模式 + +- 包是一组位于同一目录中的源文件 +- 名称标识软件包 +- 导出带有第一个字母大写字母的标识符。 +- 导出的标识符可用于任何其他软件包。 +- 存在于内部目录中的软件包可以在模块的软件包中使用。然而,它们不能被其他模块使用。 +- 模块是“存储在文件树中的Go包的集合,其根部为go.mod文件”[6](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fn6) + +------ + +1. https://blog.golang.org/using-go-modules[↩](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fnref1) +2. 秘密管理软件(https://github.com/hashicorp/vault)[↩↩](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fnref2) +3. 用于管理跨多个主机的容器化应用程序的系统(HTTPS://github.com/kubernetes/kubernetes)[↩](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fnref3) +4. 您可以在这里看到可用软件列表:https://en.wikipedia.org/wiki/List_of_version-control_software[↩](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fnref4) +5. 莱纳斯·托瓦尔德是Linux内核的创建者和主要开发人员。[↩](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fnref5) +6. https://blog.golang.org/using-go-modules[↩](https://www.practical-go-lessons.com/chap-11-packages-and-imports#fnref6) + +# 参考书目 + +- [戴维斯2011理解]戴维斯、丹尼尔、简·伯里和马克·伯里。2011 年。“理解视觉脚本:通过模块化编程改善协作。”国际建筑计算杂志9(4):361-75。 +- [Gauthier1970designing] Gauthier,Richard L和Stephen D Ponto。1970 年。“设计系统程序。” +- [帕纳斯1972标准]帕纳斯,大卫·洛尔赫。1972 年。“关于将系统分解为模块的标准。”ACM 15(12)的通信:1053-58。 \ No newline at end of file