Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Golang CRUD 操作

Golang CRUD 操作方法与预处理语句

作者:FfHUCisI

在Go语言(Golang)中,进行CRUD(创建、读取、更新、删除)操作时,使用预处理语句(Prepared Statements)是一种高效且安全的方法,本文给大家介绍Golang CRUD操作与预处理语句,感兴趣的朋友一起看看吧

核心概念

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

  1. 安全:参数自动转义,从根本上杜绝 SQL 注入
  2. 性能:同一条 SQL 只解析一次,多次执行复用执行计划
  3. 类型安全:驱动负责 Go 类型到数据库类型的转换

预处理 vs 直接执行

// 直接执行:每次都要解析 SQL
db.Exec("INSERT INTO users(name, age) VALUES('张三', 25)")
// 预处理:SQL 只解析一次,后续执行只传参数
stmt, _ := db.Prepare("INSERT INTO users(name, age) VALUES(?, ?)")
defer stmt.Close()
stmt.Exec("张三", 25)
stmt.Exec("李四", 32)  // 复用同一个执行计划

NULL 值处理

数据库中的 NULL 是一个独特的值,它不等于空字符串也不等于 0。Go 的基础类型(string, int, float64)无法表达 NULL,直接 Scan 会报错。

方案一:sql.NullXxx 类型

var name sql.NullString
var age sql.NullInt64
err := db.QueryRow("SELECT name, age FROM users WHERE id = ?", 1).Scan(&name, &age)
if name.Valid {
    fmt.Println(name.String)  // 非 NULL 时可以取值
} else {
    fmt.Println("name is NULL")
}

sql.NullString 是一个结构体:type NullString struct { String string; Valid bool }Valid 为 false 表示数据库值为 NULL。

方案二:使用指针类型

var name *string
var age *int
db.QueryRow("SELECT name, age FROM users WHERE id = ?", 1).Scan(&name, &age)
if name != nil {
    fmt.Println(*name)  // 解引用
} else {
    fmt.Println("NULL")
}

指针方案更简洁,但每次都要解引用,且 nil 指针解引用会 panic。

方案三:COALESCE 在 SQL 层处理

SELECT COALESCE(name, ''), COALESCE(age, 0) FROM users WHERE id = 1

直接在 SQL 里把 NULL 转成默认值,Go 侧用基础类型接收。简单但丢失了"原始值是否为 NULL"的信息。

完整练习代码

// crud_prepared_statements.go
package main
import (
	"database/sql"
	"fmt"
	"log"
	_ "modernc.org/sqlite"
)
// User 业务结构体
type User struct {
	ID    int64
	Name  string
	Email string
	Age   int
	Bio   string // 可能为 NULL
}
func main() {
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		log.Fatal(err)
	}
	defer db.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(?, ?, ?, ?)")
	if err != nil {
		log.Fatal(err)
	}
	defer insertStmt.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
	}
	for i, u := range users {
		var bioArg interface{}
		if u.Bio == "" && i == 2 {
			bioArg = nil // 王五的 bio 存为 NULL
		} else {
			bioArg = u.Bio
		}
		res, err := insertStmt.Exec(u.Name, u.Email, u.Age, bioArg)
		if err != 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)
	if err != nil {
		log.Fatal(err)
	}
	for _, u := range allUsers {
		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)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("  用户: %+v\n", u)
	// 查询不存在
	_, err = getUserByID(db, 999)
	if err == sql.ErrNoRows {
		fmt.Println("  ID=999 的用户不存在(预期行为)")
	}
	// ============================================
	// 4. Update: 更新用户信息
	// ============================================
	fmt.Println("\n=== 更新操作 ===")
	affected, err := updateUserAge(db, 1, 29)
	if err != 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)
	if err != 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)
	if err != nil {
		log.Fatal(err)
	}
	for _, u := range pageUsers {
		fmt.Printf("  [%d] %s, %d岁\n", u.ID, u.Name, u.Age)
	}
}
// queryAllUsers 查询所有用户,正确处理 NULL 值
func queryAllUsers(db *sql.DB) ([]User, error) {
	rows, err := db.Query("SELECT id, name, email, age, bio FROM users ORDER BY id")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var users []User
	for rows.Next() {
		var u User
		var bio sql.NullString // 用 sql.NullString 接收可能为 NULL 的字段
		if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age, &bio); err != nil {
			return nil, err
		}
		if bio.Valid {
			u.Bio = bio.String
		} else {
			u.Bio = "(未填写)"
		}
		users = append(users, u)
	}
	return users, rows.Err()
}
// getUserByID 按 ID 查询单个用户
func getUserByID(db *sql.DB, id int64) (User, error) {
	var u User
	var bio 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)
	if bio.Valid {
		u.Bio = bio.String
	}
	return u, err
}
// updateUserAge 更新用户年龄
func updateUserAge(db *sql.DB, id int64, age int) (int64, error) {
	res, err := db.Exec("UPDATE users SET age = ? WHERE id = ?", age, id)
	if err != nil {
		return 0, err
	}
	return res.RowsAffected()
}
// deleteUser 删除用户
func deleteUser(db *sql.DB, id int64) (int64, error) {
	res, err := db.Exec("DELETE FROM users WHERE id = ?", id)
	if err != nil {
		return 0, err
	}
	return res.RowsAffected()
}
// countUsers 统计用户总数
func countUsers(db *sql.DB) int {
	var count int
	db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
	return count
}
// queryUsersWithFilter 条件筛选 + 分页查询
func queryUsersWithFilter(db *sql.DB, minAge, limit, offset int) ([]User, error) {
	rows, err := db.Query(
		"SELECT id, name, email, age, bio FROM users WHERE age > ? ORDER BY age LIMIT ? OFFSET ?",
		minAge, limit, offset,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var users []User
	for rows.Next() {
		var u User
		var bio sql.NullString
		if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age, &bio); err != nil {
			return nil, err
		}
		if bio.Valid {
			u.Bio = bio.String
		}
		users = append(users, u)
	}
	return users, rows.Err()
}

运行方式

mkdir -p demo2 && cd demo2
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() 能区分

到此这篇关于Golang CRUD 操作方法与预处理语句的文章就介绍到这了,更多相关Golang CRUD 操作内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文