Build Web Application with Golang 数据库实战:基于 database/sql 的 MySQL 驱动选型与 CRUD 编程

原创2026-09-30 15:48:13273 阅读
文章标签:文档教程

Build Web Application with Golang 数据库实战:基于 database/sql 的 MySQL 驱动选型与 CRUD 编程

本文是《Build Web Application with Golang》第五章数据库篇的核心实战章节,围绕 Go 语言生态中最流行的 MySQL 访问方式展开:先对比两款纯 Go 实现的 MySQL 驱动,再以 database/sql 标准接口为骨架,给出完整的建表 SQL、增删改查示例与 DSN 连接串格式,并佐以本仓库 ch.5.2 配套源码 的运行步骤。读完本文,你将掌握在 Go Web 后端中安全、稳定地接入 MySQL 的完整套路,包括驱动注册原理、连接串编写、预处理语句防注入以及结果集遍历。

MySQL 在 LAMP 中的地位与 Go 的接入方式

近年来 LAMP 架构在互联网领域非常流行,其中 M 代表 MySQL。MySQL 之所以成为众多网站后端的默认数据库,是因为它开源、易用,且与 Linux、Apache、PHP 的组合生态成熟。在《Build Web Application with Golang》的数据库章节(见 第五章导览)中,作者将 SQL 数据库驱动(MySQL、SQLite、PostgreSQL)与 NoSQL(MongoDB、Redis)并列介绍,而 MySQL 正是第一个被详细讲解的 SQL 数据库。

与 PHP 等语言自带官方数据库扩展不同,Go 语言本身不提供任何官方数据库驱动,而是通过标准库 database/sql 定义了一套驱动接口规范,第三方开发者可以基于该规范实现各自的驱动。这套接口的完整设计(sql.Register、driver.Driver、driver.Conn、driver.Stmt、driver.Tx、driver.Result 等)在 5.1 节 database/sql 接口 中有详细说明。理解这套接口是本节 MySQL 实战的前提:

  • sql.Register(name, driver):第三方驱动在自己的 init() 中调用,将驱动注册进 database/sql 内部的 map[string]driver.Driver;
  • 导入时使用 _ "github.com/go-sql-driver/mysql" 这种空白导入,目的就是只执行驱动的 init() 注册逻辑而不直接引用其包符号;
  • database/sql 在驱动之上维护了一个简单的连接池(DB 结构中的 freeConn []driver.Conn),复用空闲连接、按需新建连接。

因此,下面所有示例都遵循"注册驱动 → 打开连接 → 预处理 → 执行/查询 → 关闭"的统一流程,这套流程对任何实现了 database/sql 接口的驱动都适用。

Go 的 MySQL 驱动选型

Go 生态中支持 MySQL 的驱动不止一个,它们在标准接口支持上有所差异。本书 5.2 节列出两款纯 Go 编写的驱动:

驱动 接口支持 实现语言 特点
go-sql-driver/mysql 完整支持 database/sql 纯 Go 较新的驱动,功能更丰富,支持 keep-alive 长连接且线程安全
ziutek/mymysql 支持 database/sql 及自定义接口 纯 Go 另有一套用户自定义的接口标准

作者在书中明确表示,个人项目使用并推荐 go-sql-driver/mysql,理由有三点:

  1. 它是一款较新的数据库驱动,支持更多特性;
  2. 它完全支持 database/sql 接口标准;
  3. 支持 keep-alive 与线程安全的长连接。

这意味着在后续开发 ORM 或业务代码时,只要坚持使用 database/sql 标准接口,未来即使更换数据库驱动,业务代码也无需改动——这正是 5.1 节 反复强调的接口解耦价值。

建表与驱动依赖准备

统一的数据表结构

本书 5.2~5.4 节(MySQL、SQLite、PostgreSQL)共用同一张业务表,便于读者横向对比三种数据库的驱动差异。建表 SQL 如下:

CREATE TABLE `userinfo` (
    `uid` INT(10) NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(64) NULL DEFAULT NULL,
    `department` VARCHAR(64) NULL DEFAULT NULL,
    `created` DATE NULL DEFAULT NULL,
    PRIMARY KEY (`uid`)
);

仓库中对应的可执行脚本位于 en/code/src/apps/ch.5.2/schema.sql。注意其中的字段名是 department,而配套 main.go 中 INSERT 语句使用的列名是 departname,二者略有出入,运行前请以实际表结构为准调整 SQL 列名。

拉取驱动依赖

示例代码通过空白导入引用驱动包:

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

根据 ch.5.2 readme 的说明,需要先执行 go get 下载并安装远程驱动包,再运行程序:

go get github.com/go-sql-driver/mysql
go run main.go

完整 CRUD 示例代码

以下是基于 database/sql 接口标准对 MySQL 进行增、删、改、查的完整示例(与书中代码同源,仓库版本见 en/code/src/apps/ch.5.2/main.go):

package main

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

