发布时间:2026/8/16 22:02:48
Golang CRUD 操作与预处理语句 CRUD 操作与预处理语句核心概念CRUDCreate/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)FROMusersWHEREid1直接在 SQL 里把 NULL 转成默认值Go 侧用基础类型接收。简单但丢失了原始值是否为 NULL的信息。完整练习代码// crud_prepared_statements.gopackagemainimport(database/sqlfmtlog_modernc.org/sqlite)// User 业务结构体typeUserstruct{IDint64NamestringEmailstringAgeintBiostring// 可能为 NULL}funcmain(){db,err:sql.Open(sqlite,:memory:)iferr!nil{log.Fatal(err)}deferdb.Close()// 建表bio 字段允许 NULLdb.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:zhangsantest.com,Age:28,Bio:后端工程师},{Name:李四,Email:lisitest.com,Age:32,Bio:},// Bio 为空字符串{Name:王五,Email:wangwutest.com,Age:25,Bio:},// Bio 为 NULL}fori,u:rangeusers{varbioArginterface{}ifu.Bioi2{bioArgnil// 王五的 bio 存为 NULL}else{bioArgu.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)// 查询不存在_,errgetUserByID(db,999)iferrsql.ErrNoRows{fmt.Println( ID999 的用户不存在预期行为)}// // 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.Biobio.String}else{u.Bio(未填写)}usersappend(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.Biobio.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.Biobio.String}usersappend(users,u)}returnusers,rows.Err()}运行方式mkdir-pdemo2cddemo2 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()能区分

相关新闻

2026/8/16 21:57:46

Module Builder——Gem300之E40 ProcessJob模块

E40 ProcessJob(工艺作业,简称PJ)是SEMI E40(Processing Management)标准定义的核心对象,专门用于规范设备对物料(晶圆/基板)执行具体工艺加工的全生命周期管理。它是GEM300体系中真…

2026/8/16 23:12:54

大模型无状态架构解析:从原理到实战,构建有记忆的AI应用

1. 项目概述:从“健忘”的对话说起 你有没有过这样的体验?和某个AI助手聊得正欢,你告诉它你叫张三,喜欢打篮球,住在北京。聊了十几句后,你问它:“那我刚才说我叫什么名字来着?”它很…

2026/8/16 23:12:54

深度学习模型过拟合与超参数调优实战指南:从诊断到优化

1. 项目概述:从“炼丹”到“精炼”的实战心法 每次看到新手朋友在训练深度学习模型时,对着不断攀升的训练集精度和纹丝不动的验证集精度发愁,或者面对一大堆超参数无从下手时,我就想起自己刚入门那会儿的迷茫。大家常把模型训练戏…

2026/8/16 23:12:54

CAN 总线学习笔记:从物理层、报文帧到波形诊断的新人通用入门

适用读者:刚入行的硬件测试工程师、软件测试工程师、机器人测试工程师、汽车电子/工业控制调试工程师,以及第一次接触 CAN 总线的小白。 0. 理解 CAN,抓住 6 句话 CAN 是一种多节点共享的串行通信总线,常用于汽车、机器人、工程机械、工业控制、医疗设备等场景。 CAN 通信不…

2026/8/16 23:12:54

Docker Compose安装指南:二进制与pip方式详解及避坑实践

1. 为什么你需要关心docker-compose的安装方式? 如果你在Linux上折腾过Docker,大概率会遇到一个场景:你写好了一堆 docker run 命令,或者一个复杂的服务栈需要多个容器协同工作,每次启动都要手动敲一遍,…

2026/8/16 23:12:53

KKCE: 基于网站测速的WebRTC信令与ICE穿透延迟诊断-快快测

一、引言:为什么 TTFB 只有 30ms,视频首帧却要 5 秒? 在实时音视频应用里,我们习惯用 iperf 或简单的 HTTP 测速看网络质量。即便用 www.kkce.com 的 网站测速​ 对信令服务器做检测,TTFB 30ms、完全加载 80ms&#x…

2026/8/16 23:07:53

分治算法:从快排到归并排序的实战指南

会向耀灵淬天剑,剑拔飞刃斩仇雠。欢迎来到丘山望岳的小栈,今天分享分治算法,从熟络的快排归并讲起,话不多说,我们现在发车。 分治思想:把大问题拆成多个相同的小问题分别求解,再把小结果合并得到…

2026/8/16 0:00:35

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/16 0:00:36

工业传感器与变送器详解:序章 从物理世界到工业数据

序章 从物理世界到工业数据 ——重新认识工业传感器与变送器 工业自动化系统正变得日益复杂。今天的工业现场早已不是简单的控制回路,而是由多层技术共同构成的立体体系:PLC、DCS、SCADA、MES、工业互联网、边缘计算与人工智能。控制系统可以执行复杂算法,工业网络可以实现…

2026/8/16 0:00:35

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/16 0:00:36

工业传感器与变送器详解:序章 从物理世界到工业数据

序章 从物理世界到工业数据 ——重新认识工业传感器与变送器 工业自动化系统正变得日益复杂。今天的工业现场早已不是简单的控制回路,而是由多层技术共同构成的立体体系:PLC、DCS、SCADA、MES、工业互联网、边缘计算与人工智能。控制系统可以执行复杂算法,工业网络可以实现…

2026/8/15 9:46:39

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/16 16:53:03

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/15 9:46:30

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…