# 第 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)