func main() {
    db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8")
    checkErr(err)

    // insert
    stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?")
    checkErr(err)

    res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")
    checkErr(err)

    id, err := res.LastInsertId()
    checkErr(err)

    fmt.Println(id)
    // update
    stmt, err = db.Prepare("update userinfo set username=? where uid=?")
    checkErr(err)

    res, err = stmt.Exec("astaxieupdate", id)
    checkErr(err)

    affect, err := res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    // query
    rows, err := db.Query("SELECT * FROM userinfo")
    checkErr(err)

    for rows.Next() {
        var uid int
        var username string
        var department string
        var created string
        err = rows.Scan(&uid, &username, &department, &created)
        checkErr(err)
        fmt.Println(uid)
        fmt.Println(username)
        fmt.Println(department)
        fmt.Println(created)
    }

    rows.Close()

    // delete
    stmt, err = db.Prepare("delete from userinfo where uid=?")
    checkErr(err)

    res, err = stmt.Exec(id)
    checkErr(err)

    affect, err = res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    db.Close()
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

仓库中的可运行版本进一步做了两点工程化改进,更接近真实项目写法:

  • 用常量集中管理连接信息,DSN 通过 fmt.Sprintf 动态拼接:
const (
    DB_USER     = "user"
    DB_PASSWORD = ""
    DB_NAME     = "test"
)

dbSouce := fmt.Sprintf("%v:%v@/%v?charset=utf8", DB_USER, DB_PASSWORD, DB_NAME)
db, err := sql.Open("mysql", dbSouce)
  • 用 defer db.Close() 确保连接最终释放,而不是在函数末尾手动关闭;查询结果的打印也使用了 fmt.Printf 按列对齐输出。

逐段拆解:连接、预处理、执行与遍历

本节代码中每个关键函数的职责如下:

  • sql.Open():打开一个已注册的数据库驱动。第一个参数 "mysql" 正是 go-sql-driver/mysql 在 init() 中通过 sql.Register("mysql", ...) 注册的名字;第二个参数是 DSN(Data Source Name,数据源名称),定义数据库连接所需信息,支持以下格式:
user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname

第一行表示通过 Unix 套接字连接;第二行是通过 TCP 连接并显式指定端口;第三行是省略 host/port 的本地默认连接;第四行展示了 IPv6 地址的写法(地址需用方括号包裹)。?charset=utf8 是常见参数,用于指定连接字符集。

  • db.Prepare():返回一条待执行的 SQL 语句对象(Stmt),同时返回 SQL 执行状态。预处理语句把 SQL 结构与参数分离,SQL 骨架只解析一次,参数在 Exec 时再绑定。

  • db.Query():执行 SQL 查询并返回 Rows 结果集。

  • stmt.Exec():执行已经通过 Prepare 准备好的语句,用于 insert/update/delete 等写操作;返回的 Result 可通过 LastInsertId() 拿到自增主键,通过 RowsAffected() 拿到受影响行数。

  • rows.Next() + rows.Scan():结果集遍历的标准姿势——Next() 判断是否还有下一行,Scan() 按目标变量的类型顺序把当前行各列值填充进对应指针。遍历结束后应调用 rows.Close() 释放迭代器资源。

从 5.1 节 的接口定义可以进一步理解底层调用链:db.Prepare 内部通过 driver.Conn.Prepare 得到 driver.Stmt,stmt.Exec 对应 driver.Stmt.Exec(args []Value),rows.Next 对应 driver.Rows.Next(dest []Value);而 database/sql 层在其之上自动完成了连接池的借还与复用。

关于 SQL 注入防护

示例中所有传参均使用 =? 占位符格式,例如:

stmt.Exec("astaxie", "研发部门", "2012-12-09")
stmt.Exec("astaxieupdate", id)

这是防止 SQL 注入攻击的必要措施:参数以独立数据形式交给驱动绑定,而不是直接拼接进 SQL 字符串,从而避免恶意输入改变语句结构。在 Web 表单提交、URL 参数等不可信输入进入 SQL 之前,务必统一走预处理占位符,不要使用字符串拼接构造 SQL。

运行与验证步骤

参照 ch.5.2 readme,完整运行本示例的步骤为:

  1. 安装并启动 MySQL,确保本地服务可连接;
  2. 创建用户与数据库,与 main.go 顶部的常量保持一致:
DB_USER     = "user"
DB_PASSWORD = ""
DB_NAME     = "test"
  1. 创建 userinfo 表,执行 schema.sql 中的建表语句;
  2. go get 下载安装远程驱动包 github.com/go-sql-driver/mysql;
  3. go run main.go 执行程序,观察控制台依次输出插入行的自增 ID、更新与删除的受影响行数,以及查询结果的逐行打印。

仓库源码 en/code/src/apps/ch.5.2/main.go 的输出带有更清晰的阶段标记(Inserting / Updating / Querying / Deleting),便于对照每一步的执行顺序。

小结与延伸阅读

本节围绕 database/sql 标准接口,完成了从驱动选型、DSN 编写、建表到增删改查的 MySQL 接入闭环,并强调了 =? 占位符对 SQL 注入的防御作用。这套代码模式将在后续章节继续复用:5.3 节改用 SQLite 驱动、5.4 节改用 PostgreSQL 驱动时,业务代码结构几乎不变,这正是 Go 数据库接口设计带来的迁移成本优势。

延伸阅读:

登录后查看全文
build-web-application-with-golang