404 Not Found

404 Not Found


nginx

包与模块

第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 声明:

GO
package main // 可执行程序的入口包

package mathutil // 工具包
💡 提示:同一个目录下的所有 .go 文件必须声明相同的包名(通常与目录名一致)。

2. 初始化模块

BASH
# 在项目根目录执行
go mod init my-module

# 生成 go.mod 文件:
# module my-module
#
# go 1.24
💡 提示:模块路径通常是仓库地址,如 github.com/username/my-module,方便他人引用。

3. 整理依赖

BASH
# 添加缺失的依赖,移除未使用的依赖
go mod tidy
💡 提示:每次引入新的第三方包后,都应运行 go mod tidy

4. import 语句

GO
import "fmt"                    // 标准库
import "github.com/gin-gonic/gin" // 第三方包

// 分组导入(推荐写法)
import (
    "fmt"
    "log"
    "os"
)

5. 导出规则

GO
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. 安装第三方包

BASH
# 下载包并更新 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:

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:

GO
package main

import (
    "fmt"
    "greet-app/greeting" // 导入本地子包
)

func main() {
    // 使用导出的函数
    fmt.Println(greeting.Hello("小明"))
    fmt.Println(greeting.Goodbye("小明"))

    // ❌ 以下代码会报错:farewell 未导出
    // fmt.Println(greeting.farewell("小明"))
}

运行:

BASH
go run main.go

输出:

TEXT
你好,小明!欢迎学习Go语言。
再见,小明!

示例:使用第三方包(难度⭐⭐)

初始化项目:

BASH
mkdir http-demo && cd http-demo
go mod init http-demo
go get github.com/gin-gonic/gin
▶ 试一试

main.go:

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")
}

运行:

BASH
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:

TEXT
module bank-system

go 1.24

internal/validator/validator.go:

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:

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:

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:

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)
    }
}

运行:

BASH
go run main.go

输出:

TEXT
转账前 - 小明: 10000.00, 小红: 5000.00
转账后 - 小明: 7000.00, 小红: 8000.00
预期错误: 存款失败: 金额必须大于零

实际应用场景

场景1:构建可复用的日志工具包

my-app/
├── go.mod
├── main.go
└── logger/
    └── logger.go

logger/logger.go:

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:

GO
package main

import (
    "my-app/logger"
)

func main() {
    log := logger.New("APP", logger.LevelInfo)

    log.Debug("这条不会显示,因为级别低于Info")
    log.Info("应用程序启动")
    log.Warn("磁盘空间不足")
    log.Error("连接数据库失败")
}

输出:

TEXT
[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:

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:

GO
package model

// User 用户模型
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

internal/repository/user_repo.go:

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:

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)
    }
}

输出:

TEXT
端口: 8080
UserRepository{共2个用户}
  ID=1, Name=小明, Email=xm@example.com
  ID=2, Name=小红, Email=xh@example.com

❓ 常见问题

Q1:包名和目录名必须一致吗?

不强制,但强烈建议一致。Go 规范约定包名应与目录名相同。可以不同,但会造成混乱:

GO
// myutil/calc.go
package calculator // 包名与目录名不一致

// 导入时仍用路径,但引用时用包名
import "my-module/myutil" // 路径
calculator.Add(1, 2)      // 使用包名

Q2:go mod init 的模块路径可以随便写吗?

可以,但推荐使用仓库路径:

BASH
# ✅ 推荐:方便他人引用
go mod init github.com/yourname/yourproject

# ⚠️ 也可以:本地项目
go mod init myproject

# ❌ 避免:含特殊字符或空格
go mod init "my project"

Q3:为什么需要 go.sum 文件?

go.sum 记录了每个依赖的加密哈希值,确保下载的代码没有被篡改。它和 go.mod 一起应提交到版本控制:

BASH
# go.mod — 声明依赖及版本
# go.sum — 校验依赖完整性
# 两者缺一不可
git add go.mod go.sum

Q4:同一个包里有多个文件,怎么组织?

同一目录下的所有 .go 文件共享同一个包,可以直接互相引用,无需导入:

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 包,包含以下可导出函数:

然后在 main.go 中导入并使用它。

练习2:internal 包实践

创建一个项目,包含 internal/config 包用于读取配置(结构体形式),以及 app 包用于使用该配置。验证:在 app 包中可以导入 internal/config,但在项目外部无法导入。

练习3:多包协作计算器

设计一个计算器项目,包含以下包:

要求:scientific 包可以调用 operation 包的函数,所有验证逻辑放在 internal/validator 中。


下一课:结构体实践 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