使用Golang实现数据库单例模式时遇到的问题
使用Golang实现数据库单例模式时遇到的问题 我在使用数据库时尝试实现单例模式。
main.go
db := dbcon.Singleton()
err := db.Ping() // 这里发生panic
if err != nil {
panic(err)
}
fmt.Println("Successfully connected!")
在dbcon包中,我这样实现单例:
var db *sql.DB // 单例
var once sync.Once
func Singleton() *sql.DB {
once.Do(func() {
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
db, err := sql.Open("postgres", psqlInfo)
if err != nil {
panic(err)
}
defer db.Close()
})
return db
}
但在main.go中我遇到了:
panic: runtime error: invalid memory address or nil pointer dereference
这个实现有什么问题?
更多关于使用Golang实现数据库单例模式时遇到的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你在 once.Do 调用的函数内部重新声明了 db,这遮蔽了全局的 db 变量,因此实际上返回的是一个未初始化的指针。
更多关于使用Golang实现数据库单例模式时遇到的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
如果我注释掉 defer 没有任何变化。但我能够在 Singleton() 内部成功执行 ping 操作,但在 main 函数中它仍然出现 panic。
我发现问题在于没有初始化在 Singleton() 函数外声明的 var db 变量。后来我修改了函数内部的变量名,然后重新赋值给 db 变量,这个方法确实有效,但我不确定这是否是最佳解决方案。
cinematik:
defer db.Close()
这是因为使用了 defer db.Close()。你在关闭数据库后尝试继续使用它。

你的单例模式实现存在几个关键问题,我来逐一分析并提供修正方案:
主要问题
- 变量作用域问题:在
once.Do内部使用:=重新声明了db变量,导致包级变量db未被正确赋值 - 错误的
defer使用:在单例初始化中调用defer db.Close()会导致数据库连接立即关闭 - 缺少错误处理:没有验证数据库连接是否成功建立
修正后的代码
dbcon/dbcon.go
package dbcon
import (
"database/sql"
"fmt"
"sync"
_ "github.com/lib/pq" // PostgreSQL驱动
)
var (
db *sql.DB
once sync.Once
err error
)
// 数据库连接配置
const (
host = "localhost"
port = 5432
user = "your_username"
password = "your_password"
dbname = "your_database"
)
func Singleton() (*sql.DB, error) {
once.Do(func() {
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
// 使用包级变量db,不使用 :=
db, err = sql.Open("postgres", psqlInfo)
if err != nil {
return
}
// 验证连接是否有效
err = db.Ping()
if err != nil {
db.Close()
return
}
// 设置连接池参数
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
})
return db, err
}
main.go
package main
import (
"fmt"
"your_project/dbcon"
)
func main() {
db, err := dbcon.Singleton()
if err != nil {
panic(fmt.Sprintf("Failed to connect to database: %v", err))
}
// 测试连接
err = db.Ping()
if err != nil {
panic(fmt.Sprintf("Database connection lost: %v", err))
}
fmt.Println("Successfully connected!")
// 使用数据库进行查询示例
rows, err := db.Query("SELECT version()")
if err != nil {
panic(err)
}
defer rows.Close()
var version string
for rows.Next() {
err := rows.Scan(&version)
if err != nil {
panic(err)
}
fmt.Printf("Database version: %s\n", version)
}
}
关键修改说明
- 移除
defer db.Close():单例模式中不应该关闭连接,连接会在程序结束时自动清理 - 使用包级变量赋值:
db, err = sql.Open()而不是db, err := sql.Open() - 添加连接验证:在单例初始化时调用
db.Ping()确保连接有效 - 返回错误信息:让调用方能够处理连接失败的情况
- 设置连接池:优化数据库连接性能
替代方案:使用sync.OnceValue(Go 1.21+)
如果你的Go版本≥1.21,可以使用更简洁的实现:
var getDB = sync.OnceValue(func() (*sql.DB, error) {
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
db, err := sql.Open("postgres", psqlInfo)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
db.Close()
return nil, err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
return db, nil
})
func Singleton() (*sql.DB, error) {
return getDB()
}
这样修改后,你的单例模式就能正确工作,避免panic错误。

