当前位置:首页 > 文章列表 > Golang > Go教程 > Go database/sql.RawBytes 为什么下一次 Scan 后内容会变化

Go database/sql.RawBytes 为什么下一次 Scan 后内容会变化

来源:17golang原创 2026-11-05 07:45:42 0浏览 收藏

Go database/sql.RawBytes 为什么下一次 Scan 后内容会变化

sql.RawBytes 保存的是数据库侧内存的借用引用,不是调用方独占的数据副本。一次 Rows.Scan 后,它只在下一次 Rows.Next、Rows.Scan 或 Rows.Close 之前有效。要把内容放进结果集合、缓存或异步任务,应在这个期限结束前复制;只做切片赋值或转换成 []byte,不会解决底层数组共享问题。

官方地址:https://pkg.go.dev/database/sql

为什么当场打印正常,循环结束却变了

假设查询逐行返回一列文本,循环内每次 Scan 后立即打印,内容看起来正确;把同一个 RawBytes 追加进二维切片,循环结束后再输出,却发现旧记录像是被后面的值覆盖。首先应检查保存的是“数据副本”还是“指向借用内存的切片”。

这种现象不要求驱动每次都复用同一块内存,也不保证在每个数据库环境中立即复现。接口只给出了有效期;即便某次运行内容没有改变,超过期限后继续使用仍没有契约保障。排障不能以“我这里暂时没变”作为允许长期持有的依据。

尤其要留意循环条件:下次循环里的 Next 已经是期限终点,不必等到下一次 Scan 完成才考虑复制。把复制放在成功 Scan 的当前迭代内,位置更明确。

重新声明变量,为什么仍不是独立数据

切片可以看作描述一段底层数组的视图。每次循环重新声明 raw,改变的是变量的存放位置,并不承诺驱动交付了新的数组。把 raw 放进结构体、接口或外层切片,只是让这个视图换了一个存放容器。

// 错误示例:这里只保存切片视图,没有复制字节。
var raw sql.RawBytes
if err := rows.Scan(&raw); err != nil {
    return err
}
saved = append(saved, raw)

// 类型转换仍引用原来的底层字节,不能延长借用期限。
view := []byte(raw)
_ = view

上面是解释误区的局部片段,saved 表示调用方的结果切片。问题不在 append 是否给外层集合扩容,而在集合元素所描述的字节是否属于调用方。外层集合有自己的存储,不代表每个元素的底层数组也被复制了。

RawBytes视图和结果集合共同引用借用字节的静态结构图
原创静态结构图:外层结果集合保存切片视图,RawBytes 与视图仍可关联同一借用内存;这不是运行截图。

正确做法:在当前行里复制,再保存

下面的函数接收一个只返回一列的 Rows,取得它的关闭责任,将每行内容保存到调用方拥有的内存。示例保留 nil 和非 nil 空切片的区别,不用 append 到 nil 的简写把两者合并。

package rowcopy

import (
    "database/sql"
    "fmt"
)

// CollectOneColumn 接管 rows;调用结束后不能继续遍历它。
func CollectOneColumn(rows *sql.Rows) (result [][]byte, resultErr error) {
    if rows == nil {
        return nil, fmt.Errorf("结果集不能为空")
    }
    defer func() {
        if err := rows.Close(); err != nil && resultErr == nil {
            result = nil
            resultErr = fmt.Errorf("关闭结果集: %w", err)
        }
    }()

    for rows.Next() {
        var raw sql.RawBytes
        if err := rows.Scan(&raw); err != nil {
            return nil, fmt.Errorf("扫描当前行: %w", err)
        }

        var owned []byte
        if raw != nil {
            // make 和 copy 建立独立字节,且保留非 nil 的空值。
            owned = make([]byte, len(raw))
            copy(owned, raw)
        }
        result = append(result, owned)
        // 下一次 Next 之前,借用字节已经复制完毕。
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("遍历结果集: %w", err)
    }
    return result, nil
}

函数每次为当前值建立独立存储,因此不会把一块临时工作区反复当作多行结果保存。不要改成在循环外维护一个 scratch 切片,复制后将同一个 scratch 追加多次;那只是把驱动内存共享改成了业务缓冲共享。

这个接口明确要求查询结果只有一列,因为 Scan 的目标个数必须匹配列数。多列业务应为每个需要保留的字段建立清楚的转换与复制策略,不必把所有类型先转成 RawBytes 再还原。

既然都要复制,能否直接 Scan 到 []byte

可以。对于需要保留整列字节的任务,直接把 []byte 变量地址传给 Scan,通常比“先借用再手工复制”更简单。官方文档明确区分:扫描到 *[]byte 会保存调用方拥有的副本,扫描到 *RawBytes 则避免这层复制,同时要求遵守其期限。

// 当前循环已通过 rows.Next,且查询只返回这一列。
var owned []byte
if err := rows.Scan(&owned); err != nil {
    return err
}
// 这个值由调用方持有,可保存到结果集合中。
saved = append(saved, owned)

选择 RawBytes 的理由应是当前行同步处理时确实可以不保留字节。例如读取后立即完成解析,并把解析得到的独立数值保存下来。如果最终总要留下全部原始内容,直接使用 []byte 能减少生命周期约定在业务代码中的传播。

使用方式数据归属适合任务
RawBytes 当前行同步消费借用立即解析且不保留原始字节
RawBytes 后 make/copy副本归调用方选择性保留某些值
Scan 到 []byte副本归调用方明确需要保存整列字节
RawBytes 直接放异步队列仍为借用不适合作为长期移交方式
借用数据复制为调用方副本后交给结果集合与异步任务的关系图
原创静态结构图:复制建立独立存储,结果集合和异步任务接收拥有的数据;独立存储不自动替代并发同步。

异步处理时,复制应该发生在哪里

应在扫描 goroutine 继续 Next 或关闭 Rows 之前完成复制,再把副本交给工作队列。把 RawBytes 发进 channel 后,让接收方稍后复制,并不能保证接收方复制时借用仍然有效。无缓冲 channel 的发送完成,也只代表接收已经发生,不代表接收方后续处理完毕。

有了独立副本以后,多个任务若还要同时修改它,仍需要自己的所有权或同步规则。复制解决的是数据生命周期,不是自动消除所有数据竞争。更清楚的移交方式是发送后由接收方独占,发送方不再修改该值。

内存方面,收集整个结果集会随着数据规模增长。可以按行复制后交给有界队列,限制在途数据,或者在当前行直接解析并丢弃原始字节。是否减少复制应结合用途决定,不宜为了“零拷贝”把借用限制传播到难以控制的下游。

关闭、NULL 和扫描失败还有哪些边界

不要在关闭 Rows 后继续访问旧 RawBytes,即便没有进入下一行;Close 同样结束其有效期。循环结束后也不能省略 Rows.Err,因为 Next 返回 false 既可能是正常读完,也可能是遍历失败。示例对失败不返回部分成功结果,调用者若需要部分结果,应明确设计另一种返回契约。

NULL 与空内容应依据业务区分。示例用 raw 是否为 nil 分支,复制时保留这一状态;如果转换成普通字符串并把它们都变成空字符串,就会丢掉区分信息。需要明确空值状态的字段,可以选择合适的可空类型或单独记录标记。

常见问题

把 RawBytes 转成普通 []byte 就能保存吗?仅做类型转换不会复制,需要 make/copy 等真实的数据复制动作。

换成另一个数据库驱动会解决吗?不能依赖某个驱动暂时未覆盖内存。应按 RawBytes 的公共有效期设计代码。

打印后再复制可以吗?只要仍在有效期内且没有异步保留旧引用,操作顺序可以按业务安排。关键是下一次 Next、Scan 或 Close 前完成需要的复制。

本文未连接数据库或运行示例,不提供具体驱动的复现输出。排查时沿着“谁拥有字节、何时继续遍历、何时移交数据”三个问题检查,就能找到多数 RawBytes 内容变化问题的根源。

参考:Go 官方 RawBytes 有效期;Rows.Scan 数据复制约定。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go httptrace 怎么单独统计 HTTP 请求的 DNS 解析耗时Go httptrace 怎么单独统计 HTTP 请求的 DNS 解析耗时
上一篇
Go httptrace 怎么单独统计 HTTP 请求的 DNS 解析耗时
Go net.ParseIP 解析 IPv4 映射 IPv6 后怎么统一比较
下一篇
Go net.ParseIP 解析 IPv4 映射 IPv6 后怎么统一比较
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    890次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    891次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    974次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    960次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    784次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码