当前位置:首页 > 文章列表 > Golang > Go问答 > Go binary.Uvarint 返回 n=0 时怎样处理截断输入

Go binary.Uvarint 返回 n=0 时怎样处理截断输入

来源:17golang原创 2026-10-13 12:30:18 0浏览 收藏

我在处理一段按帧传输的二进制数据时,第一次看到 binary.Uvarint 返回 n=0,很容易把它和“解出了数字 0”混在一起。实际含义完全不同:n=0 表示当前 []byte 太短,暂时还不能判断一个完整的无符号变长整数;只有 n>0 才表示成功,n 则表示溢出。

官方地址:https://pkg.go.dev/encoding/binary#Uvarint

所以,遇到 n=0 时不要推进游标、不要把返回的 x=0 当成业务值,也不要马上把输入判成坏包。应该保留这段未完成的字节,等下一段数据到达后再重试。

先看懂 Uvarint 的三个 n

Uvarint 返回 (uint64, int)。第二个返回值比第一个更重要:它既告诉你是否成功,也告诉你输入消耗和错误类型。

n 的结果含义处理动作
大于 0成功解码,n 是消费的字节数读取 x,游标向后移动 n
等于 0缓冲区太短,当前输入可能只是半个值保留原字节,等待更多输入
小于 0数值超过 uint64,-n 是已读字节数按协议记录溢出并拒绝当前帧
Uvarint 返回 n 大于零、等于零和小于零的三种状态说明图
图1:Uvarint 返回值与输入状态的对应关系说明图,不是运行截图。

这里最容易踩的坑是:x=0 不能单独说明成功。合法的整数 0 确实可能编码成一个字节,但只要 n=0,这个 x 就没有可用意义。

把 n=0 和合法的零值分开

先用一个小例子观察返回值。示例中的注释只说明判断重点,不把日志输出伪装成代码。

package main

import (
    "encoding/binary"
    "fmt"
)

func main() {
    cases := [][]byte{
        {0x00},       // 合法的数字 0,完整编码只有一个字节
        {0x80},       // 延续位仍为 1,当前缓冲区还不完整
        {0x80, 0x01}, // 完整编码,结果为 128
    }

    for _, buf := range cases {
        value, n := binary.Uvarint(buf)
        switch {
        case n > 0:
            // 只有 n 大于 0,value 才是本次解码得到的有效数字。
            fmt.Printf("value=%d consumed=%d\\n", value, n)
        case n == 0:
            // n 等于 0 表示输入不足,不能把 value=0 当成结果。
            fmt.Println("need more bytes")
        default:
            // n 小于 0 表示 uint64 溢出,应拒绝这段编码。
            fmt.Printf("overflow, bytes-read=%d\\n", -n)
        }
    }
}

判断顺序建议固定为 n>0、n==0、最后处理 n。这样即便业务允许数字 0,也不会把“输入不足”误记为成功。

分片输入时怎样保留尾部

在网络读取或分块文件读取中,一次 Read 不保证刚好切在变长整数边界上。当前缓冲区如果以带延续位的字节结尾,Uvarint 返回 0,这时不能丢掉尾部。

我更倾向于把解析函数写成“返回消费数”的小组件:成功时删除已经消费的前缀,不足时原样保留缓冲区,溢出时返回明确错误。下面的代码演示这个边界。

package main

import (
    "encoding/binary"
    "errors"
    "fmt"
)

var errVarintOverflow = errors.New("uvarint overflow")

// takeUvarint 只消费完整值;输入不足时保留全部字节等待下一片。
func takeUvarint(buf []byte) (value uint64, rest []byte, complete bool, err error) {
    value, n := binary.Uvarint(buf)
    switch {
    case n > 0:
        // n 是实际消费的字节数,后续内容交给下一轮解析。
        return value, buf[n:], true, nil
    case n == 0:
        // 当前片段不够完整,不能移动切片起点或丢弃尾部。
        return 0, buf, false, nil
    default:
        // 负数表示溢出;-n 仅用于诊断,不应继续消费成合法值。
        return 0, buf, false, errVarintOverflow
    }
}

