Go使用MySql

7 阅读12分钟

在Go语言中使用MySql或类Sql数据库的惯用方法是通过database/sql包.它为面向行的数据库提供了轻量级的接口.

导入数据库驱动:

如果要使用database/sql包.需要该程序包本身以及要使用的特定数据库驱动程序(官方提供的包只是提供接口,没有指定任何实现.所以需要手动导入.)

deepseek_mermaid_20260505_7a9c4f.png

import(

"database/sql"

_"github.com/go-sql-driver/mysql"

)

访问数据库:

连接数据库:

导入包后.可以使用sql.Open()函数来连接数据库.定义如下.

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

sql.Open()函数连接成功后会返回一个*sql.DB指针类型.

package main

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

func main() {
    db, err := sql.Open("mysql", "username:password@tcp(127.0.0.1:3306)/test?charset=utf8")
    if err != nil {
       fmt.Println(err)
    }
    defer db.Close()
}

sql.Open()不建立与数据库的任何连接.也不会验证驱动程序的连接参数.只是准备数据库抽象以供后续使用.如果要立即检查数据库是否可用(例如.检查是否可以建立网络连接并登陆).可以使用db.Ping()来执行此操作.并记住检查错误.

err = db.Ping()
if err != nil {
    //处理逻辑错误.
}

设置最大连接数:

database/sql包中的SetMaxOpenConns()方法用于设置最大连接数.定义如下.

func (db *DB) SetMaxOpenConns(n int)

SetMaxOpenConns()方法用于设置与数据库建立连接的最大数目.其参数n为整数类型.如果n大于0且小于最大闲置连接数.则最大连接数为n.如果n<=0.则不会限制最大开启连接数.默认为0(无限制).

设置最大闲置连接数:

database/sql包中的SetMaxIdleConns()方法用于设置最大闲置连接数.定义如下.

func (db *DB) SetMaxIdleConns(n int)

SetMaxIdleConns()方法用于设置连接池中的最大闲置连接数.其参数n为整数类型.如果n大于最大开启连接数.则新的最大闲置连接数会以最大开启连接数为准,如果n<=0.则将不会保留闲置连接.

deepseek_mermaid_20260505_6070d1.png

数据库查询:

Go语言的database/sql包提供了Query()和Exec()函数来进行数据库查询.

从数据库获取数据.

package main

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

var (
    id    int
    phone string
)

func main() {
    db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/test?charset=utf8")
    if err != nil {
       fmt.Println(err)
    }
    defer db.Close()
    rows, err := db.Query("select id,phone from user where id = ?", 3)
    if err != nil {
       log.Fatal(err)
    }
    defer rows.Close()
    for rows.Next() {
       //通过scan方法赋值.
       err := rows.Scan(&id, &phone)
       if err != nil {
          log.Fatal(err)
       }
       log.Println(id, phone)
    }
    err = rows.Err()
    if err != nil {
       log.Fatal(err)
    }
}

deepseek_mermaid_20260505_4cf2e3.png 1).db.Query()函数将查询发送到数据库.

2).defer rows.Close()语句关闭数据库连接.

3).rows.Next()迭代行.

4).rows.Scan()读取每行中的列变量.

5).完成遍历行后.检查遍历中是否产生错误.

Scan()函数会在后天执行数据类型转换.假设从使用字符串列定义的表中选择一些行.例如Varchar(100)或类似的列.如果将指针指向一个字符串.则Go语言会将字节复制到字符串中,可以使用strconv.ParseInt()函数或类似的方式将值转换为数字.

预处理查询:

在MySql中.普通Sql执行过程如下.

1).在客户端准备Sql语句.

2).发送Sql语句到MySql服务器.

3).在MySql服务器中执行该Sql语句.

4).服务器将执行结果返回给客户端.

预处理执行过程:

1).将Sql拆分为结构部分和数据部分.

2).在执行Sql语句的时候.首先将前面相同的命令和结构部分发送给MySql服务器.让MySql服务器事先进行一次预处理(此时并没有真正的执行Sql语句).

