Go语言操作金仓数据库之SQL执行,类型映射与超时控制详解
作者:小确幸
时间:2026-07-02
来源:互联网
浏览:0
好的,没问题。作为一位资深的 Go 语言开发者,这些年跟各种数据库打交道,踩过的坑、总结的技巧确实不少。这篇文章我们接着聊,继续深入 Go 操作金仓数据库的那些事儿。 环境搭好了,连接也配好了。那这篇就来点实在的,聊聊真正干活的部分——怎么执行 SQL、怎么处理结果、怎么用预备语句防止注入、怎么处理
好的,没问题。作为一位资深的 Go 语言开发者,这些年跟各种数据库打交道,踩过的坑、总结的技巧确实不少。这篇文章我们接着聊,继续深入 Go 操作金仓数据库的那些事儿。
环境搭好了,连接也配好了。那这篇就来点实在的,聊聊真正干活的部分——怎么执行 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` 这种形式,不是 MySQL 习惯的 `?`。
- `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` 一次只能执行一条。想跑多条,要么分多次调用,要么用事务包一下:
```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)
防注入、提性能,这是预备语句的两大核心卖点。参数和 SQL 结构分离,恶意输入自然没法作妖。数据库也只解析一次,反复执行时复用执行计划,效率高不少。
### 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 三种占位符风格
金仓驱动支持三种占位符,看个人喜好,哪个顺眼用哪个:
```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.NullString`、`sql.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 参数的存储过程
没有输出参数,直接用 `Exec` 就行:
```go
// 直接使用 Exec
_, err := db.Exec("CALL update_user_status(1, 'active')")
```
### 4.2 带 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`,控制的是 TCP 连接建立的最长等待时间,单位秒:
```go
connStr := "host=127.0.0.1 user=system password=123456 dbname=TEST connect_timeout=10"
```
### 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` 超时依赖网络和服务端都正常。如果网络断了或者数据库宕机,`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 日志。可以自己包装一层,打印一下执行的 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` 针对查得慢,TCP 参数对极端情况。
Gokb 驱动整体用下来还算顺手,它遵循 Go 的 `database/sql` 标准接口,上手成本不高。遇到问题,先检查连接参数和预备语句是否正常释放,绝大部分问题都能解决。
本文内容来源于互联网,如有侵权请联系删除。
## 一、执行 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` 这种形式,不是 MySQL 习惯的 `?`。
- `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` 一次只能执行一条。想跑多条,要么分多次调用,要么用事务包一下:
```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)
防注入、提性能,这是预备语句的两大核心卖点。参数和 SQL 结构分离,恶意输入自然没法作妖。数据库也只解析一次,反复执行时复用执行计划,效率高不少。
### 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 三种占位符风格
金仓驱动支持三种占位符,看个人喜好,哪个顺眼用哪个:
```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.NullString`、`sql.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 参数的存储过程
没有输出参数,直接用 `Exec` 就行:
```go
// 直接使用 Exec
_, err := db.Exec("CALL update_user_status(1, 'active')")
```
### 4.2 带 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`,控制的是 TCP 连接建立的最长等待时间,单位秒:
```go
connStr := "host=127.0.0.1 user=system password=123456 dbname=TEST connect_timeout=10"
```
### 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` 超时依赖网络和服务端都正常。如果网络断了或者数据库宕机,`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 日志。可以自己包装一层,打印一下执行的 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` 针对查得慢,TCP 参数对极端情况。
Gokb 驱动整体用下来还算顺手,它遵循 Go 的 `database/sql` 标准接口,上手成本不高。遇到问题,先检查连接参数和预备语句是否正常释放,绝大部分问题都能解决。
作者最新文章
Windows 10
2026-09-16 17:44
Python安装后怎么打开:使用IDLE或命令行启动解释器
2026-09-16 13:54
Windows系统Python安装教程:下载、勾选PATH及环境变量配置
2026-09-16 13:53
“等灯不计时”落地解析:算法善意如何转化为技术能力与生态协同
2026-09-08 18:03
英伟达推出NVHBM:定制HBM带宽提升30%并扩展NVLink Fusion生态
2026-09-08 17:31
热门文章
更多
精品专题
更多
Mac软件
更多
WINDOWS
更多
Windows 10
Windows
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式
Windows/macOS/Linux
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















