登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go go-diff 怎样把差异保存为紧凑 delta 文本

来源:17golang原创

时间:2026-10-30 21:49:08 304浏览 收藏

如果你已经用 go-diff 得到了 []Diff,但不想把整组结构体直接写进缓存或消息体,可以调用 DiffToDelta 把差异编码成一串紧凑文本。真正可恢复的组合是:保存原文基准,再保存 delta;读取时用 DiffFromDelta 还原差异,最后用 DiffText2 得到目标文本。

官方地址:https://pkg.go.dev/github.com/sergi/go-diff/diffmatchpatch

要点速览
  • []Diff 是内存中的差异对象,delta 是便于存储或传输的编码文本,二者不是目标文本本身。
  • = 表示保留若干字符,- 表示删除若干字符,+ 后面携带转义后的插入文本;长度按 Unicode 字符计数。
  • DiffFromDelta 必须拿到与生成时一致的原文,delta 单独保存不能直接还原最终内容。

先把三种数据分开:原文、[]Diff 和 delta

这个问题最容易混淆的是“差异结果”和“差异文本”。DiffMain 接收两个字符串,返回由 Diff 组成的切片;每个元素只有操作类型和文本。DiffToDelta 再把这些操作压缩成以制表符分隔的 token。它记录的是“如何相对原文移动和修改”,不是可以脱离原文独立阅读的完整文档。

因此,落库记录至少要有三个字段:原文版本或原文内容、delta 文本、生成时使用的算法或格式版本。原文很大时不一定要重复保存全文,可以存一个稳定的基准版本号,读取时从版本库取回对应原文;但不能只保存 delta 后期待它像快照一样工作。

Go go-diff 中原文、目标文本、DiffMain、[]Diff、DiffToDelta、delta 文本和还原接口的静态结构关系说明图
图1:结构说明图,展示 go-diff 的差异对象、紧凑文本和还原接口之间的静态关系,不是运行截图。

用 DiffToDelta 生成紧凑差异串

最小写法是先创建 DiffMatchPatch,再计算差异并调用 DiffToDelta。checklines 适合较长文本的快速初步处理;文章关注的是保存格式,所以示例把它显式写成 false,让差异直接按字符级别计算。

package main

import (
    "fmt"

    "github.com/sergi/go-diff/diffmatchpatch"
)

func encodeDelta(original, updated string) (string, error) {
    // New 返回带默认参数的差异计算器。
    dmp := diffmatchpatch.New()
    // 用原文和新文档生成内存中的差异片段。
    diffs := dmp.DiffMain(original, updated, false)
    if len(diffs) == 0 {
        // 两个字符串相同,空 delta 代表没有任何编辑。
        return "", nil
    }
    // DiffToDelta 会把保留、删除和插入编码成 tab 分隔 token。
    return dmp.DiffToDelta(diffs), nil
}

func main() {
    delta, err := encodeDelta("Go 读配置文件", "Go 读取配置文件")
    if err != nil {
        // 这里保留错误出口,便于把编码失败交给上层记录。
        panic(err)
    }
    fmt.Println(delta)
}

返回字符串通常由三类 token 拼成,并以制表符分隔:

形式含义保存内容
=n保留原文中的 n 个 Unicode 字符只保存长度
-n从原文中跳过并删除 n 个 Unicode 字符只保存长度
+text向结果中插入文本插入内容经过 URL 查询字符串规则转义

这里的“字符”不是字节数。实现对 = 和 - 使用 UTF-8 rune 数量统计,因此中文、表情符号等多字节字符不会被当成多个编辑位置。插入文本则会经过 QueryEscape 风格编码;空格、百分号和制表符等字符不能按原样参与 token 分隔,读取时再由 DiffFromDelta 解码。

为什么 delta 不能脱离原文单独还原

delta 中的 =4 只说“保留原文当前位置开始的四个字符”,并没有把这四个字符复制进 delta。-2 也只说“向前跳过两个字符”。这就是它比完整目标文本更紧凑的原因,也是它必须绑定原文基准的原因。

Go go-diff delta 中保留长度、删除长度、插入文本、字符计数、转义和原文基准的关系说明图
图2:数据结构说明图,展示 delta token 的字符计数、转义和原文基准关系,不是运行截图。

实际存储时可以选择下面两种模型:

  • 快照模型:保存原文全文和 delta,读取简单,适合小文档或需要独立归档的记录。
  • 版本模型:保存 base_version、delta 和格式版本,读取时按版本号取原文,适合已有文档版本库的服务。

