讲解

包(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 并发。