|
|
@@ -0,0 +1,196 @@
|
|
|
+# 第 15 课:包与模块
|
|
|
+
|
|
|
+## 学习目标
|
|
|
+
|
|
|
+将可复用逻辑拆到自定义包,理解 package、import、module 的区别,掌握大小写可见性和跨包测试。本课是前面规划的基础语法阶段最后一课,后续进入接口及实际开发。
|
|
|
+
|
|
|
+共享仓库根目录的 `go.mod`,**不要在本课再次初始化模块**。只使用标准库和本仓库的本地包,不安装第三方库,不需要 go.work 或 replace。
|
|
|
+
|
|
|
+## 文件与阅读顺序
|
|
|
+
|
|
|
+```text
|
|
|
+lessons/15_packages/
|
|
|
+├── README.md
|
|
|
+├── main.go # package main,程序入口
|
|
|
+└── score/
|
|
|
+ ├── score.go # package score,可复用的成绩逻辑
|
|
|
+ └── score_test.go # package score_test,外部测试
|
|
|
+```
|
|
|
+
|
|
|
+先敲 [score/score.go](./score/score.go),再敲 [main.go](./main.go),运行成功后再看 [score/score_test.go](./score/score_test.go)。两个目录仍属于同一课,只是用子目录演示代码拆包,不会修改其他课程。
|
|
|
+
|
|
|
+## 1. package 是包声明
|
|
|
+
|
|
|
+```go
|
|
|
+package score
|
|
|
+```
|
|
|
+
|
|
|
+同一目录里的普通 Go 源文件通常必须属于同一个包。一个包可以由多个源码文件组成,不需要每个文件都写 main。
|
|
|
+
|
|
|
+- package main:可执行程序所在的包,入口是 func main。
|
|
|
+- package score:普通库包,由其他包导入使用,本身不需要 main。
|
|
|
+- 每个包有自己的命名空间,其他课同名函数不会因此冲突。
|
|
|
+
|
|
|
+不能把 package main 和 package score 的普通源码混放在同一目录,也不要试图导入某个 `main.go` 文件。导入的是包,不是文件。
|
|
|
+
|
|
|
+## 2. module 是模块
|
|
|
+
|
|
|
+仓库根目录的模块配置:
|
|
|
+
|
|
|
+```go
|
|
|
+module example.com/go-study
|
|
|
+```
|
|
|
+
|
|
|
+模块是管理一组包及其依赖、版本的单位。一个 module 可以包含很多 package。
|
|
|
+
|
|
|
+本课自定义包的导入路径由两部分拼成:
|
|
|
+
|
|
|
+```text
|
|
|
+模块路径:example.com/go-study
|
|
|
+包相对目录:lessons/15_packages/score
|
|
|
+完整路径:example.com/go-study/lessons/15_packages/score
|
|
|
+```
|
|
|
+
|
|
|
+所以主程序写:
|
|
|
+
|
|
|
+```go
|
|
|
+import "example.com/go-study/lessons/15_packages/score"
|
|
|
+```
|
|
|
+
|
|
|
+当前模块中的包由 Go 在本地解析,不会因为路径像网址就去下载它。这个 module 是学习标识,不要求对应真实网站,也不要求与 Git 地址一致。以后发布给外部使用时,再设计可获取的模块路径。
|
|
|
+
|
|
|
+Go 导入路径使用 `/`,不是 Windows 路径中的反斜杠;不要写相对导入 `./score`。
|
|
|
+
|
|
|
+## 3. 大写导出,小写包内使用
|
|
|
+
|
|
|
+```go
|
|
|
+const Max = 100
|
|
|
+
|
|
|
+func Parse(text string) (int, error) {
|
|
|
+ // ...
|
|
|
+}
|
|
|
+
|
|
|
+func valid(value int) bool {
|
|
|
+ // ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- Max、Parse 大写开头:导出,其他包可以访问。
|
|
|
+- valid 小写开头:只允许同一个包访问,不是“只允许同一个文件访问”。
|
|
|
+
|
|
|
+主程序可写 `score.Parse("85")` 和 `score.Max`,不能调用 `score.valid(85)`。函数、类型、变量、常量以及结构体字段都要考虑这条可见性规则。
|
|
|
+
|
|
|
+Parse 是包级函数,不是方法:`score.Parse` 前面的 score 是导入的包名;`student.Birthday` 前面的 student 则是接收者变量。虽然都有点号,含义不相同。
|
|
|
+
|
|
|
+## 4. 导入路径与包名
|
|
|
+
|
|
|
+本课导入路径最后一段是 score,源码声明也是 package score,因此用 `score.Parse` 调用。
|
|
|
+
|
|
|
+导入路径是“到哪里找到包”,包名是“代码里怎样引用它”;两者通常一致,但不是必须一致。也可以显式起别名,例如将现有 import 行改成:
|
|
|
+
|
|
|
+```go
|
|
|
+import grades "example.com/go-study/lessons/15_packages/score"
|
|
|
+```
|
|
|
+
|
|
|
+随后使用 `grades.Parse` 和 `grades.Max`。不要同时保留两个写法导致重复导入。一般优先使用清晰的默认包名,只有冲突或确有需要时再起别名。
|
|
|
+
|
|
|
+Go 不允许普通的未使用导入;import 了就要实际使用。包之间也不能形成循环导入。本课是 main 导入 score,不让 score 反过来导入 main。
|
|
|
+
|
|
|
+## 5. 外部测试包
|
|
|
+
|
|
|
+前一课用同包测试,可以访问未导出函数。本课测试文件写:
|
|
|
+
|
|
|
+```go
|
|
|
+package score_test
|
|
|
+```
|
|
|
+
|
|
|
+这是 Go 对测试的特殊支持:同一目录中的 `_test.go` 文件可以使用 `包名_test` 作为外部测试包名。
|
|
|
+
|
|
|
+它像其他使用者一样显式 import score,只能访问导出的 Max 和 Parse,不能访问 valid。这里没有在 score 目录混放两个普通包,因为 score_test 只用于测试文件。
|
|
|
+
|
|
|
+两种测试方式都合法:
|
|
|
+
|
|
|
+- `package score`:同包测试,可直接测试包内成员。
|
|
|
+- `package score_test`:外部测试,验证对外接口。
|
|
|
+
|
|
|
+本课选择外部测试是为了练习导入和可见性,不代表所有测试都必须这样写。
|
|
|
+
|
|
|
+## 6. 依赖管理命令
|
|
|
+
|
|
|
+当前没有第三方模块依赖,不需要 go get,也没有 go.sum 是正常现象。下列命令在仓库根目录执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+# 查看模块路径
|
|
|
+go list -m
|
|
|
+
|
|
|
+# 查看本课的两个包
|
|
|
+go list ./lessons/15_packages/...
|
|
|
+
|
|
|
+# 根据源码整理模块依赖
|
|
|
+go mod tidy
|
|
|
+```
|
|
|
+
|
|
|
+将来需要第三方库时,再选择可信模块和明确版本执行 `go get 模块路径@版本`,并检查 go.mod / go.sum 的改动。`go install ...@版本` 安装命令行工具,不等于给项目添加库依赖。本课不执行无必要的第三方安装。
|
|
|
+
|
|
|
+## 编译并运行
|
|
|
+
|
|
|
+在**仓库根目录**执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+New-Item -ItemType Directory -Force bin/15_packages | Out-Null
|
|
|
+go build -o ./bin/15_packages/packages.exe ./lessons/15_packages
|
|
|
+.\bin\15_packages\packages.exe
|
|
|
+```
|
|
|
+
|
|
|
+Go 会自动编译入口包依赖的 score 包,不需要手工先编译 score.go。修改任一相关源码后,重新编译程序。
|
|
|
+
|
|
|
+快速编译并运行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+go run ./lessons/15_packages
|
|
|
+```
|
|
|
+
|
|
|
+进入本课目录后也可以执行 `go run .`。不要只写 `go run score/score.go`,因为 score 是库包,不是可执行入口。
|
|
|
+
|
|
|
+## 预期输出
|
|
|
+
|
|
|
+```text
|
|
|
+=== 调用自定义包 ===
|
|
|
+成绩上限:100
|
|
|
+输入 "85" 成功:85
|
|
|
+输入 "100" 成功:100
|
|
|
+输入 "101" 失败:成绩 101 无效,应在 0~100 之间
|
|
|
+输入 "abc" 失败:strconv.Atoi: parsing "abc": invalid syntax
|
|
|
+main 负责组织流程,score 包负责成绩转换与校验
|
|
|
+```
|
|
|
+
|
|
|
+标准库错误文字可能随版本变化,不应逐字依赖。
|
|
|
+
|
|
|
+## 测试
|
|
|
+
|
|
|
+在仓库根目录执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+# 包括主程序目录及所有子包的测试
|
|
|
+go test -count=1 -v ./lessons/15_packages/...
|
|
|
+
|
|
|
+# 只测试 score 包
|
|
|
+go test -v ./lessons/15_packages/score
|
|
|
+```
|
|
|
+
|
|
|
+`./lessons/15_packages` 只指本课入口包;加 `/...` 才会递归包含 score 子包。普通入口包可能显示 `[no test files]`,实际成绩测试在 score 子包中。
|
|
|
+
|
|
|
+## 动手练习
|
|
|
+
|
|
|
+- [ ] 画出 main 导入 score、score 导入标准库的关系。
|
|
|
+- [ ] 新增导出常量 Min,在包内校验中使用,并让 main 输出它。
|
|
|
+- [ ] 临时将 Parse 改成 parse,观察 main 和外部测试的编译错误,再恢复。
|
|
|
+- [ ] 使用 grades 导入别名,修改相应调用,确认行为不变。
|
|
|
+- [ ] 在 score 目录新建另一个 package score 源文件,增加函数,理解一个包可以包含多个文件。
|
|
|
+- [ ] 给新导出函数补一条外部测试,并运行递归测试命令。
|
|
|
+
|
|
|
+若另行手敲完整例程,放到本课的 practice 子目录,主程序 import 路径也要指向自己创建的子包;不要仅复制 score 目录却忘记修改导入路径,导致仍调用参考实现。
|
|
|
+
|
|
|
+下一课学习接口 interface,认识不同类型如何通过相同的方法约定被统一使用。
|
|
|
+
|
|
|
+[上一课:单元测试专题](../14_testing/README.md) · [学习总览](../../README.md)
|