Golang数据库操作笔记

golang 连接数据库需要使用到系统标准库 database/sql 和 数据库驱动,database/sql 支持数据库连接池,是并发安全的。

下面以 mysql 为例,展示下连接,查询,写入,更新,删除,事务等操作。

下载数据库连接驱动#

go get -u github.com/go-sql-driver/mysql

数据库操作#

连接数据库#

连接数据库使用 sql.Open() 函数打开数据库连接(此时只会判断 DSN 是否错误,不会连接数据库),并使用 db.Ping() 进行数据库连接。

func Open(driverName, dataSourceName string) (*DB, error)

dataSourceName 的格式为 username:password@tcp(host:port)/database

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql" // 导入mysql包,并不使用
)

// 连接mysql示例
func main() {
    dsn := "root:123123@tcp(127.0.0.1:3306)/golang"
    db, err := sql.Open("mysql", dsn) // 打开一个数据库连接,不会校验用户名和密码是否正确
    if err != nil {
        fmt.Printf("DSN[%s]格式错误: %v \n", dsn, err) // 如果出现错误,则DSN格式错误
        return
    }

    err = db.Ping() // 尝试连接mysql

    if err != nil {
        fmt.Printf("连接数据库失败: %v \n", err)
        return
    }

    fmt.Println("连接数库成功")
}

查询#

单行查询#

单行查询使用 db.QueryRow() 进行查询

func (db *DB) QueryRow(query string, args ...interface{}) *Row
func (r *Row) Scan(dest ...interface{}) error

QueryRow 函数会返回一个 *Row 指针,通过该指针对象的 Scan() 方法可以获得结果,如果没有查询到结果,会返回一个 sql.ErrNoRows

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "log"
)

var db *sql.DB

// 声明一个user结构体
type user struct {
    id       int
    nickname string
    avatar   *string // 数据库字段可以为空时,使用指针类型
}

func initDB() (err error) {
    dsn := "root:@tcp(127.0.0.1:3306)/golang"
    db, err = sql.Open("mysql", dsn) // 打开一个数据库连接,不会校验用户名和密码是否正确
    if err != nil {
        return err
    }
    err = db.Ping()

    if err != nil {
        return err
    }
    return
}

// 连接mysql示例
func main() {
    err := initDB() // 连接数据库

    if err != nil {
        log.Println("数据库连接失败,", err)
        return
    }
    sqlStr := "select id, nickname, avatar from users where id = ?" // 构建查询语句
    var u user
    err = db.QueryRow(sqlStr, 1).Scan(&u.id, &u.nickname, &u.avatar) // 查询单条数据并扫描结果

    if err != nil {
        if err == sql.ErrNoRows {
            log.Print("未查询到结果!")
      return
        }
        log.Printf("查询失败: %s", err)
    return
    }

    log.Printf("查询结果:%#v", u)
}

注意

  1. Scan 方法会释放当前的数据库连接回到连接池中,如果调用了 QueryRow 不调用 Scan 方法,则会导致数据库连接无法释放;
  2. Scan 的时候,需要保证查询的字段顺序及数量和变量保持一致,否则会返回错误。
  3. 当数据库中字段可以为空时,字段应使用指针类型或 sql.NullString
  4. 如果未查询到结果,Scan 会返回一个 sql.ErrNoRows ,需要通过 err == sql.ErrNoRows 来确定。

多行查询#

多行查询使用 db.Query() 函数进行查询,返回一个 *Rowserror ;

示例:

// 多行查询
func queryMany() {
    sqlStr := "select id, nickname, avatar from users"

    rows, err := db.Query(sqlStr) // 查询sql语句
    if err != nil {
        fmt.Println("查询失败", err)
        return
    }
    users := make([]user, 0)

    defer rows.Close() // rows需要手动调用Close()释放连接
    for rows.Next() {  // 遍历调用Scan方法
        var u user
        err = rows.Scan(&u.id, &u.nickname, &u.avatar) // 对对象进行赋值
        if err != nil {
            fmt.Println("获取数据失败", err)
            continue
        }
        users = append(users, u)
    }
    fmt.Printf("%#v", users)
}

注意

  1. Rows 需要手动调用 Close() 方法释放数据库连接;
  2. 获取 Rows 中的数据,需要使用循环来进行操作。

插入、修改和删除#

插入、修改和删除统一通过 Exec 函数进行操作。

func (db *DB) Exec(query string, args ...interface{}) (Result, error)

Exec 方法返回一个 Result 对象,该对象有两个方法:

