init函数在包首次被import时触发,执行于包级变量初始化之后、main函数之前,按依赖拓扑序和文件名字典序执行,不可显式调用且仅当包被实际依赖时才运行。

init 函数不是“启动脚本”,而是包加载的临界点——它只在包被首次 import 时触发,且执行时机严格受限于变量初始化和依赖图。
init 执行时机:var 初始化之后、main 之前,但不等于“程序启动时”
很多人误以为 init 是程序一运行就集体执行的“开机自启函数”。实际并非如此:init 的触发前提是“该包被导入”,且仅当它首次进入构建依赖图时才执行。更关键的是,它总在同包内所有包级 var 初始化完成后才开始运行。
- 如果某个
var初始化调用了未初始化包的函数(比如dbConn = newDB(os.Getenv("DSN")),而os包还没 init),会 panic -
init里读环境变量(os.Getenv)可能返回空字符串——因为环境变量加载通常晚于包初始化阶段 - 空白导入
import _ "net/http/pprof"的唯一目的,就是强制触发该包的init(注册 handler),否则它根本不会加载
同一包内多个 init 的执行顺序:文件名升序 + 文件内代码顺序
Go 编译器按源文件名的字典序加载(如 01_db.go → config.go → z_router.go),每个文件中多个 init 按从上到下顺序执行。这不是语言规范保证的行为,而是当前实现事实;Go 1.21+ 引入 GOEXPERIMENT=fileorder 后更稳定,但仍不应依赖。
- a.go 中的
init读 b.go 声明的全局变量dbConn,但 b.go 名字排在后面 →dbConn还是零值 - 同一文件里写两个
init是合法的,但它们之间无法插入变量初始化逻辑——变量早已初始化完毕 - 不要用文件名控制初始化顺序,应显式拆解为初始化函数并手动调用
跨包 init 顺序:依赖图拓扑排序,而非 import 行顺序
编译器构建有向无环图(DAG),被依赖包的 init 一定先于依赖它的包执行。这个顺序只看直接 import,不追踪间接依赖。
-
mainimporthttp,httpimportnet→ 执行顺序必为net.init()→http.init()→main.init() -
main同时 importdb和cache,二者无直接 import 关系 → 它们的init执行顺序未定义,每次 build 都可能不同 -
import _ "github.com/lib/pq"必须出现在database/sql.Open("postgres", ...)之前,否则驱动未注册,报错sql: unknown driver "postgres"
为什么 init 里连数据库/读配置常失败?
因为 init 是包加载阶段的临界区:所有操作必须无副作用、不阻塞、不依赖运行时输入。但业务逻辑几乎全踩中这些雷。
- 数据库连接需要网络 I/O,
init阶段无法重试或超时控制 - 配置加载依赖
os.Getenv或文件读取,而这些资源可能尚未就绪 - 日志模块在多个第三方 SDK 的
init中互相覆盖,导致关键错误静默丢失 - 真正安全的做法是把初始化逻辑收口到显式函数(如
SetupDB()),在main中按需调用
最易被忽略的一点:你写的 init 可能根本没被执行——只要那个包没被任何路径 import,它的 init 就永远不会跑。别靠它做关键路径的初始化,尤其是跨包协作场景。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











