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