包与导入基础
本教程共 80 篇 · 第 6 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
6. 包与导入基础
本节目标:搞懂 Go 的包机制,学会声明包、导入包、控制可见性,写出能被别人复用的代码。
什么是包
Go 用**包(Package)**来组织代码。一个包就是同一个目录下的一组 .go 文件,它们共享同一个 package 声明。
你可以把包理解成一个文件夹里的所有代码文件。这些文件里的函数、变量、类型,互相之间可以直接访问,不需要导入。
来看个例子:
myproject/
go.mod
main.go
utils/
helper.go
math.go
这里 main.go 属于 main 包,utils/ 目录下的两个文件属于 utils 包。helper.go 和 math.go 里的代码互相可以直接用,不需要 import。
package 声明
每个 .go 文件的第一行(注释除外)必须是 package 声明:
package main // 可执行程序
package utils // 库包
包名通常和目录名一致。比如 utils/ 目录下的文件写 package utils。但也有例外,比如 main 包的目录名通常不叫 main。
main 包的特殊地位
main 包是 Go 程序的入口。只有 package main 里才能有 func main(),这是程序运行的起点。
package main
import "fmt"
func main() {
fmt.Println("程序从这里开始执行")
}
可执行程序必须有且只有一个 main 包和 main 函数。库包不需要 main 函数。
import 导入
要用别的包里的东西,就得先导入。
单个导入
import "fmt"
导入标准库的 fmt 包,之后就能用 fmt.Println 这些函数了。
批量导入
多个包建议用括号括起来,这是 Go 推荐的风格:
import (
"fmt"
"math"
"strings"
)
你也可以每个都单独写一行 import,但不如这种写法整洁。
导入路径
导入路径分两种情况:
标准库:直接写包名
import "fmt"
import "math/rand"
第三方或自己的包:写完整的模块路径
import "github.com/google/uuid"
import "example/hello/utils"
导入路径 = 模块路径 + 包在模块里的子目录路径。
别名导入
如果两个包名一样,或者包名太长,可以起别名:
import (
f "fmt" // 别名为 f
"math/rand" // 包名是 rand
crand "crypto/rand" // 别名为 crand,避免和 math/rand 冲突
)
用了别名之后,只能用别名来调用,不能用原名:
f.Println("hello") // 正确
fmt.Println("hello") // 错误,fmt 未定义
匿名导入
如果你只需要执行某个包的初始化代码(init 函数),但不需要用它的导出内容,可以用下划线 _ 做匿名导入:
import (
_ "github.com/lib/pq" // 注册 PostgreSQL 驱动,但不直接调用
)
这在数据库驱动注册的场景很常见。驱动包的 init 函数会把自己注册进去,你不需要手动调用它。
可见性规则
Go 控制可见性的方式特别简单:看首字母大小写。
- 大写字母开头:导出的(Exported),包外可以访问
- 小写字母或下划线开头:未导出的(Unexported),只有包内能访问
没有 public、private、protected 这些关键字。
package utils
const PublicName = "Go" // 包外可以访问
const privateName = "golang" // 只有 utils 包内能访问
func PublicFunc() {} // 包外可以调用
func privateFunc() {} // 只有 utils 包内能调用
type User struct { // 包外可以访问
Name string // 包外可以访问
email string // 只有 utils 包内能访问
}
Note这个规则适用于所有名称:函数、变量、常量、类型、结构体字段。大写就是公开的,小写就是私有的,就这么简单。
来写一个完整的例子
我们创建一个简单的模块,包含 main 包和一个 greeting 包。
项目结构:
greet/
go.mod
main.go
greeting/
greeting.go
初始化模块
mkdir greet
cd greet
go mod init example/greet
写 greeting 包
创建 greeting/greeting.go:
package greeting
import "fmt"
// Hello 返回问候语,大写开头,包外可访问
func Hello(name string) string {
msg := fmt.Sprintf("Hello, %s!", name)
return msg
}
// help 是小写的,包外访问不了
func help() string {
return "this is private"
}
写 main 包
创建 main.go:
package main
import (
"fmt"
"example/greet/greeting"
)
func main() {
msg := greeting.Hello("Go")
fmt.Println(msg)
// greeting.help() // 编译报错:help 没有导出
}
运行
go run .
输出:
Hello, Go!
如果你取消注释 greeting.help() 那行,编译会报错,因为 help 是小写的,外部访问不了。
包的初始化顺序
Go 在导入包时会执行初始化:
- 先初始化导入的包(按依赖顺序,被依赖的先初始化)
- 执行包级别变量的声明
- 执行
init函数(如果有的话) - 最后执行
main函数
每个包只会被初始化一次,不管被导入多少次。
init 函数是一种特殊的函数,不需要调用,Go 会在包初始化时自动执行:
package greeting
func init() {
// 这个函数在包被导入时自动执行
fmt.Println("greeting 包初始化了")
}
一个文件里可以写多个 init 函数,它们按出现顺序执行。
内部包
Go 有一个特殊约定:路径中包含 /internal/ 的包,只能被 /internal/ 的父目录下的包导入。
比如模块 example/myapp 的结构:
myapp/
go.mod
main.go
internal/
config/
config.go
handler/
handler.go
main.go可以导入example/myapp/internal/configexample/myapp/handler可以导入example/myapp/internal/config- 但外部模块不能导入
example/myapp/internal/config
这让你可以放心地把不想对外暴露的实现细节放在 internal/ 目录下。
常见问题
循环导入
Go 不允许循环导入。如果 A 导入了 B,B 就不能再导入 A。
// packageA/a.go
package packageA
import "example/mod/packageB" // A 导入了 B
// packageB/b.go
package packageB
import "example/mod/packageA" // 报错!B 不能导入 A
遇到循环导入,通常的解决办法是:
- 把公共部分提取到第三个包
- 用接口解耦
- 重新审视包的职责划分
导入了但没用
Go 编译器会报错:导入了包却没使用。这是 Go 的设计选择,强制你保持代码整洁。
import (
"fmt"
"math" // 编译报错:imported and not used
)
包名和导入路径不一致
大多数情况下包名和导入路径最后一段一致。但有时候不一致,比如 gopkg.in/yaml.v3 的包名是 yaml。这时候你要注意实际包名是 yaml,不是 yaml.v3。
Tip如果不确定一个包的实际包名,导入之后看编辑器的提示,或者用
go doc查一下。
学完这节你能做什么
- 理解 Go 包的组织方式
- 声明包、导入包、使用别名和匿名导入
- 用大小写控制可见性
- 写一个包含多个包的小项目
- 知道循环导入怎么避免
下一节进入基础语法的学习,从 token、标识符、关键字开始。