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 文本和还原接口的静态结构关系说明图](/uploads/20261030/1793368144-c09ac72b34-0e9066128b-go-diff-delta-structure.webp)
用 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 也只说“向前跳过两个字符”。这就是它比完整目标文本更紧凑的原因,也是它必须绑定原文基准的原因。

实际存储时可以选择下面两种模型:
- 快照模型:保存原文全文和 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 计数和错误处理,并为每个兼容实现准备互操作样例。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
364 收藏
-
263 收藏
-
397 收藏
-
108 收藏
-
349 收藏
-
350 收藏
-
297 收藏
-
219 收藏
-
346 收藏
-
451 收藏
-
248 收藏
-
234 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习