Go 的"包"是组织代码的唯一单元。一个目录就是一个包,没有头文件,没有 .cpp/.h 分离。 理解包的规则,是读懂 Go 项目结构的第一步。


1. 核心概念速览

package 声明

每个 .go 文件的第一行必须是 package 声明,且同一目录下所有文件必须声明相同的包名。

// 文件: mathutil/add.go
package mathutil

// 同目录的 subtract.go 也必须是:
// package mathutil

func Add(a, b int) int { return a + b }

package main — 程序入口

package main 是特殊约定,表示可执行程序的入口,不是库包。Go 没有独立的入口函数声明——入口就是 package main 里的 func main()。

package main

import "fmt"

func main() {
    fmt.Println("Hello, Go!")
}

C++ 对照: package main ≈ 包含 int main() 的那个编译单元。


2. C++ 对照速查表

Go 概念C++ 对应差异说明
packagenamespace + #include一个 package 同时提供命名空间和导入功能,不需要头文件
import#includeimport 加载的是编译好的包,不是文本包含
首字母大写 = 导出public / protected基于命名规则,不是关键字;小写 = 包内可见
首字母小写 = 私有private同上,没有 private 关键字
一个目录一个包一个模块/目录同目录必须同 package,不可混装
init()全局变量的构造函数更可靠,保证在 main() 之前执行
import . "fmt"using namespace std;同样不推荐,污染命名空间
循环导入循环 #include两者都是编译错误
internal/C++ 无直接对应类似 friend 的简化版,限制子包的可见性
go.mod 依赖管理CMake / Conan / vcpkgGo 原生支持,无需第三方工具

3. 导入语法

单行导入

package main

import "fmt"

func main() {
    fmt.Println("单行导入")
}

分组导入(推荐)

package main

import (
    "fmt"
    "os"
    "strings" // 标准库放一组
)

func main() {
    fmt.Println(strings.Join(os.Args[1:], " "))
}

C++ 对照: Go 的 import 会自动加载包内所有导出符号,不需要逐个 include 头文件。

导入别名

import (
    pb "github.com/golang/protobuf/proto" // 给长路径起短别名
    myrand "math/rand"                    // 给标准库起别名(少见但合法)
)

使用时用别名代替原名:pb.Marshal(msg) 而不是 proto.Marshal(msg)。

特殊导入方式

import (
    . "fmt"    // 点导入:直接使用包内符号,无需前缀(不推荐!)
    _ "net/http/pprof" // 空导入:只执行 init(),不使用包内符号
)

func main() {
    Println("等同于 fmt.Println") // 点导入后可以直接用
}
  • . 点导入: 把包的所有导出符号导入当前命名空间,类似 C++ 的 using namespace std;
  • _ 空导入: 只为触发 init() 副作用(如注册驱动、注册插件),C++ 没有对应概念

C++ 对照: import . "fmt" ≈ using namespace fmt;,同样不推荐在生产代码中使用。


4. 可见性规则(导出 vs 私有)

Go 没有 public / private / protected 关键字。首字母大小写决定可见性:

package mathutil

// 首字母大写 → 导出(public),其他包可以调用
func Add(a, b int) int {
    return a + b
}

// 首字母小写 → 私有(package 内部可见)
func validate(n int) bool {
    return n >= 0
}

// 结构体字段同理
type User struct {
    Name string  // 导出字段
    age  int     // 私有字段:外部包无法直接访问
}

C++ 对照: 这相当于基于命名规则的 public / private,而不是关键字。 好处是强制你从 API 设计角度思考可见性;坏处是重构时改个大小写就改变了可见性。

导出规则总结:

名称示例外部包可见?
大写开头Add, Name, URL是
小写开头add, name, url否(仅本包内)
下划线开头_helper否(小写开头)
数字开头1stFunc非法标识符

5. init() 函数

每个包可以有多个 init() 函数,甚至每个 .go 文件都可以有自己的 init()。

package config

import "fmt"

func init() {
    fmt.Println("config 包的 init() 执行了")
    // 通常在这里做初始化:读配置、注册驱动等
}

func init() {
    // 同一个文件可以有多个 init(),按顺序执行
    fmt.Println("config 包的第二个 init()")
}

执行顺序

程序启动时,Go 按依赖关系自动执行所有 init():

1. 递归初始化所有被导入的包(最深层依赖先执行)
2. 同一个包内:按文件名字母序执行各文件的 init()
3. 同一个文件内:按出现顺序执行多个 init()
4. 最后执行 main 包的 init()
5. 最后执行 main()
// 示例:观察初始化顺序
package main

import (
    "fmt"
    "example.com/a" // a 包的 init() 先执行
)

func init() {
    fmt.Println("main 包的 init()")
}

func main() {
    fmt.Println("main()")
    // 输出顺序: a 包 init → main 包 init → main()
}

C++ 对照: init() 类似全局变量的构造函数,但更可靠——不需要担心"静态初始化顺序惨案"(SIOF),因为 Go 保证按依赖顺序初始化。

init() 的常见用途

package mydb

import "database/sql"

