跳转至

包:Go 的代码组织法,从 fmt 到写自己的包

这一章讲 Go 程序怎么「搭积木」:每一块积木是一个包,包就是一个目录。 读完你会走完两条路——用别人的包(import 的四种写法), 和写自己的包(从 go mod init 到在另一个程序里调用你的函数), 以及一道 Go 的硬墙:循环导入直接编译报错,没有任何商量。

1. 包是什么,为什么程序必须从包开始

书里给的定义:包就是一个组织多个 .go 文件的目录;包里的代码可以在多个 Go 程序间复用1。 命名规矩和系统目录一致(小写字母);带下划线前缀的文件名通常留给测试代码 或绑定特定平台的代码1。

两个必须记住的基本事实:

  • 每个 Go 文件都属于某个包——没有「不属于包的代码」;
  • main 包是唯一特殊的包:它告诉编译器「这是可执行程序,不是给人当库用的」, 编译产物是一个能直接运行的二进制文件。共享库不需要 main 包、也没有 main 函数2。

早期 Go 的磁盘布局还保留一个历史名词:工作区(workspace)根下分 src(源代码)、pkg(编译后的包对象)、bin(可执行文件)三个目录1。 今天只需要知道 go build 会自己管好这些,不用手工维护——Go 1.11 引入的 模块(见第 3 节)已经取代了手动摆目录的老办法,书里讲 src/GOPATH 的部分是新旧两套并存。

2. import:用别人的代码,四种写法

import 声明告诉编译器:这个文件要用到哪些包;包的位置用一个字符串(叫导入路径)指定3。

编译器拿到导入路径后的查找顺序,书里给的原文值得记住: 先看本地目录,再看 vendor 目录,再看 GOROOT/src,最后看 GOPATH/src3。 GOROOT 是 Go 自己的安装目录(装着编译器和标准库); GOPATH 是你放项目和第三方库的工作区目录,默认在用户主目录4。 用 go env 命令可以看这两个变量的当前值4。

写法一:常规导入

import "fmt"
import "math"
// 或一次导多个:
import (
    "fmt"
    "math"
    "github.com/mlowicki/package_name"   // 第三方包的导入路径就是它的网址
)

导入了就有义务用:导而不用,编译报错——这个洁癖从变量推广到了包5。

写法二:别名导入

导入路径太长或想换个短名,用别名:import m "math",之后写 m.Pow(6, 2)5。

写法三:空白导入 _ "math"

只执行这个包的 init 函数,但不用它的任何函数6。 很多数据库驱动、图片格式包靠这个注册自己——你 import 它,只为触发它包里的 init 函数,之后别的代码就能用那些「凭空出现」的能力。书里给的理由: 导了不用要报错,_ 告诉编译器「我知道我在干什么」5。

写法四:点导入 . "math"

包里的导出名字直接进来,不用写前缀:Pow(-6, 2) 而不是 math.Pow(-6, 2)7。 书里立刻给了警告:点导入要慎用——两个包有同名函数时你分不清谁是谁,代码可读性也变差8。

3. 主线例子:从零写一个 factFib 包,再在别人手里跑

这是本章主线,四步走,照原书的完整例子9:

第 ① 步:建模块。 在项目目录里敲 go mod init myproject—— 生成一个 go.mod 文件,这是 Go 模块的出生证明,项目从此有了名字和版本身份9。

第 ② 步:写包。 建目录 custom_package/factFib/,里面放两个文件, 两个文件的第一行都是 package factFib——同一个目录 = 同一个包:

// fact.go
package factFib

func Factorial(num int) int {        // 大写 F!导出的,外面才能用
    if num > 1 {
        return num * Factorial(num-1)
    }
    return 1
}
// fib.go
package factFib

import "fmt"

func Fibonacci(num int) {            // 大写 F!
    f1, f2 := 0, 1
    for i := 1; i <= num; i++ {
        fmt.Println(f1)
        f1, f2 = f2, f1+f2
    }
}

注意每个函数名都是大写开头——回忆拆解 02 的铁律:小写就是包内私有, 写包的人必须主动「签字」把函数交给外面。

第 ③ 步:用包。 主程序按目录结构引用它:

// myprogram.go
package main

import (
    "fmt"
    "custom_package/factFib"     // 导入路径 = 目录的相对路径
)

func main() {
    fmt.Println(factFib.Factorial(10))   // → 3628800
    fmt.Println("Fibonacci series is:")
    factFib.Fibonacci(10)                // → 0 1 1 2 3 5 8 13 21 34
}

第 ④ 步:跑。 go run myprogram.go,两行输出都出来9。

图说:包的边界就是目录的边界;导出规则就是大小写;导入路径就是目录路径。 三件事一句话:包 = 目录 + 首字母大小写管的门禁。

嵌套包

