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也是没事的。

posted on 2020-05-16 22:32  人生苦短use,what?  阅读(1503)  评论(0编辑  收藏  举报