
本文详解如何在 Go 同一包的多个文件间正确声明、初始化并复用未导出(小写)的全局变量(如 *sql.DB),重点解决因误用 := 导致的局部变量遮蔽与 nil 指针问题。
本文详解如何在 go 同一包的多个文件间正确声明、初始化并复用未导出(小写)的全局变量(如 `*sql.db`),重点解决因误用 `:=` 导致的局部变量遮蔽与 nil 指针问题。
在 Go 中,同一包下的所有 .go 文件共享同一个命名空间,因此无需导出(即无需首字母大写)即可跨文件访问包级变量。关键在于:变量必须被正确定义、初始化,且初始化逻辑不能意外创建同名局部变量覆盖全局变量。
? 核心问题::= 遮蔽了全局变量
你代码中的致命错误出现在 models/db.go 的 init() 函数里:
db, err := NewDB(dbinfo) // ❌ 错误!这是短变量声明,创建了局部 db 和 err
该语句实际等价于:
var db *DB // 局部变量,仅作用于 init() 内 var err error // 局部变量 db, err = NewDB(dbinfo) // 赋值给局部变量,全局 db 仍为 nil
结果是:全局 var db *DB 始终保持零值 nil,而 db_util.go 中的 if db != nil 自然失败。
✅ 正确做法:显式声明 + 赋值
需分离声明与赋值,确保修改的是包级变量:
// models/db.go
package models
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq" // PostgreSQL 驱动(按需替换)
)
type DB struct {
*sql.DB
}
var db *DB // ✅ 包级变量,小写,仅本包可见
func init() {
dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable",
DB_USER, DB_PASSWORD, DB_NAME)
var err error
db, err = NewDB(dbinfo) // ✅ 使用 = 赋值,作用于全局 db
if err != nil {
log.Fatal("failed to initialize DB:", err)
}
// 可选:验证连接有效性
if err = db.Ping(); err != nil {
log.Fatal("DB ping failed:", err)
}
// 测试查询(可选)
rows, err := db.Query("SELECT * FROM profile")
if err != nil {
log.Fatal("query failed:", err)
}
defer rows.Close()
}
同时,确保 NewDB 函数返回正确的类型(注意:原示例中 NewDB 返回 *DB,但 sql.Open 返回 *sql.DB,建议统一使用标准库类型以减少封装开销):
func NewDB(dataSourceName string) (*DB, error) {
sqlDB, err := sql.Open("postgres", dataSourceName)
if err != nil {
return nil, err
}
if err = sqlDB.Ping(); err != nil {
return nil, err
}
return &DB{sqlDB}, nil
}
? 在其他文件中安全使用
在 models/db_util.go 中可直接引用 db,无需任何 import 或额外操作:
// models/db_util.go
package models
func (p *Profile) InsertProfile() {
if db == nil {
log.Fatal("database not initialized")
}
_, err := db.Exec("INSERT INTO profile (...) VALUES (...)", p.Name, p.Email)
if err != nil {
log.Printf("insert failed: %v", err)
return
}
}
⚠️ 注意事项与最佳实践
- 避免在 init() 中执行耗时或可能失败的 I/O:虽然 init() 是常见初始化位置,但若数据库不可达,整个程序将 panic。生产环境建议改用显式初始化函数(如 SetupDB()),由 main() 显式调用,便于错误处理和测试。
- 并发安全:*sql.DB 本身是并发安全的,无需额外加锁;但自定义结构体(如 *DB)若添加非线程安全字段,需自行同步。
- 资源清理:记得在程序退出前调用 db.Close()(通常在 main() 的 defer 或 os.Exit 前)。
- 配置注入更佳:硬编码 DB_USER 等常量不利于测试。推荐通过函数参数或配置结构体传入连接信息。
✅ 总结
| 问题 | 正确方案 |
|---|---|
| 跨文件访问未导出全局变量 | ✅ 允许——Go 包内共享命名空间 |
| db 为 nil | ❌ 误用 := 创建局部变量 → ✅ 改用 var err error; db, err = ... |
| 初始化时机 | ✅ init() 可行,但推荐显式初始化函数提升可控性 |
| 连接有效性验证 | ✅ 必须调用 db.Ping(),sql.Open() 不建立真实连接 |
遵循以上原则,你就能在保持封装性(不导出 db)的同时,在整个 models 包中安全、可靠地复用数据库连接。