func init() {
    // 注册数据库驱动:导入时自动生效
    sql.Register("mydriver", &MyDriver{})
}

对应 C++ 中 __attribute__((constructor)) 或静态对象的构造函数,但 Go 的方式更可预测。


6. 循环导入

Go 严禁循环导入,编译直接报错:

// package a
package a

import "example.com/b" // a 导入 b

func Hello() string { return b.World() }

// package b
package b

import "example.com/a" // b 又导入 a → 编译错误!
import cycle not allowed
package example.com/a
    imports example.com/b
    imports example.com/a

C++ 对照: C++ 的循环 #include 通过前向声明(forward declaration)可以部分解决,Go 没有这个机制。 解决方案: 提取公共部分到第三个包,或使用接口解耦。


7. internal/ 包(受限可见性)

internal/ 目录下的包只能被其父目录导入,这是 Go 语言级别的强制规则。

myproject/
├── internal/
│   └── secret/          ← 只有 myproject/ 下的包能导入
│       └── key.go
├── pkg/
│   └── public/          ← 这里可以 import "myproject/internal/secret"
│       └── api.go
└── cmd/
    └── main.go          ← 这里也可以 import "myproject/internal/secret"
// pkg/public/api.go
package public

import "myproject/internal/secret" // OK:在 myproject/ 树内

func GetKey() string {
    return secret.Key()
}
// other-project/main.go
package main

import "myproject/internal/secret" // 编译错误!不在 myproject/ 树内

C++ 对照: 没有直接对应。可以类比为一种极简的 friend 机制——只要在同一个项目目录树下就能访问。


8. 第三方包与 go mod

Go 使用 go.mod 管理依赖,无需额外工具(类似 CMake 的 find_package,但内置且更简单)。

# 初始化模块
go mod init example.com/myproject

# 添加依赖(import 后自动下载)
go mod tidy

# 查看依赖图
go mod graph
// go.mod 示例
module example.com/myproject

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    google.golang.org/grpc v1.58.0
)

import path 规则

import (
    "fmt"                        // 标准库:直接写包名
    "os"                         // 标准库
    "github.com/gin-gonic/gin"   // 第三方:完整模块路径
    "example.com/myproject/pkg"  // 项目内:完整模块路径 + 子包
)

9. 常见坑

坑 1:导入但不使用 → 编译报错

package main

import "fmt" // 错误:imported and not used

func main() {
    println("hello") // 没用到 fmt
}
imported and not used: "fmt"

C++ 不会因为 include 了头文件但没用就报错。Go 强制你保持导入列表干净。

坑 2:init() 执行顺序不直观

// main.go
package main

import (
    _ "example.com/a" // 空导入,只为触发 a 的 init()
    "fmt"
)

func main() {
    fmt.Println("main")
}
# a 的 init() 先于 main() 执行,甚至先于 main 包的 init()
# 如果 a 的 init() 依赖环境变量或文件,调试时很容易困惑

坑 3:包名和目录名不一致

// 文件路径: utils/string.go
package strutil  // 包名和目录名不一致

func Upper(s string) string { return s } // 调用时用 strutil.Upper()

虽然合法,但强烈不建议。惯例是包名 = 目录名,否则使用者会困惑。

坑 4:import . “fmt” 污染命名空间

package main

import . "fmt"

func main() {
    Println("hello") // 看得出来 Println 来自哪里吗?
}

多人协作时,. 导入会让代码可读性严重下降——你无法快速判断函数来自哪个包。

坑 5:package main 是特殊入口

// lib/main.go
package main  // 这不是库包!go build 会生成可执行文件,不是 .a 文件

func Helper() string { return "" } // 别人无法 import "你的模块/lib" 来用这个函数

如果你要写库,不要用 package main。

坑 6:循环导入是编译错误

循环导入在编译期就会被捕获,不是运行时错误。如果你的包结构出现循环依赖,需要重构。

坑 7:同一包可以跨多个文件

// mathutil/add.go
package mathutil
func Add(a, b int) int { return a + b }

// mathutil/subtract.go
package mathutil
func Subtract(a, b int) int { return a - b }

这两个文件共享同一个包空间,可以直接互相调用函数(包括私有函数),无需导入。


10. 练习任务

  • 创建一个 mypkg 包,包含 Greet(name string) string 函数,在 main 包中导入并调用
  • 在 mypkg 中添加一个未导出函数 normalize(s string) string,尝试在 main 中调用,观察编译报错
  • 给 mypkg 添加一个 init() 函数打印日志,观察它在 main() 之前执行
  • 创建两个包 a 和 b,让它们互相导入,观察循环导入的编译错误
  • 在项目中创建 internal/ 目录,把一个包放进去,尝试从项目外部导入它
  • 使用 import . "fmt" 重写一个程序,然后去掉点导入重写一遍,体会命名空间污染问题
  • 在一个包中创建两个文件,各自有 init() 函数,观察它们的执行顺序
  • 运行 go mod init + go mod tidy,导入一个第三方包(如 github.com/stretchr/testify),完成一个简单的断言测试