本日关键词(实战):database/sql、驱动、连接池、DB.Open、Query/QueryRow、Exec、预编译、参数化查询、SQLite、PostgreSQL、DSN、CRUD
本日语法/概念(实战):
| 语法/概念 | 实战用途 | 本日示例 |
|---|---|---|
sql.Open(driver, dsn) |
建连接池,DSN 从配置读(Day 3) | sqlite、postgres |
db.QueryRow / db.Query |
查一条、查多行,配合 Scan 取结果 |
CRUD 查询 |
db.Exec |
插入/更新/删除,返回 LastInsertId、RowsAffected | 建表、插入、更新、删除 |
占位符 ?(SQLite)或 $1,$2(Postgres) |
参数化查询,防 SQL 注入,生产必用 | 所有带参数的 SQL |
row.Scan(&变量...) |
把查询结果填进变量(传地址 &变量) | 取单条/多列 |
rows.Close() |
必须关闭结果集,否则占连接、可能泄漏 | defer rows.Close() |
获取实战代码 :如需在本地跑通本文示例,请克隆仓库 WenSongWang/go-quickstart-7days,本文示例在 day4 目录,克隆后在项目根目录执行下文中的命令即可。
一、本篇目标
学完本文并跑通本目录示例,你将掌握:
| 模块 | 内容 |
|---|---|
| 标准库 | database/sql + 驱动(本目录 SQLite 用 modernc.org/sqlite 纯 Go,Postgres 用 lib/pq) |
| 连接 | 连接池、Query/Exec、预编译语句 |
| CRUD | 插入(Create)、查一条/列表(Read)、更新(Update)、删除(Delete) |
二、前置要求
- 已完成 Day 1~3。
- 命令在项目根目录执行。
三、示例与知识点(先混个眼熟)
| 示例 | 主要知识点 |
|---|---|
day4/sqlite/ |
内存 SQLite、建表、INSERT、QueryRow/Query、UPDATE、DELETE;纯 Go 驱动,无需 CGo |
day4/postgres/ |
连本地 Postgres、Ping、QueryRow 查当前时间;可选,需已安装 Postgres |
四、核心概念与最小示例(不看代码也能懂)
database/sql 是啥?和驱动啥关系?
database/sql 是 Go 标准库里的通用数据库接口 :代码只和「连接、查询、执行」这些抽象打交道,不关心底层是 SQLite 还是 Postgres 。具体怎么连、怎么发 SQL,由驱动 实现(如 modernc.org/sqlite、github.com/lib/pq)。驱动通过匿名导入 (_ "modernc.org/sqlite")在 init 里把自己注册到 database/sql,之后 sql.Open("sqlite", dsn) 才能用。
Open 和「真正连上」有啥区别?
sql.Open(driver, dsn) 多数实现里不会立刻建一条物理连接 ,只是准备好连接池和配置。真要确认能连上,可以再调一次 db.Ping()。本日 SQLite 用内存库,Open 即可;Day 4 的 postgres 示例里会看到 db.Ping() 的用法。
为什么必须用占位符(参数化查询)?
若把用户输入直接拼进 SQL,例如 "SELECT * FROM users WHERE name = '" + name + "'",恶意输入可以改写 SQL(SQL 注入 )。用占位符:db.QueryRow("SELECT ... WHERE id = ?", id),驱动会把参数安全地传给数据库,生产环境必须这样写。
rows 为什么要 Close?defer 放哪?
db.Query() 返回的 *sql.Rows 会占用连接,不关掉会一直占着连接池里的连接,严重时占满。所以只要开了 rows,就要在不用时 rows.Close() ,通常用 defer rows.Close() 放在拿到 rows 之后、检查 err 之后,避免 err 时 rows 为 nil 仍调 Close。
本目录 SQLite 为啥用 modernc 不用 go-sqlite3?
github.com/mattn/go-sqlite3 依赖 CGo 和 C 编译器,Windows 上没装 gcc 或 CGO_ENABLED=0 时会报错。本仓库用 modernc.org/sqlite (纯 Go),无需 CGo/gcc,克隆后直接 go run ./day4/sqlite 即可。
易踩坑小结
| 坑 | 原因 | 解法 |
|---|---|---|
忘记 rows.Close() |
占连接,连接池耗尽 | 拿到 rows 后立刻 defer rows.Close()(在判 err 之后) |
| 拼接 SQL | SQL 注入 | 一律用 ? / $1,$2 占位符 + 参数 |
| 连接串写死在代码里 | 换环境、换密码要改代码 | 用 Day 3 的配置(如 cfg.DBDSN)或环境变量 |
五、Day 4 示例代码与逐段解读
day4/sqlite/main.go(完整 CRUD)
下面是一份完整可运行的 CRUD 示例(建表 → 插入 → 查一条 → 查多行 → 更新 → 删除 → 再查列表)。
go
package main
import (
"database/sql"
"fmt"
"log"
_ "modernc.org/sqlite"
)
func main() {
db, err := sql.Open("sqlite", "file::memory:?cache=shared")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 建表
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)
`)
if err != nil {
log.Fatal(err)
}
// 插入(Create)
res, err := db.Exec("INSERT INTO users (name) VALUES (?)", "小王")
if err != nil {
log.Fatal(err)
}
id, _ := res.LastInsertId()
fmt.Println("插入 ID:", id)
// 查一条(Read)
var name string
err = db.QueryRow("SELECT name FROM users WHERE id = ?", id).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println("查询到:", name)
// 查多行(Read)
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var rid int64
var n string
rows.Scan(&rid, &n)
fmt.Printf(" id=%d name=%s\n", rid, n)
}
// 更新(Update)
_, err = db.Exec("UPDATE users SET name = ? WHERE id = ?", "小王(已改)", id)
if err != nil {
log.Fatal(err)
}
_ = db.QueryRow("SELECT name FROM users WHERE id = ?", id).Scan(&name)
fmt.Println("更新后 name =", name)
// 删除(Delete)
res2, err := db.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
log.Fatal(err)
}
n, _ := res2.RowsAffected()
fmt.Printf("删除影响行数: %d\n", n)
// 再查列表,应为空
rows2, _ := db.Query("SELECT id, name FROM users")
defer rows2.Close()
for rows2.Next() {
var rid int64
var n string
rows2.Scan(&rid, &n)
fmt.Printf(" id=%d name=%s\n", rid, n)
}
}
逐段解读:
- Open + defer db.Close() :
sql.Open("sqlite", "file::memory:?cache=shared")用 modernc 驱动打开内存库;程序结束前关闭连接池。 - Exec 建表 / 插入 / 更新 / 删除 :不返回结果集的 SQL 都用
db.Exec;插入后用LastInsertId()拿自增 id,删除后用RowsAffected()拿影响行数。 - QueryRow + Scan :只查一行时用
QueryRow,用Scan(&变量...)把列填进变量;必须传指针 (如&name),Scan 才能写回。 - Query + rows.Next() + Scan :查多行用
Query,用rows.Next()逐行,每行用rows.Scan(&rid, &n)填进变量;务必defer rows.Close(),且放在判 err 之后。 - 占位符
?:所有带变量的 SQL 都用?占位符加后面的参数,不要拼接字符串,防止 SQL 注入。
六、运行当天代码
本日只需跑 SQLite 即可学完所有知识点,无需安装 PostgreSQL;postgres 示例为可选(已装 Postgres 时可多练一个驱动),没装可跳过。
方式一:SQLite(推荐)
bash
go run ./day4/sqlite
预期输出类似:
插入 ID: 1
查询到: 小王
id=1 name=小王
更新后: 1
name = 小王(已改)
删除 id=1,影响行数: 1
删除后列表:
本目录 SQLite 使用纯 Go 驱动(modernc.org/sqlite),无需 CGo/gcc,直接运行即可。
方式二:PostgreSQL(可选)
需本地已安装并启动 Postgres,并设置环境变量 DB_DSN,例如:
bash
# 设置连接串后再运行
go run ./day4/postgres
七、学习建议
- 先跑 SQLite:不依赖外部服务,适合本地学习。
- 对照代码:把「建表、插入、QueryRow、Query 循环、更新、删除」和上面逐段解读对上号。
- 安全习惯:以后写 SQL 一律用占位符;连接串用 Day 3 的配置或环境变量,不要写死在代码里。
八、小结
Day 4 掌握「连接 + 查询 + 完整 CRUD(增删改查)」,为 Day 7 的综合 API(可接真实或内存数据层)打基础。若时间紧,至少跑通 day4/sqlite 再进入 Day 5。