9.1 包(Package)基础
基础部分(package 声明、大小写可见性)第 01 章 1.9 节已讲过,这里补充进阶细节。
每个 .go 文件第一行都是包声明:
package user // 这个文件属于 user 包
包的作用
- 组织代码:把相关功能的代码放在一起
- 命名空间:避免命名冲突(不同包可以有同名函数)
- 封装:通过大小写控制可见性
可见性规则(再次强调)
// 文件:mathutil/mathutil.go
package mathutil
var PublicVar = 100 // 大写:包外可访问
var privateVar = 50 // 小写:仅包内可访问
func Add(a, b int) int { ... } // 大写:公开
func helper() { ... } // 小写:私有
// 文件:main.go(在别的包)
package main
import "yourmodule/mathutil"
mathutil.Add(1, 2) // ✅ 大写可访问
mathutil.PublicVar // ✅
// mathutil.helper() // ❌ 小写不可访问
对比 PHP:相当于 PHP 的 public / private,但 Go 用大小写代替关键字,更简洁。
💡 可见性 ≠ 作用域:
- 这里讲的是可见性——标识符能不能跨包访问(大写=能,小写=不能),由大小写决定
- 而作用域是另一个概念——变量在哪些代码块内能被访问,由声明在哪对
{}里决定- 比如包内的一个小写变量,可见性是「私有」(别的包访问不到),但作用域可能是「整个包」或「某个函数内」
- 作用域详见 第 03 章 3.6 节,两个概念别混淆。
9.2 Go Module —— 项目的根
从 Go 1.11 开始,每个项目都是一个 module。go.mod 是项目的根标识文件,类似 package.json。
创建 module
mkdir myproject
cd myproject
go mod init github.com/yourname/myproject
生成的 go.mod:
module github.com/yourname/myproject
go 1.22
添加依赖
go get github.com/gin-gonic/gin # 最新版
go get github.com/gin-gonic/gin@v1.9.1 # 指定版本
go get github.com/gin-gonic/gin@latest # 显式最新
这会:
- 下载包到 Go module cache(
$GOPATH/pkg/mod) - 更新
go.mod(记录依赖) - 更新
go.sum(记录哈希,保证完整性)
管理依赖的常用命令
go mod tidy # 自动添加缺失依赖、删除未用依赖(最常用)
go mod download # 只下载不安装
go mod graph # 查看依赖树
go mod verify # 验证依赖完整性
go list -m all # 列出所有依赖
go get -u # 升级所有依赖到最新次版本
对比 npm:
| npm | Go | |
|---|---|---|
| 初始化 | npm init |
go mod init |
| 配置文件 | package.json |
go.mod |
| 锁文件 | package-lock.json |
go.sum |
| 装依赖 | npm install xxx |
go get xxx |
| 装全部依赖 | npm install |
go mod download |
| 清理 | npm prune |
go mod tidy |
关键差异:Go 没有 go install(无参数)来装全部依赖。Go 在 go build / go run 时会自动下载所需依赖。
9.3 导入包
标准库导入
import "fmt"
import "os"
// 或批量
import (
"fmt"
"os"
"strings"
)
第三方包导入
import "github.com/gin-gonic/gin"
// 使用
r := gin.Default()
给包起别名
import (
f "fmt" // 别名 f
. "strings" // 点导入:直接用包内函数(不推荐)
_ "github.com/lib/pq" // 下划线:只执行 init,不直接使用
)
f.Println("用别名")
// Contains("hello", "ell") // 点导入直接用(容易污染命名空间)
下划线导入 _ 很常见:用于注册驱动、数据库连接等。包内的 init() 函数会执行(第 03 章 3.7 节讲过 init),但你不直接调用包内函数。
举个真实的例子——你用 database/sql 连 MySQL,需要导入 MySQL 驱动:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // ← 下划线导入:只为了让驱动的 init 执行
)
func main() {
// 这里能用 "mysql" 这个驱动名,是因为上面导入时,
// mysql 驱动包的 init() 把自己注册进了 database/sql
db, err := sql.Open("mysql", "user:pass@/dbname")
// ...
}
为什么这样设计? 因为 database/sql 只定义了「怎么用数据库」的接口,不关心具体是 MySQL 还是 PostgreSQL。具体驱动自己负责「在 init 里注册自己」。你只要下划线导入驱动包,它的 init 就跑了,注册就完成了——你不用(也不能)直接调用驱动包的函数。
这种模式叫「自注册」,是 Go 生态的核心机制之一。除了数据库驱动,image 包的图片格式解码、encoding 的子格式注册,都是这个套路。
当你导入一个包时,Go 不会执行这个包里的所有代码,而是只会自动执行包级别的变量初始化和
init()函数。自动执行:包级变量的初始化
包里的所有全局变量(var声明)会在导入时自动计算并赋值。// 假设导入了一个包 "mypkg" package mypkg var Age = 18 // 自动执行,赋值为 18 var Name = getName() // 自动调用 getName() 函数,将返回值赋给 Name func getName() string { return "Tom" }当你导入
mypkg时,Age和Name会自动获得值,getName()也会被自动调用(用于赋值)。自动执行:
init()函数
如果这个包里定义了init()函数,它会在变量初始化完成后自动执行。这是一个特殊的无参、无返回值函数,不能手动调用。package mypkg import "fmt" func init() { // 这个代码在导入时自动执行 fmt.Println("mypkg 包已初始化") }
⚠️ 严禁未使用导入
import (
"fmt"
"os" // ❌ 如果代码里没用 os,编译错误
)
这是 Go 强制的简洁性。不用的 import 必须删除。
9.4 内部包:internal/
Go 有一个特殊约定:internal/ 目录下的包只能被其父目录的包导入。
myproject/
├── go.mod
├── internal/ ← 特殊目录
│ └── config/
│ └── config.go ← 只能被 myproject 内部引用
├── api/
│ └── handler.go ← 可以 import myproject/internal/config
└── main.go
外部项目无法导入你的 internal/,这是 Go 内置的封装机制。
对比 npm:npm 用 private: true 控制包是否发布。Go 的 internal/ 是目录级别的私有控制。
9.5 标准项目结构
Go 社区(非强制)推荐的项目布局:
myproject/
├── go.mod
├── go.sum
├── main.go ← 程序入口
├── cmd/ ← 多个可执行命令(可选)
│ ├── server/
│ │ └── main.go
│ └── cli/
│ └── main.go
├── internal/ ← 私有代码(强烈推荐)
│ ├── handler/ ← HTTP 处理器
│ ├── service/ ← 业务逻辑
│ ├── repository/ ← 数据访问
│ └── model/ ← 数据模型
├── pkg/ ← 可被外部引用的公共库(可选)
│ └── logger/
├── configs/ ← 配置文件
├── migrations/ ← 数据库迁移
├── docs/ ← 文档
└── tests/ ← 集成测试
小项目可以更简单
学习阶段,不要追求复杂的目录结构。一个简单的 Web API 可以这样:
blog-api/
├── go.mod
├── main.go ← 入口
├── handler/ ← 路由处理
├── model/ ← 数据结构
├── service/ ← 业务逻辑
└── config.go ← 配置
9.6 包初始化顺序
Go 程序的初始化顺序:
1. 所有导入的包(深度优先)
2. 包级别变量声明
3. init() 函数(按文件名顺序,可能有多个)
4. main() 函数
package main
import "fmt"
var x = compute() // 2. 先执行
func init() { // 3. 然后 init
fmt.Println("init")
}
func main() { // 4. 最后 main
fmt.Println("main")
}
func compute() int {
fmt.Println("compute")
return 42
}
// 输出:compute → init → main
9.7 多文件包
同一个目录下的所有 .go 文件必须属于同一个包。它们共享命名空间:
user/
├── user.go ← package user
├── user_db.go ← package user
└── user_validate.go ← package user
三个文件可以互相访问对方的私有变量(同包内)。这是组织大文件的方式。
9.8 示例:一个简单的API服务器目录布局
blog-api/
├── go.mod
├── main.go ← 入口,启动服务器
├── internal/
│ ├── config/
│ │ └── config.go ← 配置加载
│ ├── model/
│ │ ├── user.go ← User 数据结构
│ │ └── post.go ← Post 数据结构
│ ├── repository/
│ │ ├── user_repo.go ← 用户数据库操作
│ │ └── post_repo.go ← 文章数据库操作
│ ├── service/
│ │ ├── user_service.go ← 用户业务逻辑
│ │ └── post_service.go ← 文章业务逻辑
│ ├── handler/
│ │ ├── user_handler.go ← HTTP 处理
│ │ └── post_handler.go
│ └── middleware/
│ └── auth.go ← JWT 鉴权中间件
└── configs/
└── config.yaml
分层架构:handler(HTTP 层)→ service(业务层)→ repository(数据层)→ model(数据结构)。每层职责清晰,便于测试和维护。
Node.js / PHP ↔ Go 对照表
| 概念 | Node.js / PHP | Go | 备注 |
|---|---|---|---|
| 模块文件 | 一个 .js 文件 | 一个 .go 文件(属某包) | |
| 包/命名空间 | npm 包 / PHP namespace | package | |
| 项目清单 | package.json | go.mod | |
| 锁文件 | package-lock.json | go.sum | |
| 装依赖 | npm install | go get / go mod download | Go 自动下载 |
| 私有目录 | private: true | internal/ 目录 | Go 目录级私有 |
| 默认导出 | module.exports / export | 大写字母 | Go 用大小写 |
| 命名导入 | import { x } | 包名.大写标识符 | |
| 入口 | package.json main | package main + func main | Go 必须是 main |
| 初始化 | 模块顶层代码 | init() 函数 | Go 显式 |
| 多文件共享 | require 同文件 | 同目录同包 | Go 共享命名空间 |
附:协助理解的一些附属内容
1. 一个常见的疑问:「module 和 package 有什么区别?」
这是最容易混淆的地方,画个层级关系:
module(模块) ← 一个 go.mod 定义,是「项目/库」级别的概念
└── package(包) ← 一个目录下的 .go 文件集合
└── file.go
具体例子,比如项目:
blog-api/ ← 这整个是一个 module
│ module 名: github.com/you/blog-api
│
├── go.mod
├── main.go ← package main(这是一个 package)
└── internal/
├── handler/ ← package handler(另一个 package)
│ └── user_handler.go
├── service/ ← package service
└── repository/ ← package repository
关系:
- 1 个 module = 1 个 go.mod
- 1 个 module 下可以有N 个 package(每个目录一个 package)
- 同一个 module 内的 package 互相 import 用相对路径,比如
import "github.com/you/blog-api/internal/handler"
2. 核心规则:每个目录 = 一个独立的 package
不管父子关系,每个目录(不管嵌套多深)都是独立的 package。
user/ ← package "user"(父目录)
├── user.go │ 顶部写:package user
│ │
├── admin/ ← package "admin"(子目录,独立包!)
│ └── admin.go │ 顶部写:package admin
│ │
└── profile/ ← package "profile"(子目录,独立包!)
└── profile.go │ 顶部写:package profile
三个目录 = 三个独立的 package。它们之间没有自动的继承/包含关系。
关键认知:Go 没有「包继承」
这是和面向对象最大的不同。新手会以为:
❌ 错误想法:admin 是 user 的子包,能直接用 user 的东西
✅ 正确理解:admin 和 user 是两个完全独立的包,要用就得 import
admin 包想用 user 包的东西,必须显式 import:
// admin/admin.gopackage admin
import "github.com/yourname/blog/user" // ← 必须显式导入父目录的包!
func DoSomething() { u := user.User{Name: "张三"} // 像用第三方包一样用}
父目录用子目录也一样,反过来也要 import:
// user/user.gopackage user
import "github.com/yourname/blog/user/admin" // 父用子,也要 import
func UseAdmin() { admin.DoSomething()}
3. 包名 vs import 路径
这里有个容易混淆的点,我分开讲:
import "github.com/yourname/blog/user/admin"// └─────────── import 路径(完整)────────────┘
admin.DoSomething() // ← 用的时候只用包名 "admin"
两个东西:
- import 路径:完整的目录路径(
.../user/admin)—— 唯一定位 - 包名:目录里的
.go文件顶部package xxx声明的名字 —— 用的时候引用
约定:包名 = 目录名最后一段
虽然技术上包名可以随便起,但 Go 社区强烈建议包名和目录名一致:
目录: user/admin/ → 包名: admin ✅(约定)
目录: user/management/ → 包名: admin ❌(别扭,包名和目录名不一致)
比如你博客项目:
internal/handler/ → package handler ✅
internal/service/ → package service ✅
internal/repository/ → package repository ✅
包名和目录名对得上,import 进来用着不绕。
一个完整的例子
假设你的项目结构:
blog/
├── go.mod module: github.com/yourname/blog
├── main.go package main
├── user/ package user
│ ├── user.go
│ ├── admin/ package admin
│ │ └── admin.go
│ └── profile/ package profile
│ └── profile.go
每个包的 import 路径:
| 目录 | 包名 | import 路径 |
|---|---|---|
blog/ |
main | (入口,不 import) |
blog/user/ |
user | github.com/yourname/blog/user |
blog/user/admin/ |
admin | github.com/yourname/blog/user/admin |
blog/user/profile/ |
profile | github.com/yourname/blog/user/profile |
在 main.go 里用这三个包:
package main
import (
"github.com/yourname/blog/user"
"github.com/yourname/blog/user/admin"
// 完整路径,和 user 是兄弟用法
"github.com/yourname/blog/user/profile"
)
func main() {
u := user.New()
admin.DoAdmin()
profile.Load()
}
注意:main.go 里 user、admin、profile 三个包地位完全平等,admin 不会因为「是 user 的子目录」就有特殊待遇。
4. internal/ 目录的魔法(前面讲过)
internal/ 目录下的包只能被 internal 的祖先目录引用。这是 Go 唯一特殊的目录:
blog/
├── internal/ ← 这里的包只能被 blog 内部用
│ └── handler/ 外部项目 import 不了
└── main.go
注意 internal/ 自己不形成包,它只是个标记目录,子目录 handler/ 才是包。所以包名是 handler,import 路径是 .../internal/handler。
5. 模块下根目录下的go文件如何设置包名(三种情况分清楚)
情况 1:根目录是「可执行程序入口」 → package main
// main.go
package main ← 必须是 main!
import "fmt"
func main() { ← 必须有 main 函数! fmt.Println("Hello")}
go.mod 里模块名怎么写都行(github.com/yourname/ch01-hello 或 ch01-hello),但包名必须是 main。这两个是不同的东西:
- 模块名(go.mod 里):全局唯一标识
- 包名(文件顶部):本地命名空间
package main + func main() = Go 编译器识别为「这是一个可执行程序」,编译后会生成 .exe。
情况 2:根目录是「可被引用的库」 → 包名跟模块名走
如果你的项目是个给别人 import 的库(不是可执行程序),根目录的包名应该和模块名最后一段一致:
// go.mod
module github.com/yourname/myutils ← 模块名最后一段是 myutils
// myutils.go(根目录)
package myutils ← 包名和模块名最后一段一致
func Helper() { ... }
别人引用时:
import "github.com/yourname/myutils"
// 用的时候
myutils.Helper()
情况 3:根目录不放任何业务代码(最推荐!)
大型项目的最佳实践——根目录只放 cmd/ 等入口,不放业务 .go 文件:
myproject/
├── go.mod
├── cmd/ ← 入口都放这里
│ └── server/
│ └── main.go ← package main
├── internal/
│ ├── handler/
│ └── service/
这样根目录干净,业务代码都在子包里。Kubernetes、Eino 等大型项目都这么做。
为什么 main 是特殊的?
Go 编译器对 package main 有特殊待遇:
扫描所有包 → 发现 package main + func main()
↓
标记为「可执行程序」
↓
生成可执行文件(而非 .a 库文件)
如果根目录写 package main 但没有 func main():
$ go build
# runtime.main must be a function
报错。两者必须同时存在。