Go database/sql 教程

栏目: 数据库 · 发布时间: 5年前

Go使用 SQL 与类SQL数据库的惯例是通过标准库database/sql。这是一个对关系型数据库的通用抽象,它提供了标准的、轻量的、面向行的接口。不过database/sql的包文档只讲它做了什么,却对如何使用只字未提。快速指南远比堆砌事实有用,本文讲述了database/sql的使用方法及其注意事项。 1. 顶层抽象 在 Go 中访问数据库需要用到sql.DB接口:它可以创建语句(statement)和事务(transaction),执行查询,获取结果。 sql.DB并不是数据库连接,也并未在概念上映射到特定的数据库(Database)或模式(schema)。它只是一个抽象的接口,不同的具体驱动有着不同的实现方式。通常而言,sql.DB会处理一些重要而麻烦的事情,例如操作具体的驱动打开/关闭实际底层数据库的连接,按需管理连接池。 sql.DB这一抽象让用户不必考虑如何管理并发访问底层数据库的问题。当一个连接在执行任务时会被标记为正在使用。用完之后会放回连接池中。不过用户如果用完连接后忘记释放,就会产生大量的连接,极可能导致资源耗尽(建立太多连接,打开太多文件,缺少可用网络端口)。 2. 导入驱动 使用数据库时,除了database/sql包本身,还需要引入想使用的特定数据库驱动。 尽管有时候一些数据库特有的功能必需通过驱动的Ad Hoc接口来实现,但通常只要有可能,还是应当尽量只用database/sql中定义的类型。这可以减小用户代码与驱动的耦合,使切换驱动时代码改动最小化,也尽可能地使用户遵循Go的惯用法。本文使用PostgreSQL为例,PostgreSQL的著名的驱动有: github.com/lib/pq github.com/go-pg/pg github.com/jackc/pgx。 这里以pgx为例,它性能表现不俗,并对PostgreSQL诸多特性与类型有着良好的支持。既可使用Ad-Hoc API,也提供了标准数据库接口的实现:github.com/jackc/pgx/stdlib。 import ( "database/sql" _ "github.com/jackx/pgx/stdlib" ) 使用_别名来匿名导入驱动,驱动的导出名字不会出现在当前作用域中。导入时,驱动的初始化函数会调用sql.Register将自己注册在database/sql包的全局变量sql.drivers中,以便以后通过sql.Open访问。 3. 访问数据 加载驱动包后,需要使用sql.Open()来创建sql.DB: func main() { db, err := sql.Open("pgx","postgres://localhost:5432/postgres") if err != nil { log.Fatal(err) } defer db.Close() } sql.Open有两个参数: 第一个参数是驱动名称,字符串类型。为避免混淆,一般与包名相同,这里是pgx。 第二个参数也是字符串,内容依赖于特定驱动的语法。通常是URL的形式,例如postgres://localhost:5432。 绝大多数情况下都应当检查database/sql操作所返回的错误。 一般而言,程序需要在退出时通过sql.DB的Close()方法释放数据库连接资源。如果其生命周期不超过函数的范围,则应当使用defer db.Close() 执行sql.Open()并未实际建立起到数据库的连接,也不会验证驱动参数。第一个实际的连接会惰性求值,延迟到第一次需要时建立。用户应该通过db.Ping()来检查数据库是否实际可用。 if err = db.Ping(); err != nil { // do something about db error } sql.DB对象是为了长连接而设计的,不要频繁Open()和Close()数据库。而应该为每个待访问的数据库创建一个sql.DB实例,并在用完前一直保留它。需要时可将其作为参数传递,或注册为全局对象。 如果没有按照database/sql设计的意图,不把sql.DB当成长期对象来用而频繁开关启停,就可能遭遇各式各样的错误:无法复用和共享连接,耗尽网络资源,由于TCP连接保持在TIME_WAIT状态而间断性的失败等…… 4. 获取结果 有了sql.DB实例之后就可以开始执行查询语句了。 Go将数据库操作分为两类:Query与Exec。两者的区别在于前者会返回结果,而后者不会。 Query表示查询,它会从数据库获取查询结果(一系列行,可能为空)。 Exec表示执行语句,它不会返回行。 此外还有两种常见的数据库操作模式: QueryRow表示只返回一行的查询,作为Query的一个常见特例。 Prepare表示准备一个需要多次使用的语句,供后续执行用。 4.1 获取数据 让我们看一个如何查询数据库并且处理结果的例子:利用数据库计算从1到10的自然数之和。 func example() { var sum, n int32 // invoke query rows, err := db.Query("SELECT generate_series(1,$1)", 10) // handle query error if err != nil { fmt.Println(err) } // defer close result set defer rows.Close() // Iter results for rows.Next() { if err = rows.Scan(&n); err != nil { fmt.Println(err) // Handle scan error } sum += n // Use result } // check iteration error if rows.Err() != nil { fmt.Println(err) } fmt.Println(sum) } 整体工作流程如下: 使用db.Query()来发送查询到数据库,获取结果集Rows,并检查错误。 使用rows.Next()作为循环条件,迭代读取结果集。 使用rows.Scan从结果集中获取一行结果。 使用rows.Err()在退出迭代后检查错误。 使用rows.Close()关闭结果集,释放连接。 一些需要详细说明的地方: db.Query会返回结果集*Rows和错误。每个驱动返回的错误都不一样,用错误字符串来判断错误类型并不是明智的做法,更好的方法是对抽象的错误做Type Assertion,利用驱动提供的更具体的信息来处理错误。当然类型断言也可能产生错误,这也是需要处理的。 if err.(pgx.PgError).Code == "0A000" { // Do something with that type or error } rows.Next()会指明是否还有未读取的数据记录,通常用于迭代结果集。迭代中的错误会导致rows.Next()返回false。 rows.Scan()用于在迭代中获取一行结果。数据库会使用wire protocal通过TCP/UnixSocket传输数据,对Pg而言,每一行实际上对应一条DataRow消息。Scan接受变量地址,解析DataRow消息并填入相应变量中。因为Go语言是强类型的,所以用户需要创建相应类型的变量并在rows.Scan中传入其指针,Scan函数会根据目标变量的类型执行相应转换。 例如某查询返回一个单列string结果集,用户可以传入[]byte或string类型变量的地址,Go会将原始二进制数据或其字符串形式填入其中。但如果用户知道这一列始终存储着数字字面值,那么相比传入string地址后手动使用strconv.ParseInt()解析,更推荐的做法是直接传入一个整型变量的地址(如上面所示),Go会替用户完成解析工作。如果解析出错,Scan会返回相应的错误。 rows.Err()用于在退出迭代后检查错误。正常情况下迭代退出是因为内部产生的EOF错误,使得下一次rows.Next() == false,从而终止循环;在迭代结束后要检查错误,以确保迭代是因为数据读取完毕,而非其他“真正”错误而结束的。遍历结果集的过程实际上是网络IO的过程,可能出现各种错误。健壮的程序应当考虑这些可能,而不能总是假设一切正常。 rows.Close()用于关闭结果集。结果集引用了数据库连接,并会从中读取结果。读取完之后必须关闭它才能避免资源泄露。只要结果集仍然打开着,相应的底层连接就处于忙碌状态,不能被其他查询使用。 因错误(包括EOF)导致的迭代退出会自动调用rows.Close()关闭结果集(和释放底层连接)。但如果程序自行意外地退出了循环,例如中途break & return,结果集就不会被关闭,产生资源泄露。rows.Close方法是幂等的,重复调用不会产生副作用,因此建议使用 defer rows.Close()来关闭结果集。 以上就是在Go中使用数据库的标准方式。 4.2 单行查询 如果一个查询每次最多返回一行,那么可以用快捷的单行查询来替代冗长的标准查询,例如上例可改写为: var sum int err := db.QueryRow("SELECT sum(n) FROM (SELECT generate_series(1,$1) as n) a;", 10).Scan(&sum) if err != nil { fmt.Println(err) } fmt.Println(sum) 不同于Query,如果查询发生错误,错误会延迟到调用Scan()时统一返回,减少了一次错误处理判断。同时QueryRow也避免了手动操作结果集的麻烦。 需要注意的是,对于单行查询,Go将没有结果的情况视为错误。sql包中定义了一个特殊的错误常量ErrNoRows,当结果为空时,QueryRow().Scan()会返回它。 4.3 修改数据 什么时候用Exec,什么时候用Query,这是一个问题。通常DDL和增删改使用Exec,返回结果集的查询使用Query。但这不是绝对的,这完全取决于用户是否希望想要获取返回结果。例如在PostgreSQL中:INSERT ... RETURNING *;虽然是一条插入语句,但它也有返回结果集,故应当使用Query而不是Exec。 Query和Exec返回的结果不同,两者的签名分别是: func (s *Stmt) Query(args ...interface{}) (*Rows, error) func (s *Stmt) Exec(args ...interface{}) (Result, error) Exec不需要返回数据集,返回的结果是Result,Result接口允许获取执行结果的元数据 type Result interface { // 用于返回自增ID,并不是所有的关系型数据库都有这个功能。 LastInsertId() (int64, error) // 返回受影响的行数。 RowsAffected() (int64, error) } Exec的用法如下所示: db.Exec(`CREATE TABLE test_users(id INTEGER PRIMARY KEY ,name TEXT);`) db.Exec(`TRUNCATE test_users;`) stmt, err := db.Prepare(`INSERT INTO test_users(id,name) VALUES ($1,$2) RETURNING id`) if err != nil { fmt.Println(err.Error()) } res, err := stmt.Exec(1, "Alice") if err != nil { fmt.Println(err) } else { fmt.Println(res.RowsAffected()) fmt.Println(res.LastInsertId()) } 相比之下Query则会返回结果集对象*Rows,使用方式见上节。其特例QueryRow使用方式如下: db.Exec(`CREATE TABLE test_users(id INTEGER PRIMARY KEY ,name TEXT);`) db.Exec(`TRUNCATE test_users;`) stmt, err := db.Prepare(`INSERT INTO test_users(id,name) VALUES ($1,$2) RETURNING id`) if err != nil { fmt.Println(err.Error()) } var returnID int err = stmt.QueryRow(4, "Alice").Scan(&returnID) if err != nil { fmt.Println(err) } else { fmt.Println(returnID) } 同样的语句使用Exec和Query执行有巨大的差别。如上文所述,Query会返回结果集Rows,而存在未读取数据的Rows其实会占用底层连接直到rows.Close()为止。因此,使用Query但不读取返回结果,会导致底层连接永远无法释放。database/sql期望用户能够用完就把连接还回来,所以这样的用法很快就会导致资源耗尽(连接过多)。所以,应该用Exec的语句绝不可用Query来执行。 4.4 准备查询 在上一节的两个例子中,没有直接使用数据库的Query和Exec方法,而是首先执行了db.Prepare获取准备好的语句(prepared statement)。准备好的语句Stmt和sql.DB一样,都可以执行Query、Exec等方法。 准备语句的优势 在查询前进行准备是Go语言中的惯用法,多次使用的查询语句应当进行准备(Prepare)。准备查询的结果是一个准备好的语句(prepared statement),语句中可以包含执行时所需参数的占位符(即绑定值)。准备查询比拼字符串的方式好很多,它可以转义参数,避免SQL注入。同时,准备查询对于一些数据库也省去了解析和生成执行计划的开销,有利于性能。 占位符 PostgreSQL使用$N作为占位符,N是一个从1开始递增的整数,代表参数的位置,方便参数的重复使用。MySQL使用?作为占位符,SQLite两种占位符都可以,而Oracle则使用:param1的形式。 MySQL PostgreSQL Oracle ===== ========== ====== WHERE col = ? WHERE col = $1 WHERE col = :col VALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3) 以PostgreSQL为例,在上面的例子中:"SELECT generate_series(1,$1)" 就用到了$N的占位符形式,并在后面提供了与占位符数目匹配的参数个数。 底层内幕 准备语句有着各种优点:安全,高效,方便。但Go中实现它的方式可能和用户所设想的有轻微不同,尤其是关于和database/sql内部其他对象交互的部分。 在数据库层面,准备语句Stmt是与单个数据库连接绑定的。通常的流程是:客户端向服务器发送带有占位符的查询语句用于准备,服务器返回一个语句ID,客户端在实际执行时,只需要传输语句ID和相应的参数即可。因此准备语句无法在连接之间共享,当使用新的数据库连接时,必须重新准备。 database/sql并没有直接暴露出数据库连接。用户是在DB或Tx上执行Prepare,而不是Conn。因此database/sql提供了一些便利处理,例如自动重试。这些机制隐藏在Driver中实现,而不会暴露在用户代码中。其工作原理是:当用户准备一条语句时,它在连接池中的一个连接上进行准备。Stmt对象会引用它实际使用的连接。当执行Stmt时,它会尝试会用引用的连接。如果那个连接忙碌或已经被关闭,它会获取一个新的连接,并在连接上重新准备,然后再执行。 因为当原有连接忙时,Stmt会在其他连接上重新准备。因此当高并发地访问数据库时,大量的连接处于忙碌状态,这会导致Stmt不断获取新的连接并执行准备,最终导致资源泄露,甚至超出服务端允许的语句数目上限。所以通常应尽量采用扇入的方式减小数据库访问并发数。 查询的微妙之处 数据库连接其实是实现了Begin,Close,Prepare方法的接口。 type Conn interface { Prepare(query string) (Stmt, error) Close() error Begin() (Tx, error) } 所以连接接口上实际并没有Exec,Query方法,这些方法其实定义在Prepare返回的Stmt上。对于Go而言,这意味着db.Query()实际上执行了三个操作:首先对查询语句做了准备,然后执行查询语句,最后关闭准备好的语句。这对数据库而言,其实是3个来回。设计粗糙的程序与简陋实现驱动可能会让应用与数据库交互的次数增至3倍。好在绝大多数数据库驱动对于这种情况有优化,如果驱动实现sql.Queryer接口: type Queryer interface { Query(query string, args []Value) (Rows, error) } 那么database/sql就不会再进行Prepare-Execute-Close的查询模式,而是直接使用驱动实现的Query方法向数据库发送查询。对于查询都是即拼即用,也不担心安全问题的情况下,直接Query可以有效减少性能开销。

入群交流(该群和以上内容无关):Go中文网 QQ交流群:731990104 或 加微信入微信群:274768166 备注:入群; 公众号:Go语言中文网


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Tales from Facebook

Tales from Facebook

Daniel Miller / Polity Press / 2011-4-1 / GBP 55.00

Facebook is now used by nearly 500 million people throughout the world, many of whom spend several hours a day on this site. Once the preserve of youth, the largest increase in usage today is amongst ......一起来看看 《Tales from Facebook》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具