Go学习笔记:包管理与项目组织——模块、导入导出与 init 执行顺序

0 阅读5分钟

一、Go 的包系统概述

Go 的包管理相比其他语言更简洁:

语言包管理
Java目录层级 = 包名,import com.example.utils
Node.js每个文件一个模块,require('./utils')
Go一个目录一个包import "module/utils"

Go 的核心规则就一条:一个目录下的所有 .go 文件必须属于同一个包。


二、创建包

2.1 包名规则

  • 包名 = 目录名(约定,不强制但最好遵守)
  • 包名全小写,不用下划线,不用驼峰
  • package xxx 写在每个 .go 文件的第一行

2.2 一个最简单的包

myapp/
├── go.mod         ← module myapp
├── main.gopackage main
└── mathutil/      ← 目录
    └── mathutil.gopackage mathutil
// mathutil/mathutil.go
package mathutil

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

💡 包名不一定要和目录名完全一致,但强烈建议保持一致,否则引用时会造成混乱。


三、导出与私有——大小写决定一切

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

首字母可见性示例
大写导出(公开),其他包可以访问Add(), Config, ErrNotFound
小写私有,只在当前包内可用add(), config, errNotFound
package mathutil

// Add 公开函数——其他包可以用
func Add(a, b int) int {
    return a + b
}

// logCalculation 私有函数——只能在 mathutil 包内用
func logCalculation(name string, a, b, result int) {
    println("[" + name + "]", a, b, "=>", result)
}

// Multiply 公开,内部调用私有函数
func Multiply(a, b int) int {
    result := a * b
    logCalculation("Multiply", a, b, result)
    return result
}
// main.go
import "myapp/mathutil"

sum := mathutil.Add(10, 5)     // ✅ 公开的,可以访问
mathutil.logCalculation(...)    // ❌ 编译错误!私有的,访问不到

这条规则适用于所有命名

package example

var Version = "1.0"        // 导出变量
var version = "1.0"        // 私有变量

const MaxSize = 1024        // 导出常量

type User struct { ... }     // 导出类型
type user struct { ... }     // 私有类型

func NewUser() *User { ... } // 导出函数
func newUser() *User { ... } // 私有函数

四、导入

4.1 标准导入

import (
    "fmt"                    // 标准库
    "os"                     // 标准库

    "myapp/mathutil"         // 项目内的包
    "myapp/greet"            // 项目内的子目录包

    "github.com/xxx/yyy"     // 第三方依赖
)

导入路径 = go.mod 中的 module 名 + 子目录路径

例如 go.mod 中定义的是 module myapp,那么 myapp/mathutil 就对应项目根目录下的 mathutil/ 目录。

4.2 别名导入

当包名冲突或太长时,可以起别名:

import (
    "crypto/rand"
    mathutil "myapp/mathutil"    // 起别名
    . "fmt"                      // 点导入:直接使用函数名(不推荐)
    _ "image/png"                // 空导入:只跑 init,不使用包内功能
)

mathutil.Add(1, 2)    // 用别名调用
Println("hello")      // 点导入后可以直接调用(但会让代码难以理解)

4.3 空导入

有时候导入包只是为了执行它的 init() 函数(比如注册数据库驱动),不需要使用包里的任何函数。这时候用 _

import (
    _ "github.com/go-sql-driver/mysql"   // 只执行 init(),注册驱动
)

五、init 函数

init() 是 Go 的一个特殊函数:

package mathutil

func init() {
    println("[mathutil] 包已初始化")
}

init 的规则

规则说明
自动执行包被导入时自动运行,不需要手动调用
每个包可以有多个 init按文件顺序执行(但建议一个包只写一个)
不能被调用mathutil.init() ❌ 不行
没有参数和返回值func init() 是固定的签名

执行顺序

[mathutil] 包已初始化          ← 1. 先初始化被依赖的包
[greet] 包已初始化              ← 2. 按导入顺序
[greet/chinese] 包已初始化      ← 3. 子包也是独立的包
[main] main 包已初始化          ← 4. main 包最后执行
main() 函数开始执行             ← 5. 所有 init 跑完才进 main

💡 所有 init 都执行完毕后,才会执行 main() 函数。


