
本文详解 go 程序中因变量作用域误用导致全局 *sql.db 为 nil 的典型错误,并提供安全、可复用的数据库初始化与共享方案。
你在 go 中定义了全局变量 var db *sql.DB,本意是让所有函数(如 getNames)都能访问同一个数据库连接池。但问题出在 main 函数内这一行:
db, err := sql.Open("sqlite3", "./data.db")
这里使用了短变量声明 :=,它会创建一个新的局部变量 db,而非给全局变量赋值。因此,全局 db 仍为 nil,而 getNames() 调用 db.Query(…) 时触发空指针解引用 panic。
✅ 正确做法是:显式使用赋值操作符 = 并确保 err 已预先声明,从而真正初始化全局 db:
package main import ( "database/sql" "fmt" "log" _ "github.com/mattn/go-sqlite3" ) var db *sql.DB // 全局数据库句柄(连接池) func main() { fmt.Println("Starting test ...") var err Error db, err = sql.Open("sqlite3", "./data.db") // ✅ 使用 = 赋值,作用于全局 db checkErr(err) err = db.Ping() // 验证连接是否可用(非必须但强烈推荐) checkErr(err) fmt.Println(getNames()) } func checkErr(err error) { if err != nil { log.Fatal(err) } } func getNames() []string { query := `SELECT name FROM places` rows, err := db.Query(query) // ✅ 此时 db 已初始化,非 nil checkErr(err) defer rows.Close() var names []string for rows.Next() { var name string if err := rows.Scan(&name); err != nil { checkErr(err) } names = append(names, name) } // 检查 rows.Next() 循环后的潜在错误(如列类型不匹配) if err := rows.Err(); err != nil { checkErr(err) } return names }
? 关键注意事项:
- sql.Open 不会立即建立连接,它只返回一个数据库连接池的句柄;务必调用 db.Ping() 进行首次健康检查。
- defer rows.Close() 必须在 for rows.Next() 循环之后、函数返回前调用,否则可能提前关闭结果集。
- rows.Scan() 后应检查其返回的 error(示例中已补充),且循环结束后需调用 rows.Err() 判断扫描过程是否有错。
- 若项目结构变复杂(如含多个包或 http 服务),建议将 db 封装进配置结构体或使用依赖注入,避免过度依赖全局变量。
? 进阶建议(生产环境):
为提升可维护性与测试性,可将数据库初始化提取为独立函数,并支持自定义连接参数:
func initDB(dataSourceName string) (*sql.DB, error) { db, err := sql.Open("sqlite3", dataSourceName) if err != nil { return nil, err } if err = db.Ping(); err != nil { db.Close() return nil, err } // 可选:设置连接池参数 db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) return db, nil }
然后在 main 中调用:
var err error db, err = initDB("./data.db") checkErr(err)
这样既保持了全局共享,又实现了职责分离与错误集中处理。