go中init函数执行顺序确定:同一文件内从上到下、同包不同文件按文件名unicode字典序、跨包按依赖拓扑序;但业务初始化应避免使用init,因其无法错误处理、重试或mock,易导致启动panic。

init 函数的执行顺序是确定的,但依赖它做业务初始化就是给自己埋雷。 它不是“能用”,而是“不该用”——尤其在数据库连接、配置加载、HTTP 服务启动这类场景里。
同一个文件里多个 init 怎么执行?
按源码从上到下顺序执行,没有例外。比如:
func init() { log.Println("first") }
func init() { log.Println("second") }
永远先打 first,再打 second。这不是约定,是编译器硬性保证。
但注意:init 里不能调用本文件尚未声明的变量(哪怕已定义在下方),因为变量初始化发生在 init 之前;而函数声明不受此限——Go 的函数声明是提升的。
- 变量初始化早于所有
init,所以var x = f()中的f()会在第一个init前执行 - 同一文件中,
init之间可以安全共享已初始化的包级变量 - 别把初始化逻辑拆成多个
init来“分步”,合并进一个更清晰
同包不同文件的 init 执行顺序靠什么?
靠文件名 Unicode 字典序,不是 import 顺序,也不是 git 提交顺序。a.go 先于 b.go,01_config.go 先于 02_db.go —— 但这是编译器实现细节,不是语言规范。
常见错误现象:panic: runtime error: invalid memory address,往往是因为 a.go 的 init 访问了 b.go 定义的全局变量,而 b.go 文件名排在后面,变量还没初始化。
- 不要靠文件名排序控制强依赖,比如“必须 config 先于 logger”
- 若真要强制顺序,把相关初始化收进单个文件,或用
01_这类前缀命名(仅作临时 workaround) - 更稳妥的做法:用普通函数封装,比如
LoadConfig()和InitLogger(),由main显式调用
跨包 init 是怎么触发的?
只在包首次被 import 时触发,且会递归初始化其所有未初始化的直接依赖包。比如 import "net/http" 会触发 net、crypto/tls 等依赖包的 init,但不会触发 net/http 间接依赖却未被显式 import 的包。
使用场景唯一合理的地方:驱动注册。例如 import _ "github.com/lib/pq" 就是为了触发它的 init,让 sql.Open("postgres", ...) 能识别驱动。
- 循环 import 直接编译失败,
import cycle not allowed,根本走不到init -
import _ "xxx"是副作用导入,不引入符号,只跑init - 测试文件(
*_test.go)里的init不影响主包初始化流,但go test会单独初始化测试包
为什么生产项目里该避开 init 做业务初始化?
因为它无法处理错误、无法重试、无法 mock、无法跳过,而且 panic 时栈信息残缺,排查困难。
典型反模式:db, _ = sql.Open(...) 放在 init 里 —— DSN 错了就直接 crash,连日志都可能没 flush 完。
-
init不能有参数、不能返回 error、不能 defer、不能 recover - 测试时无法替换依赖(比如用内存 DB 替换真实 PostgreSQL)
- 交叉编译时,条件编译的
init可能不执行,但对应变量仍为零值,导致静默失效 - 真正麻烦的不是顺序难记,而是把初始化塞进
init后,你放弃了对错误传播、生命周期和可测性的控制
复杂点从来不在“怎么让 init 按你想的跑”,而在于一旦它跑了,你就再也没法干预它——包括修复、重试、降级、打标、注入上下文。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











