关于Golang中database/sql包的学习笔记(转)
来源:SegmentFault
时间:2023-01-13 17:49:09 163浏览 收藏
本篇文章主要是结合我之前面试的各种经历和实战开发中遇到的问题解决经验整理的,希望这篇《关于Golang中database/sql包的学习笔记(转)》对你有很大帮助!欢迎收藏,分享给更多的需要的朋友学习~
因为最近在学习Go,所以找了revel这个框架来学习,感觉和php的面向对象有很大不同。revel没有提供db mapping的组件,所以在github上搜了很多ORM来学习,在jmoiron/sqlx中发现了一篇比较详细介绍database/sql这个包的文章,拿来和大家分享。本文并不是按字句的翻译,如果哪里表述不清楚建议阅读原文 原文地址
概述
sql.DB不是一个连接,它是数据库的抽象接口。它可以根据driver打开关闭数据库连接,管理连接池。正在使用的连接被标记为繁忙,用完后回到连接池等待下次使用。所以,如果你没有把连接释放回连接池,会导致过多连接使系统资源耗尽。
使用DB
导入driver
这里使用的是MySQL drivers
import ( "database/sql" _ "github.com/go-sql-driver/mysql" )
连接DB
func main() { db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello") if err != nil { log.Fatal(err) } defer db.Close() }
sql.Open的第一个参数是driver名称,第二个参数是driver连接数据库的信息,各个driver可能不同。DB不是连接,并且只有当需要使用时才会创建连接,如果想立即验证连接,需要用
Ping()方法,如下:
err = db.Ping() if err != nil { // do something here }
sql.DB的设计就是用来作为长连接使用的。不要频繁Open, Close。比较好的做法是,为每个不同的datastore建一个DB对象,保持这些对象Open。如果需要短连接,那么把DB作为参数传入function,而不要在function中Open, Close。
读取DB
如果方法包含
Query,那么这个方法是用于查询并返回rows的。其他情况应该用
Exec()。
var ( id int name string ) rows, err := db.Query("select id, name from users where id = ?", 1) if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { err := rows.Scan(&id, &name) if err != nil { log.Fatal(err) } log.Println(id, name) } err = rows.Err() if err != nil { log.Fatal(err) }
上面代码的过程为:
db.Query()表示向数据库发送一个query,
defer rows.Close()非常重要,遍历rows使用
rows.Next(), 把遍历到的数据存入变量使用
rows.Scan(), 遍历完成后检查error。有几点需要注意:
- 检查遍历是否有error
- 结果集(rows)未关闭前,底层的连接处于繁忙状态。当遍历读到最后一条记录时,会发生一个内部EOF错误,自动调用
rows.Close()
,但是如果提前退出循环,rows不会关闭,连接不会回到连接池中,连接也不会关闭。所以手动关闭非常重要。rows.Close()
可以多次调用,是无害操作。
单行Query
err在
Scan后才产生,所以可以如下写:
var name string err = db.QueryRow("select name from users where id = ?", 1).Scan(&name) if err != nil { log.Fatal(err) } fmt.Println(name)
修改数据,事务
一般用Prepared Statements和
Exec()完成
INSERT,
UPDATE,
DELETE操作。
stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)") if err != nil { log.Fatal(err) } res, err := stmt.Exec("Dolly") if err != nil { log.Fatal(err) } lastId, err := res.LastInsertId() if err != nil { log.Fatal(err) } rowCnt, err := res.RowsAffected() if err != nil { log.Fatal(err) } log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)
事务
db.Begin()开始事务,
Commit()或
Rollback()关闭事务。
Tx从连接池中取出一个连接,在关闭之前都是使用这个连接。Tx不能和DB层的
BEGIN,
COMMIT混合使用。
如果你需要通过多条语句修改连接状态,你必须使用Tx,例如:
- 创建仅对单个连接可见的临时表
- 设置变量,例如
SET @var := somevalue
- 改变连接选项,例如字符集,超时
Prepared Statements
Prepared Statements and Connection
在数据库层面,Prepared Statements是和单个数据库连接绑定的。客户端发送一个有占位符的statement到服务端,服务器返回一个statement ID,然后客户端发送ID和参数来执行statement。
在GO中,连接不直接暴露,你不能为连接绑定statement,而是只能为DB或Tx绑定。
database/sql包有自动重试等功能。当你生成一个Prepared Statement
- 自动在连接池中绑定到一个空闲连接
-
Stmt
对象记住绑定了哪个连接 - 执行
Stmt
时,尝试使用该连接。如果不可用,例如连接被关闭或繁忙中,会自动re-prepare,绑定到另一个连接。
这就导致在高并发的场景,过度使用statement可能导致statement泄漏,statement持续重复prepare和re-prepare的过程,甚至会达到服务器端statement数量上限。
某些操作使用了PS,例如
db.Query(sql, param1, param2), 并在最后自动关闭statement。
有些场景不适合用statement:
- 数据库不支持。例如Sphinx,MemSQL。他们支持MySQL wire protocol, 但不支持"binary" protocol。
- statement不需要重用很多次,并且有其他方法保证安全。例子
在Transaction中使用PS
PS在Tx中唯一绑定一个连接,不会re-prepare。
Tx和statement不能分离,在DB中创建的statement也不能在Tx中使用,因为他们必定不是使用同一个连接使用Tx必须十分小心,例如下面的代码:
tx, err := db.Begin() if err != nil { log.Fatal(err) } defer tx.Rollback() stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)") if err != nil { log.Fatal(err) } defer stmt.Close() // danger! for i := 0; i
*sql.Tx一旦释放,连接就回到连接池中,这里stmt在关闭时就无法找到连接。所以必须在Tx commit或rollback之前关闭statement。
处理Error
循环Rows的Error
如果循环中发生错误会自动运行
rows.Close(),用
rows.Err()接收这个错误,Close方法可以多次调用。循环之后判断error是非常必要的。
for rows.Next() { // ... } if err = rows.Err(); err != nil { // handle the error here }
关闭Resultsets时的error
如果你在rows遍历结束之前退出循环,必须手动关闭Resultset,并且接收error。
for rows.Next() { // ... break; // whoops, rows is not closed! memory leak... } // do the usual "if err = rows.Err()" [omitted here]... // it's always safe to [re?]close here: if err = rows.Close(); err != nil { // but what should we do if there's an error? log.Println(err) }
QueryRow()的error
var name string err = db.QueryRow("select name from users where id = ?", 1).Scan(&name) if err != nil { log.Fatal(err) } fmt.Println(name)
如果id为1的不存在,err为sql.ErrNoRows,一般应用中不存在的情况都需要单独处理。此外,Query返回的错误都会延迟到Scan被调用,所以应该写成如下代码:
var name string err = db.QueryRow("select name from users where id = ?", 1).Scan(&name) if err != nil { if err == sql.ErrNoRows { // there were no rows, but otherwise no error occurred } else { log.Fatal(err) } } fmt.Println(name)
把空结果当做Error处理是为了强行让程序员处理结果为空的情况
分析数据库Error
各个数据库处理方式不太一样,mysql为例:
if driverErr, ok := err.(*mysql.MySQLError); ok { // Now the error number is accessible directly if driverErr.Number == 1045 { // Handle the permission-denied error } }
MySQLError,
Number都是DB特异的,别的数据库可能是别的类型或字段。这里的数字可以替换为常量,例如这个包 MySQL error numbers maintained by VividCortex
连接错误
NULL值处理
简单说就是设计数据库的时候不要出现null,处理起来非常费力。Null的type很有限,例如没有
sql.NullUint64; null值没有默认零值。
for rows.Next() { var s sql.NullString err := rows.Scan(&s) // check err if s.Valid { // use s.String } else { // NULL value } }
未知Column
rows.Columns()的使用,用于处理不能得知结果字段个数或类型的情况,例如:
cols, err := rows.Columns() if err != nil { // handle the error } else { dest := []interface{}{ // Standard MySQL columns new(uint64), // id new(string), // host new(string), // user new(string), // db new(string), // command new(uint32), // time new(string), // state new(string), // info } if len(cols) == 11 { // Percona Server } else if len(cols) > 8 { // Handle this case } err = rows.Scan(dest...) // Work with the values in dest }
cols, err := rows.Columns() // Remember to check err afterwards vals := make([]interface{}, len(cols)) for i, _ := range cols { vals[i] = new(sql.RawBytes) } for rows.Next() { err = rows.Scan(vals...) // Now you can check each element of vals for nil-ness, // and you can use type introspection and type assertions // to fetch the column into a typed variable. }
关于连接池
- 避免错误操作,例如LOCK TABLE后用 INSERT会死锁,因为两个操作不是同一个连接,insert的连接没有table lock。
- 当需要连接,且连接池中没有可用连接时,新的连接就会被创建。
- 默认没有连接上限,你可以设置一个,但这可能会导致数据库产生错误“too many connections”
-
db.SetMaxIdleConns(N)
设置最大空闲连接数 -
db.SetMaxOpenConns(N)
设置最大打开连接数 - 长时间保持空闲连接可能会导致db timeout
今天关于《关于Golang中database/sql包的学习笔记(转)》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!
-
499 收藏
-
244 收藏
-
235 收藏
-
157 收藏
-
101 收藏
-
140 收藏
-
426 收藏
-
363 收藏
-
370 收藏
-
152 收藏
-
368 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习