跳到主要内容

Package(包)

包(package)是 Go 组织和复用代码的基本单位。每个 .go 文件都必须先声明自己所属的包;同一目录中的普通 Go 源文件通常属于同一个包,并共同形成一套包级作用域。

最小包示例

下面创建一个模块,其中 greeting 包负责生成问候语,main 包负责启动程序:

package-demo/
├── go.mod
├── greeting/
│ └── greeting.go
└── main.go

package-demo 目录初始化模块:

go mod init example.com/package-demo

greeting/greeting.go

package greeting

func Message(name string) string {
return "Hello, " + name
}

main.go

package main

import (
"fmt"

"example.com/package-demo/greeting"
)

func main() {
fmt.Println(greeting.Message("Go"))
}

在模块根目录运行:

go run .

输出如下:

Hello, Go

包声明与目录

包声明必须出现在源文件的非注释内容开头:

package greeting

需要区分三个名称:

名称示例作用
包名greeting在 Go 代码中限定包内名称
导入路径example.com/package-demo/greeting唯一定位要导入的包
模块路径example.com/package-demogo.mod 中标识整个模块

包名通常与目录最后一段一致,但语言并不要求两者必须相同。调用代码使用的是被导入包声明的包名,而不是自行截取导入路径。为了降低理解成本,除版本目录等惯例场景外,应让它们保持一致。

同一目录中的普通 .go 文件不能混用多个包名。测试文件是一个常见例外:它可以使用当前包名,也可以使用带 _test 后缀的外部测试包名,例如 greeting_test

包级作用域

同一个包可以拆分为多个文件。某个文件声明的包级类型、变量、常量和函数,可以被该包的其他文件直接使用:

// config.go
package greeting

const defaultName = "Go"
// greeting.go
package greeting

func DefaultMessage() string {
return "Hello, " + defaultName
}

文件名不形成命名空间。相反,import 声明只在当前源文件中有效:即使同包的另一个文件已经导入 fmt,当前文件要使用 fmt 时仍需自行导入。

main 包与程序入口

库包用于提供可复用能力;名称为 main 的包用于构建可执行程序。可执行程序还必须提供签名固定的 main 函数:

package main

func main() {
// 程序从这里开始执行。
}

main.main 不接收参数,也不返回值,不能由其他代码显式调用。命令行参数可通过标准库的 os.Args 获取。

包初始化

程序启动时会先初始化完整依赖图,再调用 main.main;包不是等到第一次调用其函数时才懒初始化。一个包会先等待它导入的包完成初始化,再按依赖关系计算包级变量初始值,最后运行无参数、无返回值的 init 函数。同一源文件中的多个 init 按出现顺序执行,但业务逻辑不应依赖不同文件之间的排列顺序。

package settings

import "fmt"

var Mode = loadMode()

func loadMode() string {
fmt.Println("initialize Mode")
return "development"
}

func init() {
fmt.Println("run init")
}

一个包可以声明多个 init 函数,程序不能主动调用它们。工程代码应优先使用显式构造函数和依赖注入;在 init 中执行网络请求、读取不稳定外部资源或启动后台任务,会使失败处理和测试变得困难。

依赖规则

  • Go 不允许包之间形成导入环。例如 order 导入 user 时,user 不能再直接或间接导入 order
  • 导入包后必须实际使用;未使用的导入会导致编译失败。
  • 包名应简短、全小写,并避免下划线或重复上层目录含义。
  • 一个包应围绕一组内聚职责组织,不要只按“工具类”或文件数量拆包。

导入环通常说明职责边界需要调整。可以把双方共同依赖的抽象移到更基础的包,或让高层包通过接口依赖调用方提供的实现,但不应为了消除环而随意创建含义模糊的公共包。

与 TypeScript 模块的区别

TypeScript 文件通常自身就是模块,并通过 export 逐项公开名称。Go 则以目录中的包作为主要编译和封装单位:同包文件共享包级声明,名称能否被其他包访问由标识符首字母决定。Go 模块(module)又位于包之上,负责记录模块路径、Go 版本和依赖关系。

模块版本选择、go.mod、语义导入版本和私有模块配置见 Module Manager(模块管理)

常见错误

  • 把包名写成完整导入路径;package 后只写包名。
  • 认为同包其他文件的导入可以复用;每个源文件都要声明自己的导入。
  • 在库目录中使用 package main,导致它无法作为普通库包导入。
  • 依赖 init 隐式完成关键业务配置,使初始化失败无法明确返回给调用方。
  • 让两个包互相导入;Go 编译器会直接拒绝导入环。

小结

  • 每个 Go 源文件都必须声明包,同目录的普通源文件通常属于同一个包。
  • 包名、导入路径和模块路径相互关联,但不是同一个概念。
  • 同包文件共享包级声明,导入声明则只在当前文件中有效。
  • main 包和 main 函数组成可执行程序入口。
  • 包初始化由依赖初始化、包级变量初始化和 init 函数组成,关键流程宜保持显式。