init函数在包首次被导入时自动执行且仅一次,不接受参数、无返回值、不可显式调用;执行时机为包级变量初始化后、main函数前,按依赖图拓扑排序及文件名字典序执行,但不应依赖其协调业务逻辑。

init 函数在包首次被导入时自动执行,且仅执行一次;它不接受参数、无返回值、不可显式调用 —— 但你不能靠它来协调跨文件或跨包的业务初始化逻辑。
init 执行时机:不是 main 前的“启动脚本”,而是包加载临界点
init 在包被首次 import(包括 _ 空白导入)时触发,发生在该包所有包级变量初始化之后、main() 之前。它不是“程序启动时统一跑一遍”,而是按需懒加载:只有真正被依赖的包才会初始化。
- 如果某个包没被任何路径 import,它的
init()永远不会执行 -
import _ "net/http/pprof"这类空白导入,就是为了强制触发目标包的init()(如注册 HTTP handler) - 变量初始化(
var x = f())先于同文件的init()执行;但如果f()调用了其他包的函数,而那个包还没初始化,就会 panic
同一包内多个 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() 是包加载阶段的临界区,所有操作必须无副作用、不阻塞、不依赖运行时输入 —— 但实际业务逻辑几乎全踩中这些雷。
-
os.Getenv("DB_URL")在init()里可能返回空字符串:环境变量加载时机晚于包初始化 -
flag.String("port", "8080", "")在init()里取不到用户传参,因为flag.Parse()在main()里才调用 -
sql.Open(...)可能超时或认证失败,但init()无法重试、无法 recover、无法 log error 后 fallback - 更隐蔽的坑:
init()调log.SetOutput(...),而log包内部依赖未初始化的全局变量,导致 panic
真正可控的做法,是把初始化逻辑拆成普通函数(如 SetupDB()、LoadConfig()),在 main() 开头显式、按需、可测试地调用 —— init 只留给注册驱动、设默认 codec 这类真正“不可跳过”的包级副作用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