包里还能有包——custom_package/factFib/nested_package/,导入路径跟着目录层层写: import "custom_package/factFib/nested_package"10。 书里补了一条版本信息:Go 1.17 之前,嵌套包得各自维护 go.mod; 1.17 之后,项目根一个 go.mod 管全部依赖,go mod tidy 会清理整个项目里没用的依赖11。

第三方包:导入路径就是网址

用别人发布在 GitHub 上的包,导入路径就是它的网址去掉协议头: import "github.com/user_name/repo_name",工具链会把它下载到本地再编译12。 (下载命令是 go get,书里一带而过;今天模块模式下它已并入 go install 的体系—— 机制不变:给路径,工具链取代码。)

4. 循环导入:Go 说「不」的地方

两个或多个包互相 import,叫循环导入;Go 语言不允许,编译器直接报错。13

书里给的例子:包 one 调包 two 的函数,包 two 又调包 one 的函数—— 编译器走进 one,发现要 two;走进 two,又发现要 one,原地打转,只能报错13。

解法书里给了两个,都值得记住:

  1. 抽一个新包:把双方都要用的东西挪进第三个包,两个包都去 import 它;
  2. 合并成一个包:分析下来如果这俩包谁也离不开谁,说明它们本来就是一个包, 拆开是多余的14。

判断(我们的,不是书里的): 「循环导入编译不过」看起来是限制, 实际是 Go 给所有项目划的一条架构底线:依赖必须能排出先后,代码才有读法。 被这条规矩逼出来的「抽公共包」动作,往往正是设计变好的时刻。 如果错,会错在: 大型项目里偶尔确实需要间接的循环(比如插件回调宿主—— 回调就是「把一个函数交给别人,事情发生时再由它反过来叫你」), 这时业界用接口反转方向的技巧绕过——机制上绕得开,但那是成熟团队的手艺, 不是新手该走的路。

5. top Go libraries:一张生态地图

章末列了一批常用第三方库15,按「什么时候会用到」重排:

库 干什么 你会在什么时候遇见
Testify 测试断言、mock、测试套件 写单元测试的第一天
GORM 数据库 ORM(用 Go 对象操作关系数据库) 项目要连 MySQL/Postgres
Go kit 微服务工具箱 搭服务化的系统
cli 写命令行程序(子命令、flag、帮助文本) 写命令行工具
glog 日志(程序运行时逐条留下的记录) 要给程序加可分级日志
Vegeta HTTP 压测 测接口扛不扛得住
Kubernetes 客户端 调 Kubernetes API 写云原生工具
gocheck 测试检查的增强版 Testify 之外的测试选择
Fuzzy 模糊字符串匹配 做搜索建议
color 终端彩色输出 给 CLI 上颜色

这张表不是背名录,它的用途是:不知道某个领域该用什么库时,先在这里查有没有 业界的默认答案。

6. 作者的判断与证据

书里的说法 性质
循环导入「任何 Go 程序都不需要」 作者的判断,偏强;见我们的判断块
点导入「应谨慎使用」 语言社区的通行共识
GOROOT 不建议手动改 属实,改了会让编译器和标准库版本错位4

7. 边界与局限

  • GOPATH 工作区模型已是旧时代的遗物:今天所有新项目都用模块(go.mod), 书的这一章是新旧两套混讲,读的时候以「go.mod + 模块」为准;
  • go.mod 的语义(版本号、依赖选择、代理)本章没有展开, 只给了 go mod init 一个命令;
  • 「Top libraries」名单带 2025 年成书时的口味,库的新旧更替很快, 用法以各库官方文档为准。

8. 可带走的

  1. 包 = 目录;每个 .go 文件第一行声明它属于哪个包;
  2. main 包 = 可执行程序;没有 main 包的代码是给别人当库用的;
  3. import 查找顺序:本地 → vendor → GOROOT/src → GOPATH/src;
  4. 导而不用 = 编译错;不用可以用 _ 空白导入(为执行 init);
  5. 别名导入换短名,点导入去前缀——点导入要慎用;
  6. 写自己的包:目录 + package 名字 + 导出函数大写开头;
  7. go mod init 项目名 建立模块;1.17 之后一个 go.mod 管全部嵌套包;
  8. 导入路径就是目录相对路径,第三方包就是它的网址;
  9. 循环导入编译直接报错:抽第三个包,或合并成一个包;
  10. 库不知道用什么,先查 Testify/GORM/cli/glog 这张生态表。

9. 原文地图