3).为了保证Sql语句结构的完整性.在第一次发送Sql语句的时候讲其中可变的数据部分都用一个数据占位符来表示.

4).然后把数据部分发送给MySql服务器.MySql服务器对Sql语句进行占位符替换.

5).MySql服务器执行完整的Sql语句并将结果返回给客户端.

预处理优点:

1).预处理语句大大减少了分析时间.只做了一次查询.(虽然语句多次执行).

2).绑定参数减少了服务器对带宽.只需要发送查询的参数.而不是整个语句.

3).预处理语句针对Sql注入是非常有用的.因为参数值发送后使用不同的协议.保证了数据的合法性.

Go语言预处理很简单.只需要Prepare()方法即可.

deepseek_mermaid_20260505_7d90ae.png

package main

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

var (
    id    int
    phone string
)

func main() {
    db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/test?charset=utf8")
    if err != nil {
       fmt.Println(err)
    }
    defer db.Close()
    stmt, err := db.Prepare("select id,phone from user where id = ?")
    if err != nil {
       log.Fatal(err)
    }
    defer stmt.Close()
    rows, err := stmt.Query(3)
    defer rows.Close()
    for rows.Next() {
       //通过scan方法赋值.
       err := rows.Scan(&id, &phone)
       if err != nil {
          log.Fatal(err)
       }
       log.Println(id, phone)
    }
    err = rows.Err()
    if err != nil {
       log.Fatal(err)
    }
}

单行查询:

Go语言单行查询可以通过QueryRow()方法实现.

package main

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

var name string

func main() {
    db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/test?charset=utf8")
    if err != nil {
       fmt.Println(err)
    }
    defer db.Close()
    err = db.QueryRow("select name from user where id=?", 3).Scan(&name)
    if err != nil {
       log.Fatal(err)
    }
    fmt.Println(name)
}

也可以在预处理中调用QueryRow()函数.

package main

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

var name string

func main() {
    db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/test?charset=utf8")
    if err != nil {
       fmt.Println(err)
    }
    defer db.Close()
    stmt, err := db.Prepare("select name from user where id=?")
    if err != nil {
       log.Fatal(err)
    }
    defer stmt.Close()
    err = stmt.QueryRow(3).Scan(&name)
    if err != nil {
       log.Fatal(err)
    }
    fmt.Println(name)
}

修改更新删除:

在Go语言中.可以使用Exec()方法来修改 更新 删除行.最好用一个准备好的语句来完成Insert Update Delete或者其他不返回行的语句.

package main

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

var name string

func main() {
    db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/test?charset=utf8")
    if err != nil {
       fmt.Println(err)
    }
    defer db.Close()
    stmt, err := db.Prepare("insert into user(id,name) values(?,?)")
    if err != nil {
       log.Fatal(err)
    }
    result, err := stmt.Exec(1, "itboFive")
    if err != nil {
       log.Fatal(err)
    }
    id, err := result.LastInsertId()
    if err != nil {
       log.Fatal(err)
    }
    fmt.Printf("id is %d\n", id)
    affected, err := result.RowsAffected()
    if err != nil {
       log.Fatal(err)
    }
    fmt.Printf("affected is %d\n", affected)
}

执行语句将生成一个sql.Result对象.它可以访问语句元数据.并返回最后插入的Id和受影响的行数.虽然Query方法也能执行Insert Update Delete的语句.但不提倡用Query()语句.Query()函数将返回一个sql.Rows对象.它将保留数据库的连接.直到sql.Rows关闭.在上面的示例中连接将永远不会释放,垃圾收集器最终会关闭底层的net.Conn.但这可能需要很长的时间.因此.使用Query()查询很容易导致连接数太多.甚至资源耗尽.

事务处理:

事务是最小的不可再分的工作单元.通常一个事务对应一个完整的业务.同时这个业务需要执行多次的DML(insert update delete)语句共同联合完成.

在Go语言中.事务本质上是保留与数据存储连接的对象.它允许执行迄今为止所看到的所有操作.并保证它们将在同一连接上执行.可以通过调用db.Begin()开始执行一个事务.并返回一个Tx对象.Tx对象提供Commit()方法来提交事务.提供Rollback()方法来回滚事务.在事务中创建的准备语句仅限于该事务.

