Go中提供了database包,database包下有sql.driver。该包用来定义操作数据库的接口,这保证了无论使用哪种数据库,操作都是相同的。但Go并没有提供连接数据库的driver,如果需要操作数据库,需要使用第三方的driver包。
因此以mysql为例:
go get github.com/Go-SQL-Driver/MySQL
安装成功之后导入方式如下:
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"os"
)
Go虽然提供一些方法,但是并不会提供一些特有的方法,但是众所周知会有些特别的方法需要交给数据库的驱动去实现。
选择使用匿名导入包,会让被导入的包编译到可执行文件中,通常来讲,导入包就可以使用包中的数据和方法。但是对于数据操作来讲,我们不应该直接使用导入的驱动包所提供的方法,而应该使用sql.DB对象所提供的统一的方法。因此在导入MySQL的区动时,使用匿名导入的方式。在导入一个数据库驱动后,该驱动会自行初始化并注册到Golang的database/sql上下文中,这样就能使用database/sql包所提供的方法来访问数据库了。
一, 连接数据库
sql中的Open()函数,原型如下所示。
func Open(driverName, dataSourceName string) (*DB, error)
driverName: 使用的驱动名,就是这个名字其实就是数据驱动注册到database/sql时所需使用的名字。
dataSourceName:数据库连接信息。它包含数据库的用户名、密码、数据库注浆机及其需要连接的数据名等信息
db, err := sql.Open("mysql", "用户名:密码@tcp(ip:端口)/数据库?charset=utf8")
var db *sql.DB // 创建连接对象
func init() {
db, _ := sql.Open("mysql", "disk:disk@tcp(127.0.0.1:3306)/fileserver?charset=utf8")
db.SetMaxOpenConns(1000) // 同时连接数
err := db.Ping() // 测试连接是否成功
if err != nil {
fmt.Println("error to link sql:" + err.Error())
os.Exit(1)
}
}
如上面代码所示:一般将数据连接的方法写在init函数中,保证到爆时候立即被执行
二,数据的增删改查
直接调用DB对象的Exec()方法如下所示:
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
通过db.Exec()插入数据,通过返回的err可知插入失败的原因,通过返回的结果可以进一步查询本次插入数据库所影响的行数(RowsAffected)和最后插入的ID(如果数据库支持查询最后插入ID)。
Exec()方法使用方式如下:
result, err := db.Exec("INSERT INTO userinfo (username, departname, created) VALUES (?,?,?)","Steven", "北京", "2020-09-16")
预编译语句(PreparedStatement)提供了诸多好处。PreparedStatement可以实现自定义参数的查询,通常会比手动拼接字符SQL串高效;还可以防止SQL注入攻击。因此一般情况下用PreparedStatement和Exec()完成对INSERT、UPDATE、DELETE操作。使用DB对象的Prepare()方法获得预编译对象stmt,然后调用Exec()方法,语句如下。
func (db *DB) Prepare(query string) (*Stmt, error)
使用如下:
stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?")
result,err : stmt.Exec("zhouli", "开发部", "2020-05-16")
获取影响数据库的行数,可以根据该数值判断是否操作(插入、删除或修改)成功。
count, err := result.RowAffected()
// 注意SQL的注入攻击
stm, err := mydb.DBConn().Prepare("insert ignore into tab_file(`file_sha1`, `file_name`, `file_size`)" +
"`file_addr`,`status`values(?,?,?,?,1)")
if err != nil {
fmt.Println("Failed to Prepare statement, err:" + err.Error())
return false
}
defer stm.Close()
ret, err := stm.Exec(filehash, filename, filesize,fileaddr)
if err != nil {
fmt.Println(err.Error())
return false
}
// 检测受到更新影响的行数
if rf,err := ret.RowsAffected();nil == err {
if rf <= 0 {
fmt.Printf("插入失败")
}
return true
}
三, 查询数据
数据库查询步骤如下:
1, 调用db.Query()方法执行SQL语句,此方法返回一个Rows作为查询结果。语法如下:
func (db *DB) Query(query string,args ...interface{}) (*Rows, error)
2, 将rows.Next()方法的返回值作为for循环的条件,迭代查询数据,语法如下所示。
func (rs *Rows) Next() bool
3, 再循环中,通过rows.Scan()方法读取每一行数据,语法如下所示:
func (rs *Rows) Scan(dest ...interface{}) error
4, 调用db.Close()关闭查询
通过QueryRow()方法查询单挑数据,语法如下所示:
func (db *DB) QueryRow(query string,args ...interface{}) *Row
整体步骤如下所示:
var username,departname, created string
err := db.QueryRow("SELECT username,departname, created FORM user_info WHERE uid=?",3).Scan(&username, &departname, &created)
查询多行数据:
stmt, err := db.Prepare("SELECT * FORM user_info WHERE uid<?")
rows, err := stmt.Query(10)
user := new(UserTable)
for rows.Next() {
err := row.Scan(&user.Uid, &user.Username, &user.Department, &user.Created)
if err != nil {
panic(err)
continue
}
fmt.Println(*user)
}
rows.Scan()方法参数的顺序很重要,必须和查询结果的column相对应(数量和顺序都要相对应),不然会造成数据读取的错位
四, 注意点
每次db.Query()操作后,都建议调用rows.Close()
因为db.Query()会从数据库连接池中获取一个连接,这个底层连接在结果集rows未关闭前会被标记为繁忙状态。当遍历到最后一条记录,会发生一个内部错误EOF错误,自动调用row.Close()。
但如果出现异常,提前退出循环,rows不会被关闭,连接不会回到连接池中,连接不会关闭,则词里阿杰会一直被占用。因此通常使用defer rows.Close()来确保数据库连接可以被正确的放回到连接池中。
但是我们从源码中可以找到rows.Close()操作是幂等操作,而一个幂等操作最大的特点就是:其任意多次指定所产生的的影响与一次执行的影响相同。所以即便对已关闭的row再执行close也是没事的。