Golang CRUD 操作与预处理语句
CRUD 操作与预处理语句
核心概念
CRUD(Create/Read/Update/Delete)是数据库操作的基本功。在database/sql体系下,除了直接拼 SQL 字符串,还有**预处理语句(Prepared Statement)**这一利器。预处理将 SQL 模板和参数分离,带来三个好处:
- 安全:参数自动转义,从根本上杜绝 SQL 注入
- 性能:同一条 SQL 只解析一次,多次执行复用执行计划
- 类型安全:驱动负责 Go 类型到数据库类型的转换
预处理 vs 直接执行
// 直接执行:每次都要解析 SQLdb.Exec("INSERT INTO users(name, age) VALUES('张三', 25)")// 预处理:SQL 只解析一次,后续执行只传参数stmt,_:=db.Prepare("INSERT INTO users(name, age) VALUES(?, ?)")deferstmt.Close()stmt.Exec("张三",25)stmt.Exec("李四",32)// 复用同一个执行计划NULL 值处理
数据库中的 NULL 是一个独特的值,它不等于空字符串也不等于 0。Go 的基础类型(string, int, float64)无法表达 NULL,直接Scan会报错。
方案一:sql.NullXxx 类型
varname sql.NullStringvarage sql.NullInt64 err:=db.QueryRow("SELECT name, age FROM users WHERE id = ?",1).Scan(&name,&age)ifname.Valid{fmt.Println(name.String)// 非 NULL 时可以取值}else{fmt.Println("name is NULL")}sql.NullString是一个结构体:type NullString struct { String string; Valid bool }。Valid为 false 表示数据库值为 NULL。
方案二:使用指针类型
varname*stringvarage*intdb.QueryRow("SELECT name, age FROM users WHERE id = ?",1).Scan(&name,&age)ifname!=nil{fmt.Println(*name)// 解引用}else{fmt.Println("NULL")}指针方案更简洁,但每次都要解引用,且 nil 指针解引用会 panic。
方案三:COALESCE 在 SQL 层处理
SELECTCOALESCE(name,''),COALESCE(age,0)FROMusersWHEREid=1直接在 SQL 里把 NULL 转成默认值,Go 侧用基础类型接收。简单但丢失了"原始值是否为 NULL"的信息。
完整练习代码
// crud_prepared_statements.gopackagemainimport("database/sql""fmt""log"_"modernc.org/sqlite")// User 业务结构体typeUserstruct{IDint64NamestringEmailstringAgeintBiostring// 可能为 NULL}funcmain(){db,err:=sql.Open("sqlite",":memory:")iferr!=nil{log.Fatal(err)}deferdb.Close()// 建表(bio 字段允许 NULL)db.Exec(`CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, age INTEGER DEFAULT 0, bio TEXT )`)// ============================================// 1. Create: 预处理批量插入// ============================================fmt.Println("=== 批量插入(预处理语句) ===")insertStmt,err:=db.Prepare("INSERT INTO users(name, email, age, bio) VALUES(?, ?, ?, ?)")iferr!=nil{log.Fatal(err)}deferinsertStmt.Close()users:=[]User{{Name:"张三",Email:"zhangsan@test.com",Age:28,Bio:"后端工程师"},{Name:"李四",Email:"lisi@test.com",Age:32,Bio:""},// Bio 为空字符串{Name:"王五",Email:"wangwu@test.com",Age:25,Bio:""},// Bio 为 NULL}fori,u:=rangeusers{varbioArginterface{}ifu.Bio==""&&i==2{bioArg=nil// 王五的 bio 存为 NULL}else{bioArg=u.Bio}res,err:=insertStmt.Exec(u.Name,u.Email,u.Age,bioArg)iferr!=nil{log.Printf("插入 %s 失败: %v",u.Name,err)continue}id,_:=res.LastInsertId()fmt.Printf(" 插入成功: ID=%d, %s\n",id,u.Name)}// ============================================// 2. Read: 全量查询 + NULL 处理// ============================================fmt.Println("\n=== 查询所有用户(NULL 处理) ===")allUsers,err:=queryAllUsers(db)iferr!=nil{log.Fatal(err)}for_,u:=rangeallUsers{fmt.Printf(" [%d] %s <%s> %d岁 | bio: %s\n",u.ID,u.Name,u.Email,u.Age,u.Bio)}// ============================================// 3. Read: 按 ID 查询单个用户// ============================================fmt.Println("\n=== 按 ID 查询 ===")u,err:=getUserByID(db,1)iferr!=nil{log.Fatal(err)}fmt.Printf(" 用户: %+v\n",u)// 查询不存在_,err=getUserByID(db,999)iferr==sql.ErrNoRows{fmt.Println(" ID=999 的用户不存在(预期行为)")}// ============================================// 4. Update: 更新用户信息// ============================================fmt.Println("\n=== 更新操作 ===")affected,err:=updateUserAge(db,1,29)iferr!=nil{log.Fatal(err)}fmt.Printf(" 更新影响行数: %d\n",affected)u,_=getUserByID(db,1)fmt.Printf(" 更新后: %s, %d岁\n",u.Name,u.Age)// ============================================// 5. Delete: 删除用户// ============================================fmt.Println("\n=== 删除操作 ===")deleted,err:=deleteUser(db,3)iferr!=nil{log.Fatal(err)}fmt.Printf(" 删除影响行数: %d\n",deleted)// 验证删除结果count:=countUsers(db)fmt.Printf(" 剩余用户数: %d\n",count)// ============================================// 6. 高级查询:条件筛选 + 排序 + 分页// ============================================fmt.Println("\n=== 分页查询: age > 26, 第1页 ===")pageUsers,err:=queryUsersWithFilter(db,26,10,0)iferr!=nil{log.Fatal(err)}for_,u:=rangepageUsers{fmt.Printf(" [%d] %s, %d岁\n",u.ID,u.Name,u.Age)}}// queryAllUsers 查询所有用户,正确处理 NULL 值funcqueryAllUsers(db*sql.DB)([]User,error){rows,err:=db.Query("SELECT id, name, email, age, bio FROM users ORDER BY id")iferr!=nil{returnnil,err}deferrows.Close()varusers[]Userforrows.Next(){varu Uservarbio sql.NullString// 用 sql.NullString 接收可能为 NULL 的字段iferr:=rows.Scan(&u.ID,&u.Name,&u.Email,&u.Age,&bio);err!=nil{returnnil,err}ifbio.Valid{u.Bio=bio.String}else{u.Bio="(未填写)"}users=append(users,u)}returnusers,rows.Err()}// getUserByID 按 ID 查询单个用户funcgetUserByID(db*sql.DB,idint64)(User,error){varu Uservarbio sql.NullString err:=db.QueryRow("SELECT id, name, email, age, bio FROM users WHERE id = ?",id,).Scan(&u.ID,&u.Name,&u.Email,&u.Age,&bio)ifbio.Valid{u.Bio=bio.String}returnu,err}// updateUserAge 更新用户年龄funcupdateUserAge(db*sql.DB,idint64,ageint)(int64,error){res,err:=db.Exec("UPDATE users SET age = ? WHERE id = ?",age,id)iferr!=nil{return0,err}returnres.RowsAffected()}// deleteUser 删除用户funcdeleteUser(db*sql.DB,idint64)(int64,error){res,err:=db.Exec("DELETE FROM users WHERE id = ?",id)iferr!=nil{return0,err}returnres.RowsAffected()}// countUsers 统计用户总数funccountUsers(db*sql.DB)int{varcountintdb.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)returncount}// queryUsersWithFilter 条件筛选 + 分页查询funcqueryUsersWithFilter(db*sql.DB,minAge,limit,offsetint)([]User,error){rows,err:=db.Query("SELECT id, name, email, age, bio FROM users WHERE age > ? ORDER BY age LIMIT ? OFFSET ?",minAge,limit,offset,)iferr!=nil{returnnil,err}deferrows.Close()varusers[]Userforrows.Next(){varu Uservarbio sql.NullStringiferr:=rows.Scan(&u.ID,&u.Name,&u.Email,&u.Age,&bio);err!=nil{returnnil,err}ifbio.Valid{u.Bio=bio.String}users=append(users,u)}returnusers,rows.Err()}运行方式
mkdir-pdemo2&&cddemo2 go mod init demo2 go get modernc.org/sqlite go run main.go要点总结
- 预处理语句
db.Prepare()返回*sql.Stmt,适合同一条 SQL 反复执行的场景;用完必须stmt.Close() - NULL 处理三方案:
sql.NullString最规范(有 Valid 标志),指针方案最简洁但有 panic 风险,COALESCE最简单但丢失 NULL 语义 - 参数占位符
?是database/sql的标准写法,驱动负责转义和类型转换,永远不要手动拼接 SQL 字符串 - Scan 目标参数顺序必须与 SELECT 字段顺序一一对应,数量不匹配会报错
rows.Err()必须在循环结束后检查——Next()返回 false 可能是正常结束,也可能是出错,只有Err()能区分