deepseek_mermaid_20260505_95e592.png

package main

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

var (
    id    int
    phone string
)

func main() {
    db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/test?charset=utf8")
    if err != nil {
       fmt.Println(err)
    }
    tx, err := db.Begin()
    if err != nil {
       log.Fatal(err)
    }
    defer db.Close()
    rows, err := db.Query("select id,phone from user where id=?", 3)
    if err != nil {
       log.Fatal(err)
    }
    defer rows.Close()
    for rows.Next() {
       err := rows.Scan(&id, &phone)
       if err != nil {
          log.Fatal(err)
       }
       log.Println(id, phone)
    }
    err = tx.Commit()
    if err != nil {
       log.Fatal(err)
    }
}

MySql错误处理技巧:

在database/sql包中.绝大多数的数据库操作都会返回一个错误作为最后一个值.

deepseek_mermaid_20260505_4c4e82.png

1.迭代结果的错误:

来自rows.Err()的错误可能是rows.Next()循环中各种错误的结果.除了正常完成循环之外.循环也可能会退出.所以始终需要检查循环是否正常终止.异常终止自动调用rows.Close()语句来关闭.

2.关闭结果的错误:

如果程序过早退出循环.则应该始终显示关闭sql.Rows.如果循环正常退出或通过错误退出.则它会自动关闭.但可能会错误的执行rows.Close()语句.

如果rows.Close()返回错误.则需要记录错误消息或进行panic异常处理.如果不记录错误消息或不进行panic异常处理.则最好忽略该错误.

3.QueryRow()中的错误:

err = db.QueryRow("select id,phone from user where id=?", 6).Scan(&id, &phone)
if err != nil {
    log.Fatal(err)
}

加入没有id为6的用户.则结果不会有行数据.Go语言定义了一个特殊的错误常数.称为sql.ErrNoRows.当结果为空时.它将从QueryRow()返回.空的结果通常不被程序认为是错误的.如果不检查错误是否是这个特殊的常量.则可能会导致意想不到的错误.

err = db.QueryRow("select id,phone from user where id=?", 6).Scan(&id, &phone)
if err == sql.ErrNoRows {
    //结果没有行.但没有发生错误.
}else {
    log.Fatal(err)
}

4.识别特定的数据库错误:

rows, err := db.Query("select id,phone from user where id=?", 3)\\
if strings.Contains(err.Error(),"Access denied"){
    //处理错误.
}

上面编码方式不是一个好的办法.例如.字符串值可能会根据服务器用于发送错误消息的语言而有所不同.更好的方式是比较错误码以确定特定的错误是什么.

这样做的机制因驱动的开发者而异.这不是database/sql包本身的一部分.

rows, err := db.Query("select id,phone from user where id=?", 3)
if driver, ok := err.(*mysql.MySQLError); ok {
    if driver.Number == 1045 {
       //处理错误
    }
}

MySqlError类型上是由上述特定驱动程序提供.并且驱动程序之间的.Number字段可能不同.错误码Number的值取自MySql的错误消息.因此是数据库特定的.而不是驱动程序设置的.

使用NULL:

当返回的列有可为空的布尔值 字符串 整数和浮点数的类型时.可以用sql.NullString来判断.

for rows.Next() {
    var s sql.NullString
    err := rows.Scan(&s)
    //检查错误
    if s.Valid {
       //用s.string获取值.
    }else {
       //null值.
    }
}

在Go语言中.每个变量在初始化的时候都有默认空值.如果需要自定义类型来处理NULL.则可以通过sql.NullString来实现.

如果不能避免在数据库中具有NULL值.还可以使用COALESCE()函数来处理.用COALESCE()函数来处理NULL值可以避免引入很多的sql.Null空指针类型.

rows, err := db.Query("select id,coalesce(phone,'') from user where id=?", 3)

使用未知列:

Scan()函数要求准确传递正确数目的目标变量.如果不知道查询将返回多少列.则可以使用Columns()来获取列的数量.通过检查此列表长度以查看多少列.并且可以将切片传递给具有正确数值的Scan()方法.

