包、模块与 go 工具链
写 Android 时,你有 build.gradle、settings.gradle、一堆插件、几分钟的构建、Gradle 版本和 AGP 版本的相性问题。Go 把这一整摊子几乎全删了:一个 go.mod、一条命令、几秒钟。这一章讲 Go 怎么组织代码,以及它一条会让你先火大、后感激的硬规矩——未使用的东西,一律编译不过。
包:Go 的基本组织单位
Go 代码以包(package)为单位组织。每个 .go 文件开头都要声明自己属于哪个包:
package user // 这个文件是 user 包的一部分
规则很简单:同一个目录下的所有文件,必须属于同一个包。一个包就是一个目录。包内的东西互相可见(不管大小写),包外只能看见首字母大写的(上一章讲的可见性)。程序的入口是特殊的 package main 里的 func main()。
和 Java / Kotlin 最大的不同:Go 没有「一个文件一个 public 类」的规矩,也没有嵌套的包层级关系。包和包之间是平的,靠导入路径引用,不存在「子包能访问父包私有成员」这种事 —— a/b 和 a 是两个毫无特殊关系的独立包。这又是一次减法:删掉了「包的层级可见性」这个你可能都没意识到自己在依赖的复杂规则。
模块:go.mod 就是全部
一堆包组成一个模块(module),模块是版本化、可被别人依赖的单位。一个模块的根目录有一个 go.mod 文件,它小得可怜:
module github.com/you/myapp
go 1.23
require (
github.com/google/uuid v1.6.0
)
就这些。模块名(通常是它的代码仓库地址)、Go 版本、依赖列表。你 import 一个外部包时,写的是它的完整路径:
import (
"fmt" // 标准库
"github.com/google/uuid" // 第三方,路径即身份
)
没有中央仓库、没有 groupId:artifactId:version 那套坐标 —— 包的身份就是它的 URL。你 go get github.com/google/uuid,Go 直接去那个地址拉代码。依赖的精确版本和校验和记在 go.mod 和 go.sum 里,就这两个文件,没有 Gradle 那种层层叠叠的构建脚本。
如果你搜到的老教程让你配 GOPATH、把代码放进 $GOPATH/src/...、纠结目录结构 —— 忘了它。那是 2018 年 Go Modules 出现之前的旧世界。现在你在任何地方 go mod init 就能开一个项目,和它待在硬盘哪个角落毫无关系。看到教 GOPATH 的教程,直接关掉,它至少过时了五年。
那条会让你火大的规矩:未使用即错误
现在讲 Go 最有性格的一条规矩。看这段代码:
package main
import (
"fmt"
"os" // ← 导入了却没用
)
func main() {
x := 10 // ← 声明了却没用
fmt.Println("hi")
}
这段代码编译不过。不是警告,是错误,程序根本跑不起来:
./main.go:5:5: "os" imported and not used ./main.go:9:2: declared and not used: x
第一次撞上这个,几乎每个从别的语言来的人都会火大:「我就临时注释掉一行、留个变量待会儿用,凭什么不让我编译?」在 Java / Kotlin 里,未使用的 import 和变量顶多是个灰色的 IDE 警告,你可以视而不见。Go 把它升级成了硬错误。
但这恰恰是 Go 性格的又一次体现,而且方向很有意思。前两章我们说 Go「替你留白」(删特性);这里它反过来 —— 它不许你留白。一段死代码、一个没用上的 import,就是你留在代码里的一片模糊地带:读的人会想「这个 import 是干嘛的?这个变量是不是漏用了?」Go 直接不给这片模糊地带存在的机会。
Go 在语言层面对你慷慨地留白(删掉一大堆特性,把语言缩小),但在代码层面对你锱铢必较地不许留白(未用的变量、import 一律报错,后面还有 gofmt 强制格式、error 必须处理)。
这两件事其实是同一个信念的两面:代码里的每一个符号都应该有明确的、当下的意义,没有「以后可能会用」的模糊地带。语言小,是为了让每段代码里出现的花样少;不许死代码,是为了让每段代码里出现的东西都算数。目标都是同一个 —— 让读的人不用猜。
用一阵子你会发现,这条规矩其实在帮你:它逼你随手清理,代码库里几乎不会积攒「不知道还有没有用」的僵尸代码和依赖。你想临时留个变量调试?Go 给了个专门的出口 —— 空白标识符 _:
_ = x // 显式告诉编译器:我知道没用它,别报错 result, _ := f() // 只要第一个返回值,第二个明确丢弃
_ 是「我故意不要这个」的正式声明。它把「忘了用」和「故意不用」区分开 —— 又一次,把意图摊到明面上。
一条命令,干所有事
Go 的工具链是内建的、统一的、零配置的。你不需要装 Gradle、配插件、选构建系统。装好 Go,你就有了一个 go 命令,它一个人干所有活:
$ go run main.go # 直接编译并运行(开发时最常用) $ go build # 编译成一个可执行二进制文件 $ go test ./... # 跑当前模块所有测试 $ go fmt ./... # 按官方格式格式化所有代码 $ go vet ./... # 静态检查,揪出可疑写法 $ go mod tidy # 整理依赖,删掉没用到的 $ go get pkg@version # 添加 / 升级一个依赖
没有配置文件(go.mod 不算,它只列依赖不写构建逻辑)、没有构建脚本、没有插件生态。go build 怎么编译,是写死在工具链里的,你不能也不需要定制。又一次减法:删掉了「如何构建」这个可配置维度 —— 所有 Go 项目的构建方式一模一样。
而且它快。Go 从设计之初就把「编译快」当核心目标(还记得第 1 章那 40 分钟的 C++ 编译吗),几万行代码通常几秒钟编完。go run 快到你会把它当解释型语言用 —— 改一行、跑一下,几乎无等待。
在任意空目录里:
go mod init example.com/hello —— 生成 go.mod,项目就成立了。
新建 main.go,写上第 2 章那个五行 hello。
go run . —— 屏幕上出现 hello。
没有 SDK 配置、没有构建文件、没有等待。对比你上一次新建 Android 项目、等 Gradle sync 转圈的体验 —— 这个落差,就是 Go 把多少复杂度删进了语言本身的直接证据。
Kotlin 对照
Android / JVM 的构建是一个需要专门学习的领域。Gradle 的 DSL、AGP 版本、Kotlin 版本、插件、buildTypes、flavors、多模块依赖图、几分钟的构建、时不时的 sync 失败 —— 你可能都习惯到不觉得它复杂了,但它确实是你项目里一大块需要维护的东西。JVM 的可见性也更繁:public / private / protected / internal 四档,还有包和模块的层级。
Go 把这一整片删平了。构建逻辑不可配置(也就没得维护)、依赖就是一个 go.mod、可见性就是大小写一档、工具链一条命令全包。你会失去 Gradle 那种「什么都能定制」的能力 —— 但你也会发现,你其实很少真的需要那种能力,而它的复杂度是你天天在付的税。
还有那条「未用即错误」的规矩:它是 Kotlin 的 @Suppress("unused") 灰色警告的严格版。一开始你会嫌它管得宽,一个月后你会发现你的 Go 代码库里干干净净,没有一行「不知道还用不用得上」的东西 —— 这种整洁是它逼出来的。
这一章的一句话
Go 的代码组织简单到极致:一个目录一个包、一个 go.mod 一个模块、包身份就是 URL;工具链内建、零配置、编译飞快;而「未使用的变量和 import 一律编译错误」这条硬规矩,是 Go 在代码层面「不许你留白」的性格宣言。
卷 I 到此结束 —— 你已经能读懂、能跑起 Go 代码了。接下来卷 II 才是真正开始重装你的直觉:从一个你从没认真想过的问题开始 —— 一个变量,到底是「值」还是「引用」?