根本原因是函数内误用 := 导致声明局部变量而非赋值包级变量,使其他文件访问的仍是 nil;修复需改用 = 赋值、提前声明 err、defer 移至启动后,并避免 init 中跨包调用。

全局变量在跨文件中始终为 nil 的根本原因
不是“忘了初始化”,而是 := 在函数内误用了。当你在 main() 或其他函数里写 db, err := sql.Open(...),Go 会把 db 当作**新声明的局部变量**——哪怕包级已定义 var db *sql.DB。结果是:包级 db 仍为 nil,而局部 db 在函数退出后就销毁了。其他文件(比如 b.go)访问的还是那个未动过的 nil 指针。
修复必须改掉 :=,且确保 err 已声明
把初始化逻辑从“声明+赋值”拆成两步:
-
var err error提前声明err(否则=无法同时赋值两个变量) - 用
db, err = sql.Open(...)而非:= - 别在
main()里加defer db.Close()——它会在main返回时立即关闭连接,后续请求全失败
示例修正:
func main() {
var err error
db, err = sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
if err != nil {
panic(err)
}
// 启动后才 defer,比如在 http.ListenAndServe 前
defer db.Close() // ✅ 放这里才对
}
init 函数里做跨包依赖初始化就是埋雷
比如 a.go 的 init() 调 b.InitDB(),而 b.InitDB() 又依赖 c.Config ——但 c.init() 还没跑,c.Config 就是 nil。Go 不保证不同包 init() 的执行顺序,只保证依赖图内的先后(A import B → B.init 先于 A.init),但同级依赖间无序。
- 禁止在
init()中调用其他包的导出函数,尤其涉及状态初始化的 - 把所有“真工作”移出
init():只留纯常量、简单 map 初始化等无副作用操作 - 统一收口到
main()或显式启动函数中,按需控制顺序
更健壮的做法:用函数封装 + 显式调用
比全局变量更可控的是“懒初始化函数”。例如:
var dbOnce sync.Once
var db *sql.DB
func GetDB() *sql.DB {
dbOnce.Do(func() {
var err error
db, err = sql.Open("mysql", os.Getenv("DSN"))
if err != nil {
log.Fatal(err)
}
})
return db
}
这样既避免了初始化时机错乱,又天然线程安全;调用方无需关心是否已初始化,GetDB() 总返回可用实例。如果项目已用 DI 框架(如 Wire、fx),直接注入 *sql.DB 更彻底。
真正麻烦的从来不是“怎么初始化”,而是“谁在什么时候初始化、谁依赖谁的状态”。跨包全局变量一旦带状态,就等于把初始化顺序问题甩给 Go 运行时猜——它不猜,你程序就崩。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











