
在 go 中,使用 `:=` 声明时若左侧变量名与外层作用域(如包级)变量重名,会创建新局部变量而非赋值给全局变量;要修改全局变量必须显式声明 `err` 后用 `=` 赋值,或更推荐——避免全局变量,改用返回值与依赖注入。
Go 的短变量声明操作符 := 本质是带初始化的变量声明,而非单纯赋值。当执行 Conn, err := sql.Open(...) 时,Go 编译器会检查 Conn 和 err 是否已在当前作用域中声明:若任一变量未声明,则整体视为新声明;即使 Conn 在包级已存在(如 var Conn *sql.DB),只要 err 是首次出现,整个语句就会同时声明两个新局部变量——导致包级 Conn 完全未被触及,仍为 nil。
✅ 正确做法一:显式声明 + 普通赋值
func Init(user, pwd, dbname string, port int) error {
var err error
Conn, err = sql.Open("postgres", buildDSN(user, pwd, dbname, port))
if err != nil {
return err
}
// 注意:还需调用 Conn.Ping() 验证连接有效性
return Conn.Ping()
}此处 Conn, err = ... 是多变量赋值(不是声明),因 err 已通过 var err error 显式声明,编译器识别出 Conn 也应指向同名包级变量,从而完成对全局 Conn 的初始化。
❌ 错误写法(常见陷阱):
Conn, err := sql.Open(...) // 创建新局部 Conn,包级 Conn 仍是 nil
⚠️ 更关键的是:应优先避免包级全局变量。全局状态会破坏函数纯度、阻碍并发安全、增加测试难度,并隐含隐藏依赖。Go 社区更推崇显式依赖传递:
✅ 推荐做法二:返回值 + 依赖注入(更健壮、可测试、符合 Go 习惯)
func NewDB(user, pwd, dbname string, port int) (*sql.DB, error) {
db, err := sql.Open("postgres", buildDSN(user, pwd, dbname, port))
if err != nil {
return nil, fmt.Errorf("failed to open DB: %w", err)
}
if err := db.Ping(); err != nil {
db.Close() // 防止资源泄漏
return nil, fmt.Errorf("failed to ping DB: %w", err)
}
return db, nil
}
// 使用方完全掌控生命周期:
func main() {
db, err := NewDB("user", "pass", "mydb", 5432)
if err != nil {
log.Fatal(err)
}
defer db.Close() // 明确释放
// 传入其他函数,无需全局访问
handler := NewHandler(db)
http.ListenAndServe(":8080", handler)
}? 总结:
- := 永远是声明,不是赋值;无法“选择性复用”已有变量;
- 若必须使用包级变量,请用 var x T; x, y = ... 模式;
- 但最佳实践是彻底放弃全局连接对象,改为构造后显式传递(即依赖注入),既提升可测试性,又符合 Go “explicit is better than implicit” 的设计哲学。










