Go 语言操作金仓数据库(下篇):SQL 执行、类型映射与超时控制

Go 语言操作金仓数据库(下篇):SQL 执行、类型映射与超时控制

接上篇,环境搭好了,连接也配好了。这篇讲真正干活的部分------怎么执行 SQL、怎么处理结果、怎么用预备语句防止注入、怎么处理存储过程的 OUT 参数。

一、执行 SQL 语句

1.1 查询操作(SELECT)

查询用 Query 方法,返回 Rows 对象。这个对象会占用一个数据库连接,用完必须 Close

go 复制代码
package main

import (
    "database/sql"
    "fmt"
    _ "kingbase.com/gokb"
)

// 定义结构体接收查询结果
type User struct {
    Id   int
    Name string
    Age  int
}

func main() {
    connStr := "host=127.0.0.1 user=system password=123456 dbname=TEST sslmode=disable"
    db, err := sql.Open("kingbase", connStr)
    if err != nil {
        panic(err)
    }
    defer db.Close()

    // 执行查询
    rows, err := db.Query("SELECT id, name, age FROM users WHERE age > $1", 18)
    if err != nil {
        panic(err)
    }
    defer rows.Close()  // 重要:用完关闭

    var users []User
    for rows.Next() {
        var u User
        // Scan 按顺序把列值赋给变量
        err = rows.Scan(&u.Id, &u.Name, &u.Age)
        if err != nil {
            panic(err)
        }
        users = append(users, u)
    }

    // 检查遍历过程中是否有错误
    if err = rows.Err(); err != nil {
        panic(err)
    }

    for _, u := range users {
        fmt.Printf("id=%d, name=%s, age=%d\n", u.Id, u.Name, u.Age)
    }
}

