Go database/sql Scan 得到的 []byte 为什么不能直接长期保存
来源:17golang原创
时间:2026-10-13 01:26:20 343浏览 收藏
如果自定义 database/sql.Scanner 收到的是 []byte,默认应把它看成“驱动暂时借给你的输入”,而不是已经归你所有的数组。Go 官方文档明确说明,引用类型(例如 []byte)只在下一次调用 Scan 前有效;需要保留时,要在下一次扫描前复制内容。
官方文档:https://pkg.go.dev/database/sql#Scanner
不过,Rows.Scan 直接扫描到 *[]byte 时,database/sql 会保存一份由调用方拥有的副本。真正容易出错的是把这两种语义混在一起:自定义 Scanner 需要自己复制,普通的 *[]byte 目标则已经完成了复制。
先分清 Scanner 输入是谁拥有
自定义 Scanner 的 Scan(src any) 收到的数据来自数据库驱动。对于 []byte,接口文档把它归为引用类型:底层内存由驱动管理,下一次扫描可能复用或改变它。于是下面这种赋值虽然语法正确,却把一个短生命周期的切片头存进了长期对象。

// BadBlob 演示了错误的所有权转移:data 仍然指向驱动管理的内存。
type BadBlob struct {
Data []byte
}
// Scan 只保存切片头,下一次 Scan 后 Data 可能观察到变化。
func (b *BadBlob) Scan(src any) error {
data, ok := src.([]byte)
if !ok {
return fmt.Errorf("want []byte, got %T", src)
}
b.Data = data
return nil
}
这个问题通常出现在把字段放入缓存、消息、异步任务或跨函数返回值时。当前行结束后马上读取,往往看不出问题;循环继续扫描下一行,或者驱动复用缓冲区后,旧对象才可能出现内容变动。因此判断标准不是“现在能不能读”,而是“这个字节是否要跨过下一次 Scan”。
Rows.Scan 的三种字节接收方式不要混用
普通查询代码还要区分目标类型。扫描到 *[]byte 时,文档规定 Scan 会保存对应数据的副本,副本由调用方拥有,可以修改并长期持有;扫描到 *sql.RawBytes 则是为了避免复制,生命周期限制更严格。扫描到 *any 时,如果源值是 []byte,也会复制一份。
| Scan 目标 | 是否复制 | 适合场景 |
|---|---|---|
*[]byte | 会复制 | 需要把字段保存到结构体、缓存或后续任务 |
*sql.RawBytes | 不保证长期有效 | 只在当前行内读取,明确接受借用生命周期 |
*any 且源为 []byte | 会复制 | 暂时不知道源字段具体类型的通用接收 |
自定义 Scanner | 由实现决定 | 在 Scan 内自行建立所有权边界 |
// 直接使用 *[]byte,database/sql 会把当前列复制给调用方。
var payload []byte
if err := row.Scan(&payload); err != nil {
return fmt.Errorf("scan payload: %w", err)
}
// payload 可以交给后续函数或保存到结构体,不依赖下一次 Scan。
saved := append([]byte(nil), payload...)
如果只是处理当前行,可以使用 sql.RawBytes 减少一次复制,但必须把它限制在当前行的同步逻辑里。不要把它放入循环外的结果集合,也不要把它直接交给会异步执行的 goroutine。
自定义 Scanner 中要在入口处复制
自定义类型最稳妥的做法是在收到 []byte 时立刻复制,并在复制前处理 nil。使用接收对象已有容量可以减少分配,但要先把新数据复制进去,再替换长度;不要把原始切片直接赋值给字段。
package main
import (
"database/sql/driver"
"fmt"
)
// BlobValue 明确表示已经由业务对象拥有的二进制值。
type BlobValue struct {
Data []byte
}
// Scan 在 Scanner 边界复制驱动输入,避免下一次 Scan 改变已保存内容。
func (b *BlobValue) Scan(src any) error {
switch value := src.(type) {
case nil:
// NULL 代表没有值;清空旧内容,避免复用对象时残留上一行数据。
b.Data = nil
return nil
case []byte:
// 复用 b.Data 的容量,但不复用驱动拥有的底层数组。
b.Data = append(b.Data[:0], value...)
return nil
default:
return fmt.Errorf("BlobValue.Scan: unsupported source %T", src)
}
}
// Value 只返回业务对象自己的副本,避免调用方修改内部状态。
func (b BlobValue) Value() (driver.Value, error) {
return append([]byte(nil), b.Data...), nil
}
append(b.Data[:0], value...) 只复用 BlobValue 自己已经持有的容量;当 b.Data 为空时,append 会创建新的底层数组,不会把驱动切片挂进来。Value 是否需要再复制,取决于驱动对返回值的使用约定;在通用类型中返回副本更容易保持边界清晰。
![Rows.Scan 按目标类型分成 *[]byte 复制、sql.RawBytes 借用、*any 复制和自定义 Scanner 自行复制的决策关系图](/uploads/20261013/1791825979-c838a38c5d-80f1522f08-scan-target-copy-decision.webp)
把复制边界放在进入长期结构的位置
生命周期问题不只发生在自定义 Scanner。即使使用了 *[]byte,也应该让“读取数据库”和“进入长期结构”之间的边界可见。下面的循环把每行数据复制成独立切片后再追加,后续排序、缓存或异步处理都不再依赖当前行的临时状态。
// collectBlobs 把每一行的二进制列转换为独立拥有的切片。
func collectBlobs(rows *sql.Rows) ([][]byte, error) {
defer rows.Close()
var result [][]byte
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
return nil, fmt.Errorf("scan blob: %w", err)
}
// *[]byte 已由 database/sql 提供副本;再复制一次是为了让结果集合的所有权更直观。
result = append(result, append([]byte(nil), raw...))
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate rows: %w", err)
}
return result, nil
}
上面的第二次复制不是所有场景都必需;它表达的是“结果集合自己拥有一份数据”。如果确认 raw 已经由 *[]byte 复制,并且后续不会复用或修改它,可以直接追加 raw。关键是要基于接收类型做判断,而不是看到 []byte 就一律认为它是借用内存。
用一张表决定是否需要复制
- 自定义
Scanner.Scan收到[]byte:需要跨过下一次Scan时,立即复制。 Rows.Scan(&value)的value是[]byte:database/sql已按文档提供调用方拥有的副本。Rows.Scan(&rawBytes)的rawBytes是sql.RawBytes:只在当前行内同步使用。- 数据要进入缓存、消息队列或 goroutine:在交接边界前确认所有权,必要时用
append([]byte(nil), src...)建立独立副本。
最终可以把规则记成一句话:看见 Scanner 输入的引用类型,先问它是否要跨过下一次 Scan;如果要,就复制;如果直接扫描到 *[]byte,则使用 database/sql 已经提供的副本。
常见问题
为什么同样是 []byte,Scan 到 *[]byte 却可以长期保存?
因为 Rows.Scan 对 *[]byte 的转换会保存一份副本,副本由调用方拥有。自定义 Scanner 收到的 []byte 则需要实现者自己遵守 Scanner 的生命周期约定。
sql.RawBytes 什么时候可以用?
只在当前行的同步处理里使用,并且不会把它返回、缓存或交给异步任务时使用。它的价值是避免复制,代价就是不能把它当成长期数据。
自定义 Scanner 只保存字符串可以吗?
如果在 Scan 内把源字节转换成新的 Go 字符串,结果不再依赖原始切片;但仍要处理 nil、不支持的源类型和复用对象时的旧值清理。
-
394 收藏
-
453 收藏
-
173 收藏
-
423 收藏
-
Golang · Go教程 | 47分钟前 | 基准测试 · testing · 性能 · Go教程 · GOMAXPROCS RunParallel Go testing.B.SetParallelism 并行基准 IO吞吐404 收藏
-
309 收藏
-
273 收藏
-
246 收藏
-
Golang · Go教程 | 1小时前 | 反射 · Go教程 · database/sql · Go database/sql Rows.Scan ColumnType ScanType 动态查询导出381 收藏
-
479 收藏
-
456 收藏
-
436 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习