[7天实战入门Go语言后端] Day 4:Go 数据层入门——database/sql 与简单 CRUD

本日关键词(实战):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/sqlitegithub.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

七、学习建议

  1. 先跑 SQLite:不依赖外部服务,适合本地学习。
  2. 对照代码:把「建表、插入、QueryRow、Query 循环、更新、删除」和上面逐段解读对上号。
  3. 安全习惯:以后写 SQL 一律用占位符;连接串用 Day 3 的配置或环境变量,不要写死在代码里。

八、小结

Day 4 掌握「连接 + 查询 + 完整 CRUD(增删改查)」,为 Day 7 的综合 API(可接真实或内存数据层)打基础。若时间紧,至少跑通 day4/sqlite 再进入 Day 5。

相关推荐
lzxdyzx2 小时前
数据库操作与数据管理——Rust 与 SQLite 的集成
数据库·rust·sqlite
小跌—2 小时前
Redis数据结构和单线程
数据结构·数据库·redis
南 阳2 小时前
Python从入门到精通day35
数据库·python·oracle
青衫码上行2 小时前
Redis的事务操作
数据库·redis
堕2742 小时前
MySQL数据库《基础篇--库的操作和数据类型》
数据库·mysql
Web极客码2 小时前
解决WordPress迁移后重定向到旧域名的问题
数据库·wordpress·网站迁移
Web打印4 小时前
Phpask(php集成环境)之15 phpstudy转移到phpask
数据库·mysql
LaughingZhu10 小时前
Product Hunt 每日热榜 | 2026-02-14
数据库·人工智能·经验分享·神经网络·搜索引擎·chatgpt
软件派10 小时前
近两年国外主流数据库深度解析:从技术特性到场景适配
数据库