几个要点:

  • $1$2 作为占位符(不是 ?
  • defer rows.Close() 是必须的,不然连接会泄漏
  • rows.Err() 检查循环中是否出错

1.2 执行非查询(INSERT/UPDATE/DELETE)

不返回结果集的 SQL 用 Exec 方法:

go 复制代码
// 插入数据
result, err := db.Exec("INSERT INTO users(name, age) VALUES($1, $2)", "张三", 25)
if err != nil {
    panic(err)
}

// 获取插入的行数
rowsAffected, err := result.RowsAffected()
fmt.Printf("影响了 %d 行\n", rowsAffected)

// 获取自增 ID(需要开启 get_last_insert_id 参数)
// lastId, err := result.LastInsertId()

1.3 一次执行多条 SQL

Exec 一次只能执行一条。多条 SQL 需要分别调用,或者用事务包装:

go 复制代码
// 开启事务
tx, err := db.Begin()
if err != nil {
    panic(err)
}

// 执行多条
_, err = tx.Exec("INSERT INTO users(name, age) VALUES($1, $2)", "李四", 30)
if err != nil {
    tx.Rollback()
    panic(err)
}

_, err = tx.Exec("UPDATE stats SET count = count + 1")
if err != nil {
    tx.Rollback()
    panic(err)
}

// 提交
err = tx.Commit()
if err != nil {
    panic(err)
}

二、预备语句(Prepared Statement)

预备语句有两个好处:

  1. 防 SQL 注入:参数和 SQL 结构分离,恶意输入不会被当作 SQL 执行
  2. 提升性能:数据库只解析一次,多次执行时复用执行计划

2.1 基本用法

go 复制代码
// 准备 SQL
stmt, err := db.Prepare("INSERT INTO users(name, age) VALUES($1, $2)")
if err != nil {
    panic(err)
}
defer stmt.Close()  // 用完关闭

// 多次执行
_, err = stmt.Exec("王五", 28)
_, err = stmt.Exec("赵六", 32)

2.2 查询用预备语句

go 复制代码
stmt, err := db.Prepare("SELECT id, name FROM users WHERE age > $1")
if err != nil {
    panic(err)
}
defer stmt.Close()

rows, err := stmt.Query(25)
// 处理 rows...

2.3 三种占位符风格

Gokb 支持三种占位符,习惯用哪个都行:

go 复制代码
// 1. 匿名占位符 $N(推荐)
db.Query("SELECT * FROM users WHERE id = $1 AND name = $2", 1, "张三")

// 2. 问号占位符
db.Query("SELECT * FROM users WHERE id = ? AND name = ?", 1, "张三")

// 3. 命名占位符 :NAME(适合参数多的情况)
db.Query("SELECT * FROM users WHERE id = :id AND name = :name", 
    sql.Named("id", 1), sql.Named("name", "张三"))

命名占位符在参数多的时候代码更清晰,推荐使用。

三、类型映射

Go 类型和金仓数据库类型的对应关系:

Kingbase 类型 Go 类型
smallint, integer, bigint int64
real, double float64
char, varchar, text, clob string
date, time, timestamp time.Time
boolean bool
bytea, blob []byte
其他类型 []byte

示例:

go 复制代码
type LogRecord struct {
    Id        int64
    Message   string
    CreatedAt time.Time
    IsActive  bool
    Data      []byte
}

rows, _ := db.Query("SELECT id, message, created_at, is_active, data FROM logs")
for rows.Next() {
    var r LogRecord
    rows.Scan(&r.Id, &r.Message, &r.CreatedAt, &r.IsActive, &r.Data)
    // 处理数据...
}

3.1 处理 NULL 值

数据库字段可能是 NULL,Go 的基本类型不能表示 NULL。需要用 sql.NullStringsql.NullInt64 等:

go 复制代码
type User struct {
    Id     int64
    Name   string
    Email  sql.NullString  // 可能为 NULL
    Age    sql.NullInt64   // 可能为 NULL
}

rows, _ := db.Query("SELECT id, name, email, age FROM users")
for rows.Next() {
    var u User
    rows.Scan(&u.Id, &u.Name, &u.Email, &u.Age)
    
    if u.Email.Valid {
        fmt.Println("email:", u.Email.String)
    } else {
        fmt.Println("email: NULL")
    }
}

四、调用存储过程

4.1 无 OUT 参数的存储过程

go 复制代码
// 直接使用 Exec
_, err := db.Exec("CALL update_user_status(1, 'active')")

4.2 带 OUT 参数的存储过程

OUT 参数需要用 sql.Out 类型绑定:

go 复制代码
// 假设存储过程定义:
// CREATE OR REPLACE PROCEDURE get_user_name(
//     p_id IN INT,
//     p_name OUT VARCHAR
// ) AS BEGIN
//     SELECT name INTO p_name FROM users WHERE id = p_id;
// END;

var userName string
_, err := db.Exec(
    "CALL get_user_name(:id, :name)",
    sql.Named("id", 1),
    sql.Named("name", sql.Out{Dest: &userName}),
)
if err != nil {
    panic(err)
}
fmt.Println("用户名:", userName)

4.3 带返回值的存储过程

Kingbase 的 SQL Server 模式下,存储过程可以有返回值。用 gokb.ReturnStatus 接收:

go 复制代码
import (
    "database/sql"
    "kingbase.com/gokb"
    _ "kingbase.com/gokb"
)

var ret gokb.ReturnStatus
_, err := db.Exec(
    "proc_name",
    &ret,  // 第一个参数放返回值
    sql.Named("p1", 100),
    sql.Named("p2", sql.Out{Dest: &outValue}),
)
fmt.Println("返回值:", ret)

五、超时控制

5.1 连接超时

在连接字符串中配置 connect_timeout

go 复制代码
connStr := "host=127.0.0.1 user=system password=123456 dbname=TEST connect_timeout=10"

这个控制的是 TCP 连接建立的最长等待时间,单位秒。

5.2 执行超时

对于慢查询,可以用 Context 控制超时:

go 复制代码
import "context"

// 设置 5 秒超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 使用 QueryContext 或 ExecContext
rows, err := db.QueryContext(ctx, "SELECT * FROM huge_table WHERE complex_condition")
if err != nil {
    if err == context.DeadlineExceeded {
        fmt.Println("查询超时了")
    }
    panic(err)
}

这个超时依赖网络和服务端正常运行。如果出现断网、数据库宕机,Context 超时可能不生效。那种场景需要用 TCP 层面的超时参数:

go 复制代码
connStr := "host=127.0.0.1 user=system password=123456 dbname=TEST tcp_user_timeout=30000 keepalive_interval=10 keepalive_count=3"
  • tcp_user_timeout:TCP 层面最长等待时间(毫秒)
  • keepalive_interval:保活探测间隔(秒)
  • keepalive_count:连续多少次无响应后断开

六、获取自增列值

如果表有自增列(SERIAL 或 IDENTITY),插入后想获取自动生成的值,需要两步:

  1. 连接字符串中开启 get_last_insert_id=yes
  2. 确保自增列是表的第一个字段
go 复制代码
connStr := "host=127.0.0.1 user=system password=123456 dbname=TEST get_last_insert_id=yes"

db, _ := sql.Open("kingbase", connStr)

// 假设表定义:id SERIAL PRIMARY KEY, name VARCHAR(100)
result, err := db.Exec("INSERT INTO users(name) VALUES($1)", "张三")
if err != nil {
    panic(err)
}

lastId, err := result.LastInsertId()
fmt.Printf("刚插入的 ID: %d\n", lastId)

限制

  • 自增列必须在第一列
  • INSERT IGNORE 不支持获取自增值

七、完整示例

go 复制代码
package main

import (
    "context"
    "database/sql"
    "fmt"
    "time"
    _ "kingbase.com/gokb"
)

type User struct {
    Id   int
    Name string
    Age  int
}

func main() {
    connStr := "host=127.0.0.1 user=system password=123456 dbname=TEST sslmode=disable connect_timeout=10"
    db, err := sql.Open("kingbase", connStr)
    if err != nil {
        panic(err)
    }
    defer db.Close()

    db.SetMaxOpenConns(10)

    // 1. 建表
    _, err = db.Exec(`CREATE TABLE IF NOT EXISTS go_users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        age INT
    )`)
    if err != nil {
        panic(err)
    }
    fmt.Println("建表成功")

    // 2. 插入数据(预备语句)
    stmt, err := db.Prepare("INSERT INTO go_users(name, age) VALUES($1, $2)")
    if err != nil {
        panic(err)
    }
    defer stmt.Close()

    users := []User{
        {Name: "张三", Age: 25},
        {Name: "李四", Age: 30},
        {Name: "王五", Age: 28},
    }
    for _, u := range users {
        _, err = stmt.Exec(u.Name, u.Age)
        if err != nil {
            panic(err)
        }
    }
    fmt.Println("插入数据成功")

    // 3. 查询数据
    rows, err := db.Query("SELECT id, name, age FROM go_users WHERE age > $1", 20)
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    var results []User
    for rows.Next() {
        var u User
        err = rows.Scan(&u.Id, &u.Name, &u.Age)
        if err != nil {
            panic(err)
        }
        results = append(results, u)
    }

    fmt.Println("查询结果:")
    for _, u := range results {
        fmt.Printf("  id=%d, name=%s, age=%d\n", u.Id, u.Name, u.Age)
    }

    // 4. 更新数据
    result, err := db.Exec("UPDATE go_users SET age = $1 WHERE name = $2", 26, "张三")
    if err != nil {
        panic(err)
    }
    affected, _ := result.RowsAffected()
    fmt.Printf("更新了 %d 行\n", affected)

    // 5. 带超时的查询
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    
    row := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM go_users")
    var count int
    err = row.Scan(&count)
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("查询超时")
        } else {
            panic(err)
        }
    } else {
        fmt.Printf("总记录数: %d\n", count)
    }
}

