返回资讯中心
外部精选
软件工程
#开发者实时流

Russ Cox 推出的极简文本归档格式 txtar 在 Go 工具链中被广泛使用

来源标题:A tour of txtar

Go 工具链中广泛使用的 txtar 是 Russ Cox 于 2018 年引入的极简文本归档格式,整个格式仅由文件标记和自由文本注释组成,包文档即规范。该格式仅支持纯文本,明确不支持二进制内容、文件模式、符号链接及标记转义,设计目标是保持可手工编辑并存储文本文件树。

Hacker News 最新jcbhmr2026/11/13 · 今天23 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

今天在翻cmd/go的testdata目录时偶然遇到txtar,好奇为什么每个测试文件里都像嵌了一小段diff。后来发现这是Russ Cox在2018年引入的一种极简归档格式,一旦注意到它,就发现Go工具链里到处都是。

最上面这几行就是注释。 -- hello.txt -- hello, world -- nested/foo.go -- package nested func Foo() string { return "foo" }

两个文件标记,两个文件,顶部一段自由文本注释。格式就这么简单。包的文档说得很直白:“txtar归档中不可能出现语法错误。”包的文档注释就是规范。规则如下:

格式仅限文本。包的文档明确排除了二进制内容、文件模式、符号链接,以及针对标记语法的任何转义机制。转义缺失的问题在于:当文件正文中恰好出现以--开头、以--结尾的行时,解析器会把它当作新的标记,而且无法引用。遇到这类情况,请改用tar或zip。

它之所以能保持如此精简,是因为它是围绕包文档中列出的三个目标专门设计的:保持可手工编辑、为go命令的测试用例存储文本文件树、在git历史和代码审查中干净地diff。它最早于2018年出现在早期的vgo modules原型中。

Go API位于golang.org/x/tools/txtar。两个类型和两个函数就能覆盖常见场景:

type Archive struct { Comment []byte Files []File type File struct { Name string Data []byte func Parse(data []byte) *Archive func ParseFile(file string) (*Archive, error)

Parse不返回错误。这个格式不可能解析失败。

package main import ( "fmt" "golang.org/x/tools/txtar" ) const archive = Lines up here are the comment. -- hello.txt -- hello, world -- nested/foo.go -- package nested func Foo() string { return "foo" } func main() { ar := txtar.Parse([]byte(archive)) fmt.Printf("comment: %q\n", ar.Comment) for _, f := range ar.Files { fmt.Printf("%s (%d bytes)\n", f.Name, len(f.Data)) comment: "Lines up here are the comment.\n\n" hello.txt (14 bytes) nested/foo.go (51 bytes)

Parse返回的切片是输入数据的别名。修改传入的字节会在下次读取时破坏归档。

ar, err := txtar.ParseFile("fixture.txt") if err != nil { log.Fatal(err)

同样是*Archive,只是数据来自os.ReadFile而非字符串字面量。

txtar.FS于2024年7月加入,它为你提供一个只读的fs.FS视图来访问归档,完全不需要接触磁盘:

fsys, err := txtar.FS(ar) if err != nil { log.Fatal(err) fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error { if err != nil { return err fmt.Printf("%s (dir=%v)\n", p, d.IsDir()) return nil }) . (dir=true) hello.txt (dir=false) nested (dir=true) nested/foo.go (dir=false)

任何接受fs.FS的代码都可以直接操作归档。解析器、模板引擎或静态站点生成器可以直接从内存中的归档读取测试数据。你不需要临时目录,也不需要解压步骤。

txtar.Format(*Archive) []byte是Parse的逆操作:

ar := &txtar.Archive{ Comment: []byte("generated\n"), Files: []txtar.File{ {Name: "main.go", Data: []byte("package main\n")}, {Name: "go.mod", Data: []byte("module example\n")}, os.Stdout.Write(txtar.Format(ar)) generated -- main.go -- package main -- go.mod -- module example

这个包没有提供将文件写入磁盘的辅助函数。标准做法是遍历ar.Files,验证每个路径都位于目标目录内,然后自行写入。Go的cmd/internal/script包有一个ExtractFiles方法正是做这件事的。golang.org/x/exp/cmd/txtar CLI是另一个选择,用txtar -x解压,用txtar <path>归档文件或目录。

假设你有一个函数Format(in []byte) []byte,用于美化某种你关心的文本格式。JSON、SQL、markdown,什么都行。你想喂给它一堆输入,又不想在testdata里散落一堆十行的文件。每个用例一个txtar归档就能搞定。顶部的注释说明测试内容,-- in --是输入,-- want --是期望输出。

the empty object collapses -- in -- -- want --

testdata/nested.txt: nested objects re-indent -- in -- {"a":{"b":1}} -- want -- "a": { "b": 1

测试用glob匹配目录,解析每个归档,然后比较Format(in)与want:

func TestFormat(t testing.T) { fixtures, _ := filepath.Glob("testdata/.txt") for _, path := range fixtures { t.Run(filepath.Base(path), func(t *testing.T) { ar, err := txtar.ParseFile(path) if err != nil { t.Fatal(err) files := map[string][]byte{} for _, f := range ar.Files { files[f.Name] = f.Data got := Format(files["in"]) if !bytes.Equal(got, files["want"]) { t.Errorf("got %q, want %q", got, files["want"]) })

新增一个用例就是在testdata/里加一个新文件。注释记录了意图,输入和期望输出并排放置。你省去了每个测试的初始化样板代码,也省去了那个总是会与测试脱节的独立golden/目录。

同样的模式也适用于多文件测试数据。在一个归档里加上-- go.mod --和三个-- *.go --文件,你就得到了一个自包含的迷你模块,可以喂给linter、重构工具或代码生成工具。这正是cmd/go的script测试和gopls的marker测试所做的,各自有数百个测试数据。

上一个例子把归档当作静态数据使用。cmd/go的script测试用法不同:注释是一串要执行的命令,文件条目是这些命令操作的工作区。rogpeppe/go-internal/testscript是同一个引擎,打包成了可以从任何测试中调用的库。

假设你想对tree做一个冒烟测试。你通过文件条目给它一个小项目,然后对tree打印出的结果做断言。一个归档同时完成两件事:

tree应该遍历它被解压到的工作区

exec tree --noreport --charset=utf-8 -I want cmp stdout want -- README.md --

project

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →