讲解
包(package)是 Go 组织代码的基本单位:同一目录下的 .go 文件属于同一个包,包名通常与目录名一致。每个 Go 文件第一行声明自己属于哪个包,import 引入其他包后就能用它们的导出成员。main 包是特殊的——它产出可执行程序,其他包都是被引入的库。
可见性规则简单到只有一条:名字首字母大写即导出(public),小写即包内私有(private)。没有 public/private 关键字,大小写就是访问控制。这条约定贯穿类型、函数、方法、字段、常量,读任何 Go 代码时一眼就能分辨接口边界。
模块(module)是包的集合与依赖管理单位,由根目录的 go.mod 文件定义:go mod init example.com/myapp 创建模块,go get 添加第三方依赖,go build ./... 编译整个模块,go vet ./... 做静态检查。go.mod 记录模块路径和依赖版本,go.sum 记录依赖的校验哈希——两个文件都要提交到版本库。标准库本身也是按包组织的,time、strconv、sort、slices、strings 这些名字你已经见过很多次了。
示例
模块的创建与常用命令(命令行操作):
# 初始化一个模块
mkdir myapp && cd myapp
go mod init example.com/myapp
# 日常三板斧
go build ./... # 编译
go vet ./... # 静态检查
go fmt ./... # 格式化
标准库速览:time 与 strconv:
package main
import (
"fmt"
"strconv"
"time"
)
func main() {
now := time.Now()
fmt.Println("当前年份:", now.Year())
fmt.Println("格式化时间:", now.Format("2006-01-02 15:04"))
n, err := strconv.Atoi("123") // 多返回值:结果 + error
if err != nil {
fmt.Println("转换失败:", err)
return
}
fmt.Println("字符串转整数再加一:", n+1)
}
标准库速览:slices 包的泛型工具函数:
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{5, 2, 8, 1, 9}
slices.Sort(nums)
fmt.Println("排序后:", nums)
fmt.Println("最大:", slices.Max(nums), "最小:", slices.Min(nums))
fmt.Println("包含 8?", slices.Contains(nums, 8))
names := []string{"c", "a", "b"}
slices.Sort(names)
fmt.Println("字符串排序:", names)
}
常见坑
- 包名与目录名不一致:目录叫 utils 文件里却写 package helpers,能编译但 import 路径和包名对不上,使用者要写别名才能读顺。包名跟随目录名是铁律(main 包和 _test 除外)。
- 循环导入:包 A 引 B、B 又引 A 会直接编译失败。这是 Go 在逼你重新划分边界:把公共部分抽成第三个包。
- 提交代码忘了 go.sum:go.mod 记录版本、go.sum 记录校验值,只提交前者会让 CI 或同事的构建失败。两个文件一起提交。
- 把工具函数堆成巨型 utils 包:什么都能往里装的包最后变成依赖沼泽。按业务领域分包(user、order、payment),让每个包的职责一句话说清。
- 以为 init() 函数可以滥用:包级 init() 在导入时自动执行,适合做轻量初始化,但在里面连数据库、起协程会让 import 产生不可控副作用,测试和排查都很痛苦。
小结
目录即包、大写导出、小写私有;go.mod 定义模块与依赖;标准库按包组织随取随用。下一节进入 Go 的王牌领域:goroutine 并发。