README.md 7.4 KB

第 15 课:包与模块

学习目标

将可复用逻辑拆到自定义包,理解 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。两个目录仍属于同一课,只是用子目录演示代码拆包,不会修改其他课程。

1. package 是包声明

package score

同一目录里的普通 Go 源文件通常必须属于同一个包。一个包可以由多个源码文件组成,不需要每个文件都写 main。

  • package main:可执行程序所在的包,入口是 func main。
  • package score:普通库包,由其他包导入使用,本身不需要 main。
  • 每个包有自己的命名空间,其他课同名函数不会因此冲突。

不能把 package main 和 package score 的普通源码混放在同一目录,也不要试图导入某个 main.go 文件。导入的是包,不是文件。

2. module 是模块

仓库根目录的模块配置:

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。

3. 大写导出,小写包内使用

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 行改成:

import grades "example.com/go-study/lessons/15_packages/score"

随后使用 grades.Parse 和 grades.Max。不要同时保留两个写法导致重复导入。一般优先使用清晰的默认包名,只有冲突或确有需要时再起别名。

Go 不允许普通的未使用导入;import 了就要实际使用。包之间也不能形成循环导入。本课是 main 导入 score,不让 score 反过来导入 main。

5. 外部测试包

前一课用同包测试,可以访问未导出函数。本课测试文件写:

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 是正常现象。下列命令在仓库根目录执行:

# 查看模块路径
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 子包中。

动手练习

  • 画出 main 导入 score、score 导入标准库的关系。
  • 新增导出常量 Min,在包内校验中使用,并让 main 输出它。
  • 临时将 Parse 改成 parse,观察 main 和外部测试的编译错误,再恢复。
  • 使用 grades 导入别名,修改相应调用,确认行为不变。
  • 在 score 目录新建另一个 package score 源文件,增加函数,理解一个包可以包含多个文件。
  • 给新导出函数补一条外部测试,并运行递归测试命令。

若另行手敲完整例程,放到本课的 practice 子目录,主程序 import 路径也要指向自己创建的子包;不要仅复制 score 目录却忘记修改导入路径,导致仍调用参考实现。

下一课学习接口 interface,认识不同类型如何通过相同的方法约定被统一使用。

上一课:单元测试专题 · 学习总览