func main() {
    first := []byte{0x80} // 第一片只有半个编码
    value, rest, complete, err := takeUvarint(first)
    fmt.Println(value, len(rest), complete, err)

    // 第二片到达后,把保留的尾部和新字节拼起来再解析。
    joined := append(append([]byte(nil), rest...), 0x01)
    value, rest, complete, err = takeUvarint(joined)
    fmt.Println(value, len(rest), complete, err)
}
分片输入中保留尾部并在下一片到达后重试 Uvarint 的流程图
图2:分片输入等待补齐、重试并推进游标的结构说明图,不是运行截图。

这段策略的关键不是“多读几次”,而是保证 n=0 时输入仍然可重放。若上层缓冲区有容量上限,还要给“长期不完整”的帧设定最大长度或超时,避免恶意或损坏输入无限占用内存。

成功后怎样推进游标

一个缓冲区里可能连续放着多个 Uvarint。成功解出第一个值后,应只移动 n 个字节;不能按固定的 1 字节或 10 字节跳过。

func decodeAll(buf []byte) ([]uint64, error) {
    var values []uint64
    for len(buf) > 0 {
        value, n := binary.Uvarint(buf)
        switch {
        case n > 0:
            // 每个值的长度不同,必须按照 n 精确切掉已消费前缀。
            values = append(values, value)
            buf = buf[n:]
        case n == 0:
            // 尾部是半个值时,把它交给下一次读取,而不是静默丢弃。
            return values, nil
        default:
            // 溢出不是“还需要更多字节”,继续读取只会掩盖坏输入。
            return nil, errVarintOverflow
        }
    }
    return values, nil
}

如果协议要求一帧必须完整,可以在函数返回时额外记录“仍有未完成尾部”;如果协议允许跨帧拼接,则把这个尾部交回连接级缓冲区。两种选择都比把 n=0 当作 0 更安全。

什么时候改用 ReadUvarint

Uvarint 面向已经拿到的字节切片,适合自己管理缓冲、游标和帧边界。若手里是实现了 io.ByteReader 的读取器,可以考虑 binary.ReadUvarint,它直接从读取器逐字节读取,并用错误值区分结束和不完整输入。

官方文档规定:ReadUvarint 在没有读到任何字节时返回 io.EOF;已经读到一部分但没有完成时返回 io.ErrUnexpectedEOF。因此,选择标准很简单:

  • 已经有一块内存并需要精确控制尾部:使用 Uvarint。
  • 已有字节读取器且希望用 error 表达流结束:使用 ReadUvarint。
  • 需要批量解析多个字段:无论用哪种接口,都要明确每个字段的消费边界。

发布前我会检查这五件事

  1. 是否只用 n>0 判定成功,而没有只看 value?
  2. n=0 时是否保留了原始尾部,下一片到达后能重试?
  3. 成功后是否按 n 推进游标,并允许一个缓冲区连续出现多个值?
  4. n 是否作为溢出处理,而不是等待更多字节?
  5. 是否测试了合法 0、单字节边界、跨片段输入、空输入和溢出输入?

记住一句话就够了:Uvarint 的 n=0 描述的是“输入还不够”,不是“结果等于 0”。只要把返回值当成状态机来处理,分片读取和连续字段解析都会清晰很多。

相关问题

n

-n 表示已经读到的字节数,可用于日志和定位坏帧,但它不代表这次解码成功,不能把对应的值继续交给业务层。

空切片和截断输入一样吗?

对 Uvarint 而言,两者都会得到 n=0,都应等待输入或交给上层判断是否已经到达流结束。

能不能把不完整字节直接补一个 0?

不能。补字节会改变编码含义;应保留真实输入,等待协议规定的下一段数据。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go json.RawMessage 如何区分字段缺失和显式 nullGo json.RawMessage 如何区分字段缺失和显式 null
上一篇
Go json.RawMessage 如何区分字段缺失和显式 null
Go base64.Encoding.Strict 遇到非零填充位时的解码排查
下一篇
Go base64.Encoding.Strict 遇到非零填充位时的解码排查
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    457次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    521次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    536次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    484次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    306次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码