如何避免在使用开启/关闭数据库的用例(上下文管理)中出现代码重复?

huangapple go评论80阅读模式
英文:

How to avoid code-duplication with open/close database use-case (context management)?

问题

刚开始学习Go语言,对以下情况有些疑惑:

我有一个非常简单的代码库,我只想打开/关闭一个数据库连接并执行一个简单的查询。我可以按照以下方式实现(这里只展示重要部分):

import (
	"database/sql"
	_ "github.com/lib/pq"
)

func (db *Database) ExecQueryA() {
    dbConn, err := sql.Open("postgres", db.psqlconn)
    if err != nil {
        panic(err)
    }
    defer dbConn.Close()

    _, err = db.Exec(...
    if err != nil {
        panic(err)
    }
}

上述方法可以正常工作,但是如果我想再写x个类似的函数,我不想重复以下这部分代码:

dbConn, err := sql.Open("postgres", db.psqlconn)
if err != nil {
    panic(err)
}
defer dbConn.Close()

在每个函数的开头(即我想避免代码重复)。在Python中,我会为此编写一个上下文管理器,即使用with ..语句来为我打开和关闭数据库连接。在使用Go时,有什么最好的方法来避免这种情况下的代码重复呢?

英文:

Just getting started with Go and I'm wondering about the following situation:

I have a pretty simple codebase where I simply want to open/close a database connection and execute a simple query. I can do this as follows (just showing the important bits here):

import (
	"database/sql"
	_ "github.com/lib/pq"
)

func (db *Database) ExecQueryA() {
    dbConn, err := sql.Open("postgres", db.psqlconn)
    if err != nil {
	    panic(err)
    }
    defer dbConn.Close()

    _, err = db.Exec(...
    if err != nil {
	    panic(err)
    }
}

The above idea works fine, but what if I want to write x more of these functions, I do not want to duplicate this part:

dbConn, err := sql.Open("postgres", db.psqlconn)
if err != nil {
    panic(err)
}
defer dbConn.Close()

At the start of each function (i.e. I want to avoid code duplication). In python I would write a context manager for this, I.e. I would use a with .. statement which would open and close the database connection for me. When using Go, what is the best way to avoid code duplication in this use case?

答案1

得分: 4

正如Brits在评论中指出的那样,*sql.DB不需要在每次使用时打开和关闭。相反,一个共享的*sql.DB实例,在应用程序启动时只需要Open一次,这是一种常见且推荐的做法。

> ...应该只调用一次Open函数。很少需要关闭DB。

请注意,*sql.DB不是一个连接,而是一个管理多个连接的池,根据需要(和可能性)打开尽可能多的连接,如果需要,保留空闲连接,如果不需要,释放连接等。最重要的是,它可以安全地供多个goroutine并发使用

> DB是表示零个或多个底层连接池的数据库句柄。它可以被多个goroutine并发使用。


回答你实际的问题,减少获取和释放资源重复的一种模式是将函数字面量传递给包装函数:

func (db *Database) run(f func(c *sql.DB)) {
    c, err := sql.Open("postgres", db.psqlconn)
    if err != nil {
        panic(err)
    }
    defer c.Close()
    
    f(c)
}

func (db *Database) ExecQueryA() {
    db.run(func(c *sql.DB) {
        _, err := c.Exec(...
        if err != nil {
            panic(err)
        }
    })
}
英文:

As Brits points out in the comment to your question, the *sql.DB does not need to be open and closed every time you intend to use it. Instead a single shared instance of *sql.DB, Opened once at the launch of your app, is a common and recommended practice.

> ... the Open function should be called just once. It is rarely necessary
> to close a DB.

Note that *sql.DB is not a connection, instead, it is a pool that manages multiple connections, opens as many as necessary (and possible), keeps idle ones around if necessary, frees them if unnecessary, etc. And most of all, it is safe for concurrent use.

> DB is a database handle representing a pool of zero or more underlying
> connections. It's safe for concurrent use by multiple goroutines.


To answer your actual question, one pattern to reduce the repetition of obtaining-and-releasing resources is to pass a function literal to a wrapper function:

func (db *Database) run(f func(c *sql.DB)) {
    c, err := sql.Open("postgres", db.psqlconn)
    if err != nil {
        panic(err)
    }
    defer c.Close()
    
    f(c)
}

func (db *Database) ExecQueryA() {
    db.run(func(c *sql.DB) {
        _, err := c.Exec(...
        if err != nil {
            panic(err)
        }
    })
}

huangapple
  • 本文由 发表于 2021年7月26日 06:52:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/68523059.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定