主题 原书章 原文位置
包的定义与工作区三目录 Packages in Go text/15-fm-learning-go-programming.txt:61(搜「directory/folder」)
main 包与共享库 Packages in Go text/15-fm-learning-go-programming.txt:67(搜「shared library」)
fmt 包 Packages in Go text/15-fm-learning-go-programming.txt:73(搜「formatted I/O」)
import 与查找顺序 Packages in Go text/15-fm-learning-go-programming.txt:85(搜「import path」) · text/15-fm-learning-go-programming.txt:88(搜「vendor directory」)
导出标识符两条规则 Packages in Go text/15-fm-learning-go-programming.txt:121(搜「exported identifiers」)
go build 与可执行文件 Packages in Go text/15-fm-learning-go-programming.txt:151(搜「go build」)
别名导入 Packages in Go text/15-fm-learning-go-programming.txt:154(搜「alias of a package」)
空白导入与 init Packages in Go text/15-fm-learning-go-programming.txt:187(搜「import _ "math"」) · text/15-fm-learning-go-programming.txt:190(搜「init function」)
点导入及警告 Packages in Go text/15-fm-learning-go-programming.txt:193(搜「dot import」) · text/15-fm-learning-go-programming.txt:232(搜「naming conflicts」)
GOROOT / GOPATH Packages in Go text/15-fm-learning-go-programming.txt:253(搜「JAVA_HOME」) · text/15-fm-learning-go-programming.txt:256(搜「workspace directory」)
go mod init 与自定义包 Packages in Go text/15-fm-learning-go-programming.txt:262(搜「go mod init」) · text/15-fm-learning-go-programming.txt:361(搜「factFib.Factorial」)
嵌套包与 Go 1.17 Packages in Go text/15-fm-learning-go-programming.txt:415(搜「package inside a package」) · text/15-fm-learning-go-programming.txt:466(搜「1.17 and later」)
第三方包导入 Packages in Go text/15-fm-learning-go-programming.txt:472(搜「cloning remote code」)
循环导入与两种解法 Packages in Go text/15-fm-learning-go-programming.txt:481(搜「not allowed」) · text/15-fm-learning-go-programming.txt:587(搜「merge both the packages」)
top Go libraries Packages in Go text/15-fm-learning-go-programming.txt:644(搜「rich set of libraries」)

  1. 出处:「Packages in Go」第 61 段(text/15-fm-learning-go-programming.txt:61,搜「directory/folder」)。 ↩↩↩

  2. 出处:「Packages in Go」第 67 段(text/15-fm-learning-go-programming.txt:67,搜「shared library」)。 ↩

  3. 出处:「Packages in Go」第 85、88 段(text/15-fm-learning-go-programming.txt:85,搜「import path」;text/15-fm-learning-go-programming.txt:88,搜「vendor directory」)。 ↩↩

  4. 出处:「Packages in Go」第 244、253、256 段(text/15-fm-learning-go-programming.txt:244,搜「go env」;text/15-fm-learning-go-programming.txt:253,搜「JAVA_HOME」;text/15-fm-learning-go-programming.txt:256,搜「workspace directory」)。 ↩↩↩

  5. 出处:「Packages in Go」第 121、184 段(text/15-fm-learning-go-programming.txt:121,搜「exported identifiers」;text/15-fm-learning-go-programming.txt:184,搜「null container」)。 ↩↩↩

  6. 出处:「Packages in Go」第 190 段(text/15-fm-learning-go-programming.txt:190,搜「init function」)。 ↩

  7. 出处:「Packages in Go」第 193–229 段(text/15-fm-learning-go-programming.txt:217,搜「Pow(-6, 2)」;text/15-fm-learning-go-programming.txt:229,搜「dot import: 36」)。 ↩

  8. 出处:「Packages in Go」第 232 段(text/15-fm-learning-go-programming.txt:232,搜「naming conflicts」)。 ↩

  9. 出处:「Packages in Go」第 259–409 段(text/15-fm-learning-go-programming.txt:262,搜「go mod init」;text/15-fm-learning-go-programming.txt:274,搜「Factorial(num int)」;text/15-fm-learning-go-programming.txt:361,搜「factFib.Factorial」;text/15-fm-learning-go-programming.txt:376,搜「3628800」)。 ↩↩↩

  10. 出处:「Packages in Go」第 415–460 段(text/15-fm-learning-go-programming.txt:448,搜「nested_package」)。 ↩

  11. 出处:「Packages in Go」第 463、466 段(text/15-fm-learning-go-programming.txt:466,搜「1.17 and later」)。 ↩

  12. 出处:「Packages in Go」第 472–475 段(text/15-fm-learning-go-programming.txt:475,搜「user_name/repo_name」)。 ↩

  13. 出处:「Packages in Go」第 481、581 段(text/15-fm-learning-go-programming.txt:481,搜「not allowed」;text/15-fm-learning-go-programming.txt:581,搜「cyclic dependency」)。 ↩↩

  14. 出处:「Packages in Go」第 584–587 段(text/15-fm-learning-go-programming.txt:587,搜「merge both the packages」)。 ↩

  15. 出处:「Packages in Go」第 644–779 段(text/15-fm-learning-go-programming.txt:644,搜「rich set of libraries」)。 ↩