无论采用哪种模型,都应把原文版本作为不可变基准。原文被覆盖后再拿旧 delta 解码,token 的位置仍然合法也不代表结果正确:它可能只是基于另一份文本得到了一个看似正常的字符串。

用 DiffFromDelta 还原目标文本

恢复过程分两步:先把 delta 和原文还原成 []Diff,再用 DiffText2 拼出包含相等片段和插入片段的目标文本。不要把 DiffText1 当成目标结果;它保留的是相等和删除部分,对应原文一侧。

func applyDelta(original, delta string) (string, error) {
    // 还原时必须使用生成 delta 时的同一份原文基准。
    dmp := diffmatchpatch.New()
    diffs, err := dmp.DiffFromDelta(original, delta)
    if err != nil {
        // token 非法、长度越界或转义内容错误时返回原始错误。
        return "", fmt.Errorf("decode delta: %w", err)
    }
    // DiffText2 组合相等和插入片段,得到更新后的文本。
    return dmp.DiffText2(diffs), nil
}

DiffFromDelta 按制表符拆分 token。实现允许末尾多出一个空 token,所以由 DiffToDelta 产生的末尾制表符可以直接保存;但不要在业务层随意裁剪或替换插入文本中的转义内容,否则解码结果会改变。若服务从外部接收 delta,应把错误当成输入格式错误处理,不要静默使用不完整结果。

怎样用指标判断它是否真的更紧凑

“delta 更小”只在重复内容足够多时成立。可以在写入前计算三个简单指标:原文字节数、目标文本字节数、delta 字节数。对短文本或改动几乎覆盖全文的文本,+ 插入内容的转义开销可能让 delta 不比完整内容小;对大文档中的少量编辑,=n 和 -n 只保存数字,通常更有价值。

type StoredChange struct {
    // BaseVersion 用于在读取时找回不可变的原文基准。
    BaseVersion string
    // Delta 保存 DiffToDelta 的结果,不把它误当成完整文档。
    Delta string
    // OriginalBytes 和 DeltaBytes 便于按真实 UTF-8 传输成本做选择。
    OriginalBytes int
    DeltaBytes    int
}

func chooseRepresentation(original, updated string) StoredChange {
    // 先计算差异,再比较字节数;不要用 rune 数量估算网络或磁盘成本。
    dmp := diffmatchpatch.New()
    diffs := dmp.DiffMain(original, updated, false)
    delta := dmp.DiffToDelta(diffs)
    return StoredChange{
        BaseVersion:   "doc-v42",
        Delta:         delta,
        OriginalBytes: len(updated),
        DeltaBytes:    len(delta),
    }
}

这个比较只回答“当前编码的字节数是否更小”,不等于完整的性能结论。生产环境还要把编码 CPU、解码 CPU、基准版本读取、压缩后大小和缓存命中一起纳入采样。特别是频繁编辑同一文档的场景,连续 delta 会形成链条;链条越长,读取时需要回放的次数越多,可以定期生成新快照,避免把恢复延迟全部转移到读取端。

上线前的四项边界检查

  • 基准一致:delta 的 base_version 必须能定位到生成时的原文,不使用“当前最新版本”代替。
  • 字符口径一致:长度 token 按 rune 计数,容量和传输成本则按字节数评估,二者不要混为一个指标。
  • 格式完整:保留制表符分隔、插入文本转义和格式版本,不能用通用的空白压缩函数处理 delta。
  • 失败可见:解码错误、基准缺失和版本不匹配要显式返回;不要发布一份未经确认的半成品文本。

常见问题

只保存 delta,之后能不能直接得到完整文本?

不能。=n 和 -n 都是相对原文位置的操作,至少还要保存或取回对应的原文基准。

为什么中文 delta 的长度不能按 len(string) 解释?

Go 的 len(string) 返回字节数,而 go-diff 生成 =、- token 时按 Unicode rune 数量计数。容量评估可以看字节数,编辑位置则要按字符语义理解。

DiffText1 和 DiffText2 有什么区别?

DiffText1 拼出原文一侧的相等与删除内容,DiffText2 拼出目标一侧的相等与插入内容。应用 delta 后要取目标文本,应使用 DiffText2。

delta 能不能当作通用 patch 文件给其他语言使用?

不要默认可以。它是该 Go 实现提供的紧凑差异编码,跨语言前要先固定 token 规则、转义方式、Unicode 计数和错误处理,并为每个兼容实现准备互操作样例。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>