- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
导读
本文围绕 《Build Web Application with Golang》 第 5.3 节展开,讲解如何借助 SQLite 嵌入式关系数据库与 Go 的database/sql标准接口,完成建表、连接以及增删改查(CRUD)全流程。你将掌握:为什么 SQLite 适合作为 Go Web 应用的嵌入式存储、如何选择支持database/sql的第三方驱动、如何用参数化占位符安全地执行 SQL,以及如何将本节的代码无缝迁移到 MySQL、PostgreSQL 等其他数据库。全文以原文档为骨架,并结合仓库内 ch.5.3 示例代码与建表脚本 schema.sql 深入展开。
SQLite:零配置的嵌入式关系数据库
SQLite 是一个开源的嵌入式关系数据库引擎,其设计目标决定了它与传统客户端/服务器数据库(如 MySQL、PostgreSQL)截然不同:它实现自包容(self-contained)、零配置(zero-configuration)、支持事务(transactional),并且高度便携、结构紧凑、高效可靠。
所谓"嵌入式",指的是 SQLite 不是一个独立运行的数据库服务进程,而是以库的形式嵌入到应用程序内部。使用它的成本极低——在大多数情况下,只要确保 SQLite 的二进制文件存在,即可开始创建、连接和使用数据库,不需要安装服务、配置端口、管理账户权限。仓库中 ch.5.3 的 readme 给出的第一步就是"Download and install sqlite 3",可见它的安装门槛就是如此简单。
正是因为这种"文件即数据库"的特性,SQLite 常被称为"开源的 Access":它适合单机应用、原型验证、嵌入式设备以及 Web 应用的轻量级数据存储场景。如果你正在寻找一个嵌入式数据库项目或解决方案,SQLite 是绝对值得考虑的选项。
驱动选型:只有支持 database/sql 接口的驱动值得优先考虑
三种常见驱动对比
Go 官方并未提供数据库驱动,而是通过database/sql标准接口来统一操作方式(详见 5.1 节 database/sql 接口)。Go 支持 SQLite 的驱动比较多,但很多并不支持database/sql接口,常见的三个驱动对比如下:
| 驱动 | 是否支持 database/sql | 实现方式 | 评价 |
|---|---|---|---|
| github.com/mattn/go-sqlite3 | 支持 | 基于 cgo | 目前唯一支持database/sql的 SQLite 驱动,也是本书采用的驱动 |
| github.com/feyeleanor/gosqlite3 | 不支持 | 基于 cgo | 自定义接口,无法直接与标准库配合 |
| github.com/phf/go-sqlite3 | 不支持 | 基于 cgo | 自定义接口,无法直接与标准库配合 |
说明:以上三个驱动的实现均基于 cgo(Go 调用 C 代码的机制)。关于 cgo 的知识可参考 Go 官方文档或本书后续章节。
目前支持database/sql接口的 SQLite 驱动只有第一个(mattn/go-sqlite3)。采用标准接口开发项目有两个显著好处:
- 代码与底层驱动解耦:业务代码只依赖
database/sql的 API,不依赖任何驱动特有的接口; - 便于未来迁移:以后如果出现更好的驱动,只需更换 import 的驱动包(以及连接串),业务逻辑一行都不用改。
这一点在第 5.2 节使用 MySQL 的例子上得到了印证:对比 5.2 节 MySQL 示例 与本节的 SQLite 示例,会发现两者代码几乎一模一样,唯一区别就是导入的驱动包不同,以及sql.Open打开数据库的方式不同。这正是database/sql标准接口设计价值的直观体现。
驱动是如何注册的
database/sql本身不包含任何驱动,驱动通过包级init()函数调用sql.Register(name string, driver driver.Driver)完成注册,注册信息保存在database/sql内部的一个 map 中(drivers[name] = driver)。在 5.1 节 中可以看到 mattn/go-sqlite3 驱动的注册方式:
// https://github.com/mattn/go-sqlite3 驱动 func init() { sql.Register("sqlite3", &SQLiteDriver{}) }因此在使用时,通过空导入_ "github.com/mattn/go-sqlite3"引入驱动包,触发其init()函数自动完成注册,之后即可用sql.Open("sqlite3", ...)打开数据库。_前缀表示只执行包的初始化(副作用导入),而不直接引用包中定义的其他资源,这是 Go 语言中注册类驱动包的惯用写法。
建表:两张表的 SQLite 表结构
在编写 Go 代码之前,需要先准备好数据库与表。本书从 5.2 节 开始统一采用同一个表结构:数据库foo、用户表userinfo及关联的用户信息表userdetail。SQLite 版本对应的建表 SQL 如下(同时收录于仓库的 schema.sql):
CREATE TABLE `userinfo` ( `uid` INTEGER PRIMARY KEY AUTOINCREMENT, `username` VARCHAR(64) NULL, `department` VARCHAR(64) NULL, `created` DATE NULL ); CREATE TABLE `userdetail` ( `uid` INT(10) NULL, `intro` TEXT NULL, `profile` TEXT NULL, PRIMARY KEY (`uid`) );与 MySQL 版本(见 5.2 节)相比,可以注意到一个关键差异:SQLite 中的自增主键写法为INTEGER PRIMARY KEY AUTOINCREMENT。在 SQLite 中,只有整型主键列才具备"插入时自动生成"的行为,而userdetail表中的PRIMARY KEY (uid)属于表级约束,并不会自动生成值——它主要用于与userinfo.uid建立关联。
用命令行创建数据库并导入表结构
仓库中 ch.5.3 的 readme 给出了完整的环境准备步骤,与本节示例完全配套:
- 下载并安装 sqlite3;
- 创建数据库文件:运行
sqlite3 foo.db,生成名为foo的数据库文件(注意:SQLite 的数据库就是一个普通文件,foo.db即出现在当前目录); - 导入表结构:在 sqlite3 交互提示符中执行:
sqlite> .read schema.sql- 验证表是否创建成功:
sqlite> .tables userinfo- 退出 sqlite:
sqlite> .exit- 获取驱动依赖:运行
go get下载并安装远端包(即github.com/mattn/go-sqlite3); - 运行程序:
go run main.go。
示例代码:基于 database/sql 的完整增删改查
下面看 Go 程序如何通过database/sql标准接口操作数据库表数据,完成增删改查全流程。代码与仓库中的 ch.5.3 示例一致:
package main import ( "database/sql" "fmt" "time" _ "github.com/mattn/go-sqlite3" ) func main() { db, err := sql.Open("sqlite3", "./foo.db") checkErr(err) //插入数据 stmt, err := db.Prepare("INSERT INTO userinfo(username, department, created) values(?,?,?)") checkErr(err) res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") checkErr(err) id, err := res.LastInsertId() checkErr(err) fmt.Println(id) //更新数据 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) //查询数据 rows, err := db.Query("SELECT * FROM userinfo") checkErr(err) for rows.Next() { var uid int var username string var department string var created time.Time err = rows.Scan(&uid, &username, &department, &created) checkErr(err) fmt.Println(uid) fmt.Println(username) fmt.Println(department) fmt.Println(created) } //删除数据 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) } }可以看出,这段代码与 5.2 节 MySQL 示例 中的代码几乎一模一样,唯一改变的就是导入的驱动换了,然后调用sql.Open时采用了 SQLite 的方式(驱动名sqlite3+ 数据库文件路径./foo.db)。这正是database/sql标准接口"一次编写、多库迁移"价值的直接体现。
关键 API 详解:sql.Open、Prepare、Exec、Query、Scan
结合 5.1 节对 database/sql 接口的剖析 与本节示例,梳理一下核心调用链中每个 API 的职责与底层原理:
sql.Open(driverName, dataSourceName):打开一个注册过的数据库驱动。第二个参数是 DSN(Data Source Name)。在 SQLite 场景下,DSN 就是数据库文件的路径(如./foo.db)。注意:sql.Open只是建立连接配置、校验驱动注册,真正的连接在首次执行 SQL 时才会建立。db.Prepare(query):返回一个准备好执行 SQL 的准备语句(Stmt)。底层对应driver.Stmt,驱动的Prepare会先在数据库端解析并编译 SQL,后续多次Exec可复用该准备状态,提升重复执行效率。stmt.Exec(args...):执行准备语句中的 SQL。用于 INSERT、UPDATE、DELETE 等写操作,返回Result。Result接口包含两个方法:LastInsertId()返回插入操作得到的自增 ID,RowsAffected()返回受影响的行数(详见 5.1 节 driver.Result)。db.Query(sql, args...):直接执行查询语句并返回Rows结果集,用于 SELECT 操作。rows.Next()+rows.Scan(&dest...):迭代结果集,每次把当前行的各列依次扫描到对应的变量地址中。Scan的目标类型需与列数据类型匹配——在本例中created列被扫描进time.Time变量。db.Close():关闭数据库连接。仓库中的示例使用了defer db.Close()来确保函数退出时自动关闭。
参数化查询:用?占位符防御 SQL 注入
在本节示例中,所有携带用户数据的 SQL 都使用了?占位符的形式:
stmt, err := db.Prepare("INSERT INTO userinfo(username, department, created) values(?,?,?)") res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09")与字符串拼接 SQL 相比,这种写法有两个关键优势:
- 防止 SQL 注入:占位符的值由驱动在底层进行转义与绑定,而不是直接嵌入 SQL 文本,用户输入无法改变 SQL 语句的结构;
- 类型清晰、复用高效:
Prepare一次、Exec多次,同一语句可以携带不同参数反复执行。
这一点在 5.2 节 中也得到了明确强调:"我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止 SQL 注入。" 这是所有database/sql驱动(包括 MySQL、SQLite、PostgreSQL)通用的最佳实践。
配套管理工具
SQLite 也提供了可视化管理工具:http://sqliteadmin.orbmu2k.de/(SQLite Administrator),可以方便地新建数据库并进行管理操作。对于命令行用户,SQLite 自带的sqlite3交互程序已足够完成建库、建表、查询与数据导出等日常需求(如仓库 readme 中演示的.read schema.sql、.tables、.exit命令)。
迁移到其他数据库:标准接口的威力
由于本节代码完全基于database/sql标准接口编写,将其迁移到 MySQL 只需要做两处变更(对照 5.2 节示例):
- 将空导入改为
_ "github.com/go-sql-driver/mysql"; - 将
sql.Open的第一个参数改为"mysql",DSN 改为 MySQL 的连接串(如astaxie:astaxie@/test?charset=utf8)。
其余代码——包括Prepare、Exec、Query、Scan、RowsAffected的调用——全部保持不变。这也正是本节强调"采用标准接口有利于以后出现更好的驱动的时候做迁移"的实践意义。
小结
本节的核心结论可以归纳为三点:
- SQLite 是零配置的嵌入式数据库:无需独立服务进程,安装二进制即可建库使用,适合轻量级、嵌入式场景;
- 驱动选择要认准
database/sql标准接口:目前唯一支持该接口的 SQLite 驱动是 mattn/go-sqlite3,通过空导入_ "github.com/mattn/go-sqlite3"注册后即可使用; - 标准接口带来零成本迁移:基于
database/sql的增删改查代码在不同数据库间几乎可以原样复用,配合?占位符还能天然防御 SQL 注入。
配套的完整可运行示例、建表脚本与环境准备步骤,可分别参考仓库中的 ch.5.3/main.go、ch.5.3/schema.sql 与 ch.5.3/readme.md。更深入的database/sql接口原理(驱动注册、连接池、各接口定义)请参阅 5.1 节 database/sql 接口,后续还可继续学习 5.4 节使用 PostgreSQL 数据库。
- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
相关推荐
用 Go 连接 MySQL:基于 database/sql 驱动的完整增删改查实战指南
用 Go 连接 MySQL:基于 database/sql 驱动的完整增删改查实战指南 MySQL 凭借开源、易用、稳定等特点,长期是 LAMP 架构中 Web
文档教程TIL 实战:在 Go 中用 database/sql 与 go-sqlite3 连接 SQLite 数据库
TIL 实战:在 Go 中用 database/sql 与 go sqlite3 连接 SQLite 数据库 本篇技术指南以 TIL 仓库中的 connect
文档教程知识库用 Go 操作 MySQL:基于 database/sql 接口的增删改查实战(Build Web Application with Golang 第 5.2 章)
用 Go 操作 MySQL:基于 database/sql 接口的增删改查实战(Build Web Application with Golang 第 5.2
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考