9.1 包(Package)基础

基础部分(package 声明、大小写可见性)第 01 章 1.9 节已讲过,这里补充进阶细节

每个 .go 文件第一行都是包声明:

package user    // 这个文件属于 user 包

包的作用

  1. 组织代码:把相关功能的代码放在一起
  2. 命名空间:避免命名冲突(不同包可以有同名函数)
  3. 封装:通过大小写控制可见性

可见性规则(再次强调)

// 文件: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 开始,每个项目都是一个 modulego.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   # 显式最新

这会:

  1. 下载包到 Go module cache($GOPATH/pkg/mod
  2. 更新 go.mod(记录依赖)
  3. 更新 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 时,AgeName 会自动获得值,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 里 useradminprofile 三个包地位完全平等,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-helloch01-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

报错。两者必须同时存在