Go database/sql Scan 得到的 []byte 为什么不能直接长期保存
如果自定义 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、不支持的源类型和复用对象时的旧值清理。
Go database/sql 怎样依次读取多个 SQL 结果集
- 上一篇
- Go database/sql 怎样依次读取多个 SQL 结果集
- 下一篇
- 兽音译者公开网页怎么选?功能区、域名与免责声明核对说明
-
- Golang · Go教程 | 35分钟前 | 命令行 · go · 工程实践 · go generate Go代码生成 go generate -run 生成指令筛选
- go generate -run 怎样只执行指定生成指令
- 453浏览 收藏
-
- Golang · Go教程 | 39分钟前 | go · go test -json TestEvent Go测试事件 BuildEvent FailedBuild
- go test -json 输出怎样区分测试事件和构建事件
- 173浏览 收藏
-
- Golang · Go教程 | 48分钟前 | 基准测试 · testing · 性能 · Go教程 · GOMAXPROCS RunParallel Go testing.B.SetParallelism 并行基准 IO吞吐
- Go testing.B.SetParallelism 提高 IO 型并行基准吞吐的配置方法
- 404浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go testing.T.Cleanup 多次注册时为什么按逆序执行
- 309浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go flag.FlagSet 怎样让参数错误返回给调用方
- 273浏览 收藏
-
- Golang · Go教程 | 1小时前 | flag · go ·
- Go flag.BoolFunc 怎样处理重复出现的布尔开关
- 246浏览 收藏
-
- Golang · Go教程 | 1小时前 | 反射 · Go教程 · database/sql · Go database/sql Rows.Scan ColumnType ScanType 动态查询导出
- Go database/sql ColumnType.ScanType 怎样支持动态查询导出
- 381浏览 收藏
-
- Golang · Go教程 | 1小时前 | flag · go · Go flag.TextVar encoding.TextUnmarshaler
- Go flag.TextVar 怎样复用类型的文本解析逻辑
- 479浏览 收藏
-
- Golang · Go教程 | 1小时前 | 数据库 · Go教程 · database/sql · Go database/sql rows NextResultSet SQL结果集
- Go database/sql 怎样依次读取多个 SQL 结果集
- 456浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go xml.Decoder.DecodeElement 怎样读取嵌套节点
- 436浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 446次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 517次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 532次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 478次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 303次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览
-
- go语言数据类型之字符串string
- 2022-12-30 321浏览