LastInsertId() 用于获取插入的 id,

RowsAffected() 用户获取受影响的行数。

插入数据#

func insert() {
    sqlStr := "insert into users(name, age) values('王五', 10)" // 插入sql
    ret, err := db.Exec(sqlStr) // 执行插入语句

    if err != nil {
        fmt.Println("插入数据失败,", err)
        return
    }

    lastId, err := ret.LastInsertId() // 获取插入ID

    if err != nil {
        fmt.Println("插入数据失败,", err)
        return
    }

    fmt.Println("插入数据成功:", lastId)
}

更新数据#

func update() {
    sqlStr := "update users set age = 12 where name = '王五'"

    ret, err := db.Exec(sqlStr)
    if err != nil {
        fmt.Println("更新数据失败", err)
        return
    }

    rows, err := ret.RowsAffected()

    if err != nil {
        fmt.Println("获取行数失败", err)
        return
    }

    fmt.Println("更新数据行数:", rows)
}

删除数据#

func deleteRow() {
    sqlStr := "delete from users where age = 12"

    ret, err := db.Exec(sqlStr)

    if err != nil {
        fmt.Println("删除数据失败", err)
        return
    }

    rows, err := ret.RowsAffected()
    if err != nil {
        fmt.Println("获取行数失败", err)
        return
    }

    fmt.Println("删除行数:", rows)
}

Mysql 预处理#

什么是预处理#

普通 Sql 语句执行过程:

  1. 客户端对 sql 语句进行占位符替换得到完整的 sql 语句
  2. 客户端发送完整的 sql 语句到 mysql 服务端
  3. mysql 服务端执行完整的 sql 语句,并将结果返回给客户端

预处理 sql 语句执行过程

  1. 将 sql 语句分成命令部分和数据部分
  2. 先把命令发送到 mysql 服务端,mysql 服务端对 sql 语句进行预处理
  3. 然后将数据部分发送给 mysql 服务端,mysql 服务端对 sql 语句进行占位符替换
  4. mysql 服务端执行完整的 sql 语句,并将结果返回给客户端

为什么要预处理?#

  1. 优化 mysql 服务器重复执行 sql 的方法,提升服务器性能。对于相同的 sql,可以一次编译多次执行,节省编译的性能和时间开销
  2. 避免 sql 注入

使用预处理#

使用预处理需要使用到 db.Prepare() 方法

func (db *DB) Prepare(query string) (*Stmt, error)

该方法返回一个 Stmt 的指针对象,通过调用 StmtQuery(), QueryRow(), Exec() 等方法来执行 sql 语句,该对象需要调用 Close() 进行关闭

  1. 调用 db.Prepare() 方法进行 sql 预处理,得到 Stmt 对象
  2. 通过调用 StmtQuery(), QueryRow(), Exec() 等方法来执行 sql 语句,
  3. 关闭 Stmt 对象
func prepareQueryDemo(id int) {
    sqlStr := "select id, name, age from users where id = ?"
    stmt, err := db.Prepare(sqlStr)

    if err != nil {
        fmt.Println("预处理失败", err)
        return
    }

    defer stmt.Close()
    row := stmt.QueryRow(id)

    var u user
    err = row.Scan(&u.id, &u.name, &u.age)

    if err != nil {
        fmt.Println("获取数据失败", err)
        return
    }

    fmt.Printf("获取数据成功%#v\n", u)

}

事务#

事务使用 db.BeginTx 进行处理,开启事务后,事务内的 sql 需要使用返回的 *Tx 进行处理。不能使用 db 进行操作,否则事务不生效。还可以通过 sql.TxOption 配置隔离等级。

func transaction() {
    ctx := context.Background()
    tx, err := db.BeginTx(ctx, &sql.TxOptions{}) //开启事务
    if err != nil {
        log.Fatal("begin tx error:", err)
    }
  // do something
    sqlStr1 := "insert into users(nickname, avatar) values('王五', null)" // 插入sql
    _, err = tx.Exec(sqlStr1)                                           // 执行插入语句
    sqlStr2 := "insert into users(nickname, avatar) values('李四', null)" // 插入sql
    _, err = tx.Exec(sqlStr2)                                           // 执行插入语句
    if err != nil {
        tx.Rollback() // 事务回滚
        log.Println("Rollback", err)
        return
    }

    tx.Commit() // 事务提交
    log.Println("committed")
}

我的博客 《Golang 数据库操作》

本作品采用《CC 协议》,转载必须注明作者和本文链接
打酱油