包与模块
第11课:包与模块
生活类比:想象一家大型图书馆。每本书都按类别放在不同的书架上——文学、科学、历史……这就是"包"(package)。而图书馆的索引系统告诉你每个书架在哪里,这就是"模块"(module)。你不需要把所有书堆在一个房间里,分门别类才能高效找到你需要的内容。Go 的包系统就是代码世界的图书馆分类法。
核心概念
| 概念 | 说明 |
|---|---|
| 包(package) | Go 源码的组织单元,一个目录下的所有 .go 文件属于同一个包 |
| 模块(module) | 一组相关包的集合,由 go.mod 文件定义,是依赖管理的最小单位 |
| 导入(import) | 使用其他包中导出的标识符 |
| 导出规则 | 大写字母开头的标识符可被外部包访问,小写开头的仅包内可见 |
| internal 包 | 特殊目录名,内部包只能被其父目录树中的代码导入 |
| go get | 从远程仓库下载并安装第三方包 |
包与模块的关系
my-module/ ← 模块根目录,包含 go.mod
├── go.mod
├── main.go ← package main
├── mathutil/ ← 子包 mathutil
│ └── calc.go ← package mathutil
└── internal/ ← 内部包
└── secret.go ← package internal
基本语法与用法
1. package 声明
每个 Go 源文件的第一行必须是 package 声明:
package main // 可执行程序的入口包
package mathutil // 工具包
.go 文件必须声明相同的包名(通常与目录名一致)。
2. 初始化模块
# 在项目根目录执行
go mod init my-module
# 生成 go.mod 文件:
# module my-module
#
# go 1.24
github.com/username/my-module,方便他人引用。
3. 整理依赖
# 添加缺失的依赖,移除未使用的依赖
go mod tidy
go mod tidy。
4. import 语句
import "fmt" // 标准库
import "github.com/gin-gonic/gin" // 第三方包
// 分组导入(推荐写法)
import (
"fmt"
"log"
"os"
)
5. 导出规则
package mathutil
var Pi = 3.14159 // 大写开头 → 可导出,外部可见
var version = "1.0" // 小写开头 → 未导出,仅包内可见
func Add(a, b int) int { // 大写 → 可导出
return a + b
}
func subtract(a, b int) int { // 小写 → 不可导出
return a - b
}
6. internal 包
myapp/
├── internal/
│ └── auth/ ← 只有 myapp/ 及其子目录能导入
│ └── auth.go
└── cmd/
└── server/
└── main.go ← ✅ 可以导入 myapp/internal/auth
internal 是编译器强制的访问控制,不是约定——任何不在其父目录树中的包都无法导入它。
7. 安装第三方包
# 下载包并更新 go.mod / go.sum
go get github.com/gin-gonic/gin
# 指定版本
go get github.com/gin-gonic/gin@v1.9.1
# 删除未使用的依赖
go mod tidy
示例
示例:创建并使用自定义包(难度⭐)
项目结构:
greet-app/
├── go.mod
├── main.go
└── greeting/
└── greet.go
greeting/greet.go:
package greeting
import "fmt"
// Hello 返回问候语(大写开头,可导出)
func Hello(name string) string {
return fmt.Sprintf("你好,%s!欢迎学习Go语言。", name)
}
// farewell 返回告别语(小写开头,不可导出)
func farewell(name string) string {
return fmt.Sprintf("再见,%s!", name)
}
// Goodbye 是 farewell 的导出包装(大写开头,可导出)
func Goodbye(name string) string {
return farewell(name)
}
main.go:
package main
import (
"fmt"
"greet-app/greeting" // 导入本地子包
)
func main() {
// 使用导出的函数
fmt.Println(greeting.Hello("小明"))
fmt.Println(greeting.Goodbye("小明"))
// ❌ 以下代码会报错:farewell 未导出
// fmt.Println(greeting.farewell("小明"))
}
运行:
go run main.go
输出:
你好,小明!欢迎学习Go语言。
再见,小明!
示例:使用第三方包(难度⭐⭐)
初始化项目:
mkdir http-demo && cd http-demo
go mod init http-demo
go get github.com/gin-gonic/gin
main.go:
package main
import (
"net/http"
"github.com/gin-gonic/gin" // 第三方Web框架
)
// User 定义用户结构体
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
r := gin.Default() // 创建默认引擎
// GET 请求:返回欢迎消息
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "欢迎来到Go包管理教程!",
})
})
// GET 请求:返回用户信息
r.GET("/user", func(c *gin.Context) {
user := User{
Name: "小明",
Email: "xiaoming@example.com",
}
c.JSON(http.StatusOK, user)
})
// 启动服务器,监听 8080 端口
r.Run(":8080")
}
运行:
go run main.go
# 服务启动后访问 http://localhost:8080
示例:internal 包与多包协作(难度⭐⭐⭐)
项目结构:
bank-system/
├── go.mod
├── main.go
├── account/
│ └── account.go ← 账户包(公开)
├── internal/
│ └── validator/
│ └── validator.go ← 验证包(仅内部可用)
└── transaction/
└── transaction.go ← 交易包(公开)
go.mod:
module bank-system
go 1.24
internal/validator/validator.go:
package validator
import "errors"
// ValidateAmount 验证交易金额(仅内部包可用)
func ValidateAmount(amount float64) error {
if amount <= 0 {
return errors.New("金额必须大于零")
}
if amount > 1000000 {
return errors.New("单笔交易不能超过100万")
}
return nil
}
// ValidateAccountID 验证账户ID
func ValidateAccountID(id string) error {
if len(id) < 6 {
return errors.New("账户ID长度不能少于6位")
}
return nil
}
account/account.go:
package account
import (
"fmt"
"bank-system/internal/validator" // 导入 internal 包
)
// Account 账户结构体
type Account struct {
ID string
Name string
Balance float64
}
// NewAccount 创建新账户
func NewAccount(id, name string, balance float64) (*Account, error) {
// 使用 internal 包的验证函数
if err := validator.ValidateAccountID(id); err != nil {
return nil, fmt.Errorf("创建账户失败: %w", err)
}
if balance < 0 {
return nil, fmt.Errorf("初始余额不能为负数")
}
return &Account{
ID: id,
Name: name,
Balance: balance,
}, nil
}
// Deposit 存款
func (a *Account) Deposit(amount float64) error {
// 使用 internal 包的验证函数
if err := validator.ValidateAmount(amount); err != nil {
return fmt.Errorf("存款失败: %w", err)
}
a.Balance += amount
return nil
}
// GetBalance 获取余额
func (a *Account) GetBalance() float64 {
return a.Balance
}
transaction/transaction.go:
package transaction
import (
"fmt"
"bank-system/account"
"bank-system/internal/validator" // 同样可以导入 internal 包
)
// Transfer 转账函数
func Transfer(from, to *account.Account, amount float64) error {
// 使用 internal 包验证金额
if err := validator.ValidateAmount(amount); err != nil {
return fmt.Errorf("转账失败: %w", err)
}
if from.GetBalance() < amount {
return fmt.Errorf("转账失败: 余额不足(当前余额: %.2f)", from.GetBalance())
}
// 执行转账
if err := from.Deposit(-amount); err != nil {
return fmt.Errorf("扣款失败: %w", err)
}
if err := to.Deposit(amount); err != nil {
// 回滚
from.Deposit(amount)
return fmt.Errorf("入账失败: %w", err)
}
return nil
}
main.go:
package main
import (
"fmt"
"bank-system/account"
"bank-system/transaction"
// ❌ 以下导入会失败:internal 包不可从外部导入
// "bank-system/internal/validator"
)
func main() {
// 创建两个账户
acc1, err := account.NewAccount("ACC001", "小明", 10000)
if err != nil {
fmt.Println("错误:", err)
return
}
acc2, err := account.NewAccount("ACC002", "小红", 5000)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("转账前 - 小明: %.2f, 小红: %.2f\n", acc1.GetBalance(), acc2.GetBalance())
// 执行转账
err = transaction.Transfer(acc1, acc2, 3000)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("转账后 - 小明: %.2f, 小红: %.2f\n", acc1.GetBalance(), acc2.GetBalance())
// 测试非法金额
err = acc1.Deposit(-100)
if err != nil {
fmt.Println("预期错误:", err)
}
}
运行:
go run main.go
输出:
转账前 - 小明: 10000.00, 小红: 5000.00
转账后 - 小明: 7000.00, 小红: 8000.00
预期错误: 存款失败: 金额必须大于零
实际应用场景
场景1:构建可复用的日志工具包
my-app/
├── go.mod
├── main.go
└── logger/
└── logger.go
logger/logger.go:
package logger
import (
"fmt"
"os"
"time"
)
// Level 日志级别
type Level int
const (
LevelDebug Level = iota // 0
LevelInfo // 1
LevelWarn // 2
LevelError // 3
)
// Logger 日志器结构体
type Logger struct {
prefix string
level Level
}
// New 创建日志器实例
func New(prefix string, level Level) *Logger {
return &Logger{
prefix: prefix,
level: level,
}
}
// log 输出日志的内部方法
func (l *Logger) log(level Level, tag, msg string) {
if level < l.level {
return
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
line := fmt.Sprintf("[%s] [%s] [%s] %s\n", timestamp, tag, l.prefix, msg)
os.Stdout.WriteString(line)
}
// Debug 调试日志
func (l *Logger) Debug(msg string) {
l.log(LevelDebug, "DEBUG", msg)
}
// Info 信息日志
func (l *Logger) Info(msg string) {
l.log(LevelInfo, "INFO", msg)
}
// Warn 警告日志
func (l *Logger) Warn(msg string) {
l.log(LevelWarn, "WARN", msg)
}
// Error 错误日志
func (l *Logger) Error(msg string) {
l.log(LevelError, "ERROR", msg)
}
main.go:
package main
import (
"my-app/logger"
)
func main() {
log := logger.New("APP", logger.LevelInfo)
log.Debug("这条不会显示,因为级别低于Info")
log.Info("应用程序启动")
log.Warn("磁盘空间不足")
log.Error("连接数据库失败")
}
输出:
[2026-06-26 10:30:00] [INFO] [APP] 应用程序启动
[2026-06-26 10:30:00] [WARN] [APP] 磁盘空间不足
[2026-06-26 10:30:00] [ERROR] [APP] 连接数据库失败
场景2:分层架构中的包组织
一个典型的 Web 服务项目结构:
web-service/
├── go.mod
├── go.sum
├── main.go
├── config/
│ └── config.go ← 配置管理
├── internal/
│ ├── model/
│ │ └── user.go ← 数据模型
│ ├── repository/
│ │ └── user_repo.go ← 数据访问层
│ └── service/
│ └── user_service.go ← 业务逻辑层
└── handler/
└── user_handler.go ← HTTP 处理层
config/config.go:
package config
// Config 应用配置
type Config struct {
Port string
DBHost string
DBPort int
LogLevel string
}
// Load 加载配置(简化示例)
func Load() *Config {
return &Config{
Port: "8080",
DBHost: "localhost",
DBPort: 5432,
LogLevel: "info",
}
}
internal/model/user.go:
package model
// User 用户模型
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
internal/repository/user_repo.go:
package repository
import (
"fmt"
"bank-system/internal/model"
)
// UserRepository 用户数据仓库
type UserRepository struct {
users map[int]*model.User
nextID int
}
// NewUserRepository 创建仓库实例
func NewUserRepository() *UserRepository {
return &UserRepository{
users: make(map[int]*model.User),
nextID: 1,
}
}
// Create 创建用户
func (r *UserRepository) Create(name, email string) *model.User {
user := &model.User{
ID: r.nextID,
Name: name,
Email: email,
}
r.users[r.nextID] = user
r.nextID++
return user
}
// FindByID 根据ID查找用户
func (r *UserRepository) FindByID(id int) (*model.User, bool) {
user, ok := r.users[id]
return user, ok
}
// FindAll 查找所有用户
func (r *UserRepository) FindAll() []*model.User {
result := make([]*model.User, 0, len(r.users))
for _, u := range r.users {
result = append(result, u)
}
return result
}
// String 返回仓库摘要
func (r *UserRepository) String() string {
return fmt.Sprintf("UserRepository{共%d个用户}", len(r.users))
}
main.go:
package main
import (
"fmt"
"web-service/config"
"web-service/internal/repository"
)
func main() {
// 加载配置
cfg := config.Load()
fmt.Println("端口:", cfg.Port)
// 使用仓库
repo := repository.NewUserRepository()
repo.Create("小明", "xm@example.com")
repo.Create("小红", "xh@example.com")
fmt.Println(repo)
// 查找所有用户
for _, u := range repo.FindAll() {
fmt.Printf(" ID=%d, Name=%s, Email=%s\n", u.ID, u.Name, u.Email)
}
}
输出:
端口: 8080
UserRepository{共2个用户}
ID=1, Name=小明, Email=xm@example.com
ID=2, Name=小红, Email=xh@example.com
❓ 常见问题
Q1:包名和目录名必须一致吗?
不强制,但强烈建议一致。Go 规范约定包名应与目录名相同。可以不同,但会造成混乱:
// myutil/calc.go
package calculator // 包名与目录名不一致
// 导入时仍用路径,但引用时用包名
import "my-module/myutil" // 路径
calculator.Add(1, 2) // 使用包名
Q2:go mod init 的模块路径可以随便写吗?
可以,但推荐使用仓库路径:
# ✅ 推荐:方便他人引用
go mod init github.com/yourname/yourproject
# ⚠️ 也可以:本地项目
go mod init myproject
# ❌ 避免:含特殊字符或空格
go mod init "my project"
Q3:为什么需要 go.sum 文件?
go.sum 记录了每个依赖的加密哈希值,确保下载的代码没有被篡改。它和 go.mod 一起应提交到版本控制:
# go.mod — 声明依赖及版本
# go.sum — 校验依赖完整性
# 两者缺一不可
git add go.mod go.sum
Q4:同一个包里有多个文件,怎么组织?
同一目录下的所有 .go 文件共享同一个包,可以直接互相引用,无需导入:
// mathutil/add.go
package mathutil
func Add(a, b int) int { return a + b }
// mathutil/multiply.go — 同一包,可直接使用 Add
package mathutil
func Multiply(a, b int) int {
result := 0
for i := 0; i < b; i++ {
result = Add(result, a) // 直接调用,无需导入
}
return result
}
📖 小节
| 要点 | 内容 |
|---|---|
| package 声明 | 每个 .go 文件第一行必须声明包名 |
| 模块初始化 | go mod init <模块路径> 创建 go.mod |
| 依赖管理 | go mod tidy 整理依赖,go get 安装第三方包 |
| 导出规则 | 大写开头 = 可导出,小写开头 = 仅包内可见 |
| internal 包 | 编译器强制的访问控制,仅父目录树可导入 |
| 最佳实践 | 包名与目录名一致,按职责分包,避免循环依赖 |
📝 作业
练习1:创建字符串工具包
创建一个 stringutil 包,包含以下可导出函数:
Reverse(s string) string— 反转字符串ToUpper(s string) string— 转大写WordCount(s string) int— 统计单词数量
然后在 main.go 中导入并使用它。
练习2:internal 包实践
创建一个项目,包含 internal/config 包用于读取配置(结构体形式),以及 app 包用于使用该配置。验证:在 app 包中可以导入 internal/config,但在项目外部无法导入。
练习3:多包协作计算器
设计一个计算器项目,包含以下包:
calc/operation— 基本运算(加减乘除)calc/scientific— 科学运算(幂运算、阶乘)internal/validator— 输入验证(除零检查、负数检查等)
要求:scientific 包可以调用 operation 包的函数,所有验证逻辑放在 internal/validator 中。