六、子包(Subpackage)

Go 没有"子包"的概念,greet/chinese 只是一个独立的包 chinese,恰好放在 greet 目录下而已。

greet/               ← package greet(独立的包)
├── greet.go
└── chinese/         ← package chinese(另一个独立的包,和 greet 无关)
    └── chinese.go
import (
    "myapp/greet"           // 导入 greet 包
    "myapp/greet/chinese"   // 导入 chinese 包(两个包是独立的)
)

greet.Greet("pc")               // 用 greet 包
chinese.GoodMorning("pc")       // 用 chinese 包

关键理解: chinese 包不能直接访问 greet 包里的私有内容,即使它在目录结构上是"子目录"。它们是两个完全独立的包。


七、项目结构实例

把前面学过的综合起来,一个标准的 Go 项目结构:

myapp/
├── go.mod                    ← module myapp
├── go.sum                    ← 依赖锁文件
├── main.gopackage main,入口
├── config/
│   └── config.gopackage config
├── handler/
│   ├── handler.gopackage handler
│   └── user.go               ← 同属于 package handler
├── model/
│   └── user.gopackage model
├── service/
│   └── user.gopackage service
└── utils/
    └── helper.gopackage utils
// main.go
package main

import (
    "myapp/config"
    "myapp/handler"
    "myapp/model"
    "myapp/service"
    "myapp/utils"
)

func main() {
    // 按需调用各包的公开函数
}

八、循环依赖——红线

Go 禁止循环依赖,这是编译器的红线:

❌ 错误:
  package a 引用了 package b
  package b 又引用了 package a

编译报错:import cycle not allowed

解决方式:

  1. 提取公共类型到第三个包
  2. 接口解耦——让一方依赖接口,另一方实现接口
  3. 重新设计——通常意味着职责划分不够清晰

九、完整的多包示例

项目结构

gotest/
├── go.mod                  ← module gotest
├── main.gopackage main
├── mathutil/
│   └── mathutil.gopackage mathutil
└── greet/
    ├── greet.gopackage greet
    └── chinese/
        └── chinese.gopackage chinese

mathutil/mathutil.go

package mathutil

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

func Sub(a, b int) int {
    return a - b
}

func Multiply(a, b int) int {
    result := a * b
    logCalc("Multiply", a, b, result)
    return result
}

// 私有函数,仅在包内可见
func logCalc(name string, a, b, result int) {
    println("["+name+"]", a, b, "=>", result)
}

func init() {
    println("[mathutil] init")
}

greet/greet.go

package greet

var count int

func Greet(name string) string {
    count++
    return "你好," + name + "!"
}

func GetCount() int {
    return count
}

func init() {
    println("[greet] init")
}

greet/chinese/chinese.go

package chinese

func GoodMorning(name string) string {
    return "早上好," + name + "!"
}

func init() {
    println("[greet/chinese] init")
}

main.go

package main

import (
    "fmt"
    "gotest/greet"
    "gotest/greet/chinese"
    "gotest/mathutil"
)

func main() {
    fmt.Println(mathutil.Add(10, 5))
    fmt.Println(mathutil.Multiply(4, 3))

    fmt.Println(greet.Greet("pc"))
    fmt.Println(chinese.GoodMorning("pc"))

    fmt.Println("问候次数:", greet.GetCount())
}

func init() {
    println("[main] init")
}

运行输出

[mathutil] init             ← 被依赖的包先 init
[greet] init                ← 按导入顺序
[greet/chinese] init        ← 子包
[main] init                 ← main 包最后
[Multiply] 4 3 => 12        ← 然后进入 main()
15
12
你好,pc!
早上好,pc!
问候次数: 1

十、小结

知识点一句话
包名 = 目录名一个目录下的所有 .go 文件必须同属一个包
大写导出小写私有首字母大写 = 公开,小写 = 私有,没有 public/private 关键字
导入路径module/目录路径
init()自动执行,按依赖顺序,全部 init 跑完才进 main
子包目录上看似父子,实际上是两个独立的包
循环依赖不允许,必须通过接口或公共类型解耦
包级变量在函数外声明的变量,整个包共享,常用于维护状态