cols, err := rows.Columns()
if err != nil {
    //处理错误erroe.
} else {
    //标准的表行数.
    dest := []interface{}{
       //id
       new(uint64),
       //name
       new(uint64),
       //phone
       new(uint64),
       //delete_at
       new(uint64),
    }
    if len(cols) == 1 {
       //处理逻辑.
    } else if len(cols) == 4 {
       //处理逻辑.
    }
    err := rows.Scan(dest...)
}

如果不知道这些列或者它们的类型.可以使用sql.RawBytes.

连接池:

连接池简介:

默认情况下.连接池数量没有限制.如果尝试同时执行很多操作.则可以创建任意数量的连接.这可能会导致数据库返回错误.例如连接太多的错误.

连接池数量:

在Go语言中.可以使用db.SetMaxOpenConns()方法来限制与数据库的总打开连接数,连接回收相当快.使用db.SetMaxIdleConns()方法设置大量空闲连接可以减少此流失.并有助于保持连接重新使用.

连接池状态:

由于MySql协议是同步的.当客户端有大量的并发请求.且连接数量小于并发数的情况时.会有一部分请求被阻塞.需要等待其他请求释放连接.在某些场景下或使用不当的情况下.这里可能会成为瓶颈.库中没有详细记录每一笔请求的等待时间.只提供了累积的等待时间.以及其他监控指标.在定位问题时作为参考.

提供了db.Stats()方法.会从db对象中获取所有的监控指标.并生成DBStats对象.

deepseek_mermaid_20260505_0acc67.png

go func(db *sql.DB) {
    mt := time.NewTicker(10 * time.Second)
    for {
       select {
       case <-mt.C:
          stats := db.Stats()
          fmt.Println(stats.MaxOpenConnections)
          fmt.Errorf("monitor db conn(% p): maxopen(%d),open(%d),use(%d),idle(%d),+"+
             "wait(%d),idleClose(%d),lifeClose(%d),totalWait(%v)",
             db,
             stats.MaxOpenConnections, stats.OpenConnections,
             stats.InUse, stats.Idle,
             stats.WaitCount, stats.MaxIdleClosed,
             stats.MaxLifetimeClosed, stats.WaitDuration)

       }
    }
}(db)

使用注意事项:

资源枯竭:

如果不按预期使用database/sql包.可能会导致系统消耗一些资源或阻止它们有效的重用.

1.开放和关闭数据库可能会导致资源耗尽.

2.没有读取所有行或使用rows.Close()保留来自池的连接.

3).对于不返回行的语句.使用Query()将从池中预留一个连接.

4).没有意识到准备好的语句可能导致大量额外的数据库活动.

大uint64值:


_, err = db.Exec("insert into user(id) values ", math.MaxUint64)

如果设置了高位.则不能将大的无符号整数作为参数传递给语句.

这样会引发错误.如果使用uint64值.开始因为它们可能很小可以无错误的工作.但会随着时间的推移而增加.并开始抛出错误.

数据库特定语法:

database/sql包的API提供了面向数据库的抽象.但具体的数据库和驱动程序可能会在行为或语法上有差异.例如准备好的语句占位符.

多个结果集:

Go语言驱动程序不支持单个查询中的多个结果集.尽管有一个支持大容量的操作(如批量复制)的功能.但返回多个结果集的存储过程将无法正常工作.

调用存储过程:

调用存储过程是特定于驱动程序的.但在MySql驱动程序中.目前无法完成.

多重语句支持:

_, err = db.Exec("insert into user(id) values ;insert into user(id) values ")

database/sql包没有显示的支持多个语句.如果想同时执行多个语句.可以通过exec()方法来实现.

以上代码是允许的.上面可能会执行第一个语句.或两个都执行.事务中的每个语句必须连续执行.并且结果中的资源(比如行)必须被扫描或关闭.以便该连接可供一下个语句使用.当不使用事务时.这与通常的执行方式不同.这种情况下.完全可以循环遍历执行.并在循环内查询数据库(将在新的连接上执行).