Golang CRUD 操作与预处理语句

📅 2026/8/16 22:02:36
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()能区分