当前位置: 首页 > news >正文

Golang CRUD 操作与预处理语句

CRUD 操作与预处理语句

核心概念

CRUD(Create/Read/Update/Delete)是数据库操作的基本功。在database/sql体系下,除了直接拼 SQL 字符串,还有**预处理语句(Prepared Statement)**这一利器。预处理将 SQL 模板和参数分离,带来三个好处:

  1. 安全:参数自动转义,从根本上杜绝 SQL 注入
  2. 性能:同一条 SQL 只解析一次,多次执行复用执行计划
  3. 类型安全:驱动负责 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

要点总结

  1. 预处理语句db.Prepare()返回*sql.Stmt,适合同一条 SQL 反复执行的场景;用完必须stmt.Close()
  2. NULL 处理三方案sql.NullString最规范(有 Valid 标志),指针方案最简洁但有 panic 风险,COALESCE最简单但丢失 NULL 语义
  3. 参数占位符?database/sql的标准写法,驱动负责转义和类型转换,永远不要手动拼接 SQL 字符串
  4. Scan 目标参数顺序必须与 SELECT 字段顺序一一对应,数量不匹配会报错
  5. rows.Err()必须在循环结束后检查——Next()返回 false 可能是正常结束,也可能是出错,只有Err()能区分
http://www.jsqmd.com/news/1407272/

相关文章:

  • 统一的数据存储:一切皆为账户
  • “我想重活一次”的庖丁解牛
  • FOFA搜索语法全解析:从资产测绘到漏洞应急的精准定位指南
  • 深度分析新时代中企业应该如何利用AI更好提质增效
  • 76-覆盖率趋势图与多指标展示:为什么趋势图里不该只有一条线
  • AI agent 初步了解
  • 成都本地口碑好的装修公司怎么选?三类装修公司对比推荐与签约避坑指南 - 商业大观
  • 麒麟V10 SP1桌面版NFS共享部署全攻略:从环境配置到跨平台访问
  • 腾讯WorkBuddy桌面智能体:AI原生Agent如何重构办公自动化
  • 汽车制造视觉传感器怎么选:东集SEV400破局复杂工况检测难题 - 天下观知
  • 成都本地口碑好的装修公司怎么选?三类高性价比服务商推荐与横向对比,附合作签约避坑指南 - 产业观察报
  • OpenClaw AI Agent框架:从模块化设计到生产部署实战指南
  • Gitee代码仓库入门指南:从本地备份到团队协作的完整工作流
  • windows wsl2 安装 gentoo 的步骤
  • 开源智能体OpenClaw集成腾讯云ADP:企业级AI应用架构与实战
  • 图形化编程实现步骤之实现登录注册界面
  • 从微信聊天到网页浏览:数据如何在网络中传输?
  • Linux系统级备份与还原实战:从tar、dd到Clonezilla的完整方案解析
  • Win10桌面美化全攻略:从壁纸到Rainmeter,打造高效个性化工作空间
  • AlmaLinux 9部署OpenClaw:SELinux权限排错与自动化部署实战
  • AI技能开发中长耗时任务异步处理与状态管理实战
  • 成都口碑好的装修公司怎么选?三类靠谱装企对比与推荐,签约避坑指南 - 行业观察网
  • Python 量化实战:基于 QuantDash 搭建全市场 A 股涨停板炸板率实时监控与风控预警系统
  • Win11连接共享打印机失败?系统性排查与修复指南
  • 深入解析Git自动合并:Fast-Forward与三路合并原理及实战指南
  • 基于OpenClaw与UI自动化实现闲鱼智能客服与自动发货系统
  • windows 10 LTSC版 打开 wsl2
  • PKC 第 129 个开关:在弹窗展示按钮的位置、验证方法与风险边界
  • 小红书 算法一面 一
  • 我回测了A 股10 年的”追涨停”策略,结果可能和你想的不一样