使用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

5 回复

你在 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()。你在关闭数据库后尝试继续使用它。 图片

你的单例模式实现存在几个关键问题,我来逐一分析并提供修正方案:

主要问题

  1. 变量作用域问题:在once.Do内部使用:=重新声明了db变量,导致包级变量db未被正确赋值
  2. 错误的defer使用:在单例初始化中调用defer db.Close()会导致数据库连接立即关闭
  3. 缺少错误处理:没有验证数据库连接是否成功建立

修正后的代码

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)
	}
}

关键修改说明

  1. 移除defer db.Close():单例模式中不应该关闭连接,连接会在程序结束时自动清理
  2. 使用包级变量赋值db, err = sql.Open()而不是db, err := sql.Open()
  3. 添加连接验证:在单例初始化时调用db.Ping()确保连接有效
  4. 返回错误信息:让调用方能够处理连接失败的情况
  5. 设置连接池:优化数据库连接性能

替代方案:使用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错误。

回到顶部