八、常见问题

预备语句不释放会怎样? 每个 Prepare 都会在数据库端创建一个预备语句对象,不 Close 会一直占用资源。务必加上 defer stmt.Close()

连接数耗尽怎么办? 检查是否忘了 rows.Close(),或者事务没有 Commit/Rollback。可以用 SetMaxOpenConns 限制最大连接数。

怎么调试 SQL? Go 标准库没有内置 SQL 日志。可以自己包装一层:

go 复制代码
func Query(db *sql.DB, query string, args ...interface{}) (*sql.Rows, error) {
    log.Printf("SQL: %s, args: %v", query, args)
    return db.Query(query, args...)
}

九、小结

下篇主要讲了:

  1. SQL 执行Query 查、Exec 写,记住 defer rows.Close()
  2. 预备语句 :防注入、提升性能,支持三种占位符($1?:name
  3. 类型映射 :NULL 值用 sql.NullXxx,时间用 time.Time
  4. 存储过程 :OUT 参数用 sql.Out,返回值用 gokb.ReturnStatus
  5. 超时控制connect_timeout 连不上超时,Context 查得慢超时

Gokb 驱动整体还算顺手,遵循 Go 的 database/sql 标准接口,上手成本不高。遇到问题先检查连接参数和预备语句是否正确释放,大部分问题都能解决。

相关推荐
IVEN_1 小时前
全栈开发必看:从内存变量到关系型数据库的完整旅程
后端
MacroZheng1 小时前
横空出世!IDEA最强MyBatis插件来了,功能很全!
java·后端·mybatis
codebetter1 小时前
X86 Windows Docker Desktop 运行 arm64 容器
后端
掘金者阿豪1 小时前
Go 语言操作金仓数据库(上篇):环境搭建与连接管理
后端
何陋轩1 小时前
Spring AI Function Calling:让AI调用你的Java方法
人工智能·后端·ai编程
alwaysrun1 小时前
Rust之异步框架Tokio
后端·编程语言
Csvn1 小时前
日志系统
后端·python
CodeSheep1 小时前
中国编程第一人,一人抵一城!
前端·后端·程序员