Godoc:为Go代码生成文档

本文翻译自《Godoc: documenting Go code》。

Andrew Gerrand

2011/03/31

[注,20226:有关Go代码文档的更新指南,请参阅“Go文档注释”]

Go项目非常重视文档。文档是使软件可访问和可维护的重要组成部分。当然,文档必须写得又好又准确,但也必须易于书写和维护。理想情况下,它应该与代码本身耦合,这样文档就可以随着代码一起进化。程序员越容易制作出好的文档,就越能方便每个人。

为此,我们开发了godoc文档工具。本文描述了使用godoc制作文档方法,并介绍了如何按照我们的约定为自己的项目编写好的文档。

Godoc解析Go源代码(包括注释),并以HTML或纯文本形式生成文档。最终的结果是文档与它所记录的代码紧密结合。例如,通过godoc的web界面,你可以一键从函数的文档导航到其实现

Godoc在概念上与Python的Docstring和Java的Javadoc相似,但其设计得更简单。godoc读取的注释不是语言构造(不与Docstring一样),也不必须有机器可读的语法(不与Javadoc一样)。Godoc需要的只是好的注释,即使Godoc不存在,你也会想读这种注释。

约定很简单:要为一个类型、变量、常量、函数,甚至是一个包生成文档,请在其声明之前直接写一个常规注释,中间不能有空行。Godoc随后会将该注释生成文档。例如,以下是fmt包的Fprint函数的文档:

// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {

请注意,这个注释是一个完整的句子,以它所描述的Go语言语法元素的名称开头。这一重要的约定使我们能够生成各种格式的文档,从纯文本到HTML再到UNIX手册页,并且当工具为了简洁而截断文档时,例如当它们提取第一行或第一个句子时,可以更好地阅读文档。

package的声明之上的注释会生成这个包的文档。这些注释可以很短,就像sort包的简短描述一样:

// Package sort provides primitives for sorting slices and user-defined
// collections.
package sort

注释也可以像gob包的概述一样详细。对于需要大量介绍性文档的包,该包使用了另一种约定:包注释放在自己的doc.go文件中,该文件只包含这些注释和一个package gob包声明语句。

在编写任何大小的包注释时,请记住它的第一句话将出现在godoc的包列表中。

与顶层声明语句不相邻的注释在godoc的输出中会被省略,除了一个明显的例外:以短语“BUG(who)”开头的顶层注释被识别为已知Bug,并在godoc的输出中包含在包文档的“Bugs”小节。“who”应该是可以提供更多信息的人的用户名。例如,这是字节包中的一个已知Bug:

// BUG(r): The rule Title uses for word boundaries does not handle Unicode punctuation properly.

有时,一个结构体的字段、一个函数、一个类型,甚至整个包都会变得多余或不再需要,但必须保留以与现有程序兼容。要发出不应使用某个标识符的信号,请在其文档注释中添加一段以“Deprecated:”开头的段落,后跟一些有关弃用的信息。

Godoc在将注释文本转换为HTML时使用了一些格式规则:

  • 随后的几行文本被视为同一段落的一部分;你必须使用一个空白行来分隔段落。
  • 预格式化的文本必须相对于周围的注释文本进行缩进(有关示例,请参阅gob的doc.go)。
  • URL将被转换为HTML链接;不需要特殊标记。

请注意,这些规则都不要求你做任何不同寻常的事情。

事实上,godoc最棒的地方在于它的易用性。因此,许多Go代码,包括所有的标准库,都已经遵循了这些约定。

你自己的代码只要有如上所述的注释就可以提供良好的文档。安装在$GOROOT/src/pkg中的任何Go软件包和任何GOPATH工作空间都可以通过godoc的命令行和HTTP接口进行访问,你可以通过-path标志或在源代码目录中运行“godoc.”来指定其他索引路径。有关更多详细信息,请参阅godoc文档

使用gob包来序列化与反序列化海量数据

本文翻译自《Gobs of data》。

Rob Pike

2011/03/24

介绍

要在网络上传输某种数据结构或将它存储在文件中,必须对它进行编码,然后解码。当然,有很多可用的编码格式:JSONXML、Google的protocol buffer等等。现在还有另一种,由Go的gob包提供。

为什么要定义一种新的编码格式?这需要大量的工作,而且多余。为什么不使用现有的格式之一?好吧,有一件事,我们做到了!Go本就支持刚才提到的所有编码的protocol buffer包在一个单独的存储库中,但它是下载频率最高的存储库之一)。对于许多目的,包括与用其他语言编写的工具和系统进行通信,它们都是正确的选择。

但对于Go特定的环境,例如用Go编写的两个服务器之间的通信,有机会构建一种更易于使用且可能更高效的编码格式。

Gob以一种外部定义的、独立于语言的编码所不能做到的方式工作。与此同时,还可以从现有的系统中吸取教训。

目标

gob包的设计考虑到了许多目标。

第一,也是最显然的一点,它必须非常易于使用。首先,因为Go支持反射机制,所以无需单独的接口定义或“协议编译器”。数据结构本身就是该包所需要的全部内容,以了解如何对其进行编码和解码。另一方面,这种方式意味着gob永远不会与其他编程语言通用,但没关系:gob就是以Go语言为中心的。

效率也很重要。以XML和JSON为例的文本形式的编码太慢了,无法在高效通信的网络中使用。二进制编码是必须的。

gob的数据流必须是自我描述的。从一开始读取的每个gob数据流都包含足够的信息,使得整个gob数据流可以由对其内容一无所知的代理程序解析。此特性意味着你将始终能够解码存储在文件中的gob数据流,即使在很久以后你已经忘记了它代表什么数据。

从我们使用Google的protocol buffer的经验中也学到了一些东西。

Protocol buffer的缺点

protocol buffer对gob的设计有很大影响,故意避免了它的三个特性。(protocol buffer不是自我描述的:如果你不知道用来编码成protocol buffer的数据的具体定义,你可能无法解码它。)

首先,protocol buffer只适用于我们在Go中称为结构体的数据类型。不能直接对整数或数组进行编码,只能对包含字段的结构体进行编码。这似乎是一个毫无意义的限制,至少在Go中是这样。如果你只想发送一个整数数组,为什么必须先将它放入一个结构体中?

其次,protocol buffer可以指定,每当对类型T的值进行编码或解码时,字段T.x和T.y都必需存在。尽管这样的必需字段(required field)看起来是个好主意,但它们的实现成本很高,因为编解码器在编码和解码时必须保留一个独立的数据结构,以便能够在所需字段丢失时进行报告。这也是一个维护问题。随着时间的推移,可能需要修改数据结构定义以删除必需字段,但这可能会导致现有客户端运行崩溃。最好不要在编码中包含必需字段。(protocol buffer也可以定义可选字段。如果我们没有定义必需字段,那么所有字段都是可选的。稍后将有更多关于可选字段的内容。)

第三个protocol buffer的错误特性是默认值。如果protocol buffer省略了“默认”字段的值,则解码该结构体的行为就好像这些字段默认被设置为该值一样。当你有getter和setter方法来控制对字段的访问时,这种想法非常有效,但当容器只是一个普通的结构体(译者注:没有getter和setter方法)时,就很难干净地处理了。必需字段的实现也很棘手:默认值在哪里定义,它们有什么类型(文本是UTF-8编码的吗?还是没有解释的字节码?一个浮点数用几个bit位来表示?)尽管看起来很简单,但它们在protocol buffer的设计和实现中有许多复杂之处。我们决定把这一特性排除在外,回到Go语言的琐碎但有效的默认规则:除非你另有设置,否则字段的值为其类型的“零值”,不需要传输。

因此,gob最终看起来像是一种通用的、简化的protocol buffer。那么它是如何工作的呢?

字段的值

编码的gob数据与int8或uint16之类的类型无关。相反,有点类似于Go中的常量,它的整数值是抽象的、无尺寸(sizeless)的数字,有符号或无符号。当你对int8字段进行编码时,它的值将作为一个无尺寸、可变长的整数来传输。当你对int64字段进行编码时,它的值也会作为一个无尺寸、可变长的整数进行传输。(有符号和无符号被区别对待,但无符号值也是无尺寸的。)如果两者的值都为7,则在网络上发送的位将相同。当接收者解码该值时,将其放入接收者的变量中,该变量可以是任意整数类型。因此,编码器发送来自int8字段的值7,接收者可以将其存储在int64字段中。这很好:这个值是一个整数,只要尺寸合适,一切都可行。(如果不合适,就会产生错误。)这种变量与其尺寸的解耦为编码器的实现提供了一些灵活性:随着软件的发展,我们可以扩展整数变量的类型,但仍然能够解码旧数据。

这种灵活性也用于指针。在传输之前,所有指针都被压平。int8、*int8、**int8、***int8等类型的值都作为整数值传输,然后可以将其存储在任何尺寸的int、*int或******int等中。

在解码结构体时,只有编码器发送的字段才会存储在目标字段中。给定值

type T struct{ X, Y, Z int } // 只有导出的(公有的)字段才会被编码然后解码。
var t = T{X: 7, Y: 0, Z: 8}

t的编码后的数据仅发送7和8。因为字段Y的值是零,所以它甚至不会被发送;没有必要发送零值。

相反,接收者可以将该值解码成这样的结构体:

type U struct{ X, Y *int8 } //注意:Y是*int8类型
var u U

并且仅设置X字段的值(int8类型的名为X的变量设置为7);Z字段被忽略了——没有哪个字段是Z?在解码结构体时,字段按名称和兼容的类型进行匹配,并且只有两者中都存在的字段才会受到影响。这种简单的方法巧妙地解决了“可选字段”问题:随着T类型通过添加字段而演变,过时的接收者仍可以使用它能够识别的类型的一部分来解码数据。因此,gob提供了可选字段的重要特性——可扩展性——而不需要任何额外的机制或注释。

使用整型数,我们可以构建所有其他类型:字节、字符串、数组、切片、映射,甚至浮点数。浮点数由IEEE 754标准的浮点位模式表示,存储为整数,只要你知道它的类型,它就可以正常工作。顺便说一句,该整数是以字节反转的顺序发送的,因为浮点数的常见值,如小整数,在低位端有很多零,我们可以避免发送这些零。

gob包的一个很好的特性是,它允许你通过让你的类型满足GobEncoderGobDecoder接口来定义自己的编码,行为类似于JSON包的MarshallerUnmarshaler函数,也类似于fmt包的Stringer接口。通过此功能,可以在传输数据时表示特殊特性、强制执行约束或隐藏秘密数据。有关详细信息,请参阅文档

在网络上传输类型

第一次发送某个指定的类型时,gob包会在数据流中包含该类型的描述信息。事实上,编码器用标准的gob编码格式对内部结构体进行编码,该内部结构体描述类型信息并为其提供一个唯一的编号。(基本类型,加上描述类型的结构体的布局,是由引导程序预定义的。)在描述了类型之后,可以通过其类型编号来引用它。

因此,当我们发送第一个类型T时,gob编码器发送T的描述信息,并用一个类型号码标记它,比如127。然后,所有值(包括第一个值)都以该数字为前缀,因此T值的数据流看起来如下:

("define type id" 127, definition of type T)(127, T value)(127, T value), ...

这些类型号码使描述递归类型和发送这些类型的值成为可能。因此,gob可以对树等类型进行编码:

type Node struct {
    Value       int
    Left, Right *Node
}

(这是给读者的一个练习,可以发现零默认规则(zero-defaulting rule)是如何实现这一点的,尽管gob并不呈现指针。)

有了类型信息,gob数据流是完全自我描述的,除了几个引导类型之外,引导类型是一个明确定义的起点。

构建一个小型解释器

第一次对给定类型的值进行编码时,gob包会构建一个特定于该数据类型的小型解释器。Go通过对该类型的反射来构建解释器,但是一旦构建了解释器,它就不依赖于反射。该解释器使用unsafe包和一些技巧将数据高速转换为编码字节。我们也可以使用反射并避免unsafe包,但速度会慢得多。(Go的protocol buffer实现也采用了类似的高速方法,其设计受到了gob实现的影响。)相同类型的后续值使用已经编译得到的解释器,因此可以立即对它们进行编码。

[更新:从Go 1.4开始,unsafe包不再被gob包使用,性能略有下降。]

解码的过程是相似的,但更难实现。当你解码一个值时,gob包会保存一个字节切片,代表给定编码器要解码的类型的值,再加上一个要解码的Go值。gob包也会构建一个解码器:gob类型与对应的解码器代码一起在网络上发送。然而,一旦解码器构建完成,由于它是一个不使用反射机制的引擎,使用不安全的方法来获得最大速度。

用法

虽然幕后做了很多事情,但gob是一个高效、易于使用的数据传输编码系统。下面是一个完整的示例,显示了对不同类型地编码和解码。注意发送和接收值是多么的容易;你所需要做的就是向gob包呈现值和变量,它就可以完成所有的工作。

package main

import (
    "bytes"
    "encoding/gob"
    "fmt"
    "log"
)

type P struct {
    X, Y, Z int
    Name    string
}

type Q struct {
    X, Y *int32
    Name string
}

func main() {
    // 初始化编码器和解码器。一般来说,enc和dec会被绑定到网络连接,并且可能运行在不同的协程里。
    var network bytes.Buffer        // 代表一个网络连接
    enc := gob.NewEncoder(&network) // 将写入网络
    dec := gob.NewDecoder(&network) // 将从网络读取
    // 编码(发送)值
    err := enc.Encode(P{3, 4, 5, "Pythagoras"})
    if err != nil {
        log.Fatal("encode error:", err)
    }
    // 解码(接收)值
    var q Q
    err = dec.Decode(&q)
    if err != nil {
        log.Fatal("decode error:", err)
    }
    fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
}

rpc包构建在gob之上,将这种编码/解码自动化转变为跨网络的方法调用以及数据传输。这将是另一篇文章的主题。

细节

gob包的文档,尤其是文件doc.go,扩展了这里描述的许多细节,并包括一个完整的示例,显示了编码如何表示数据。如果你对gob实现的内部结构感兴趣,那么这是一个很好的起点。

Cgo介绍

本文翻译自《C? Go? Cgo!》。

Andrew Gerrand

2011/03/17

介绍

Cgo可以让Go包调用C代码。给定一个使用一些特殊特性编写的Go源文件,cgo输出Go和C文件,它们可以组合成单个Go包。

举个例子,这里有一个Go包,它提供了两个函数——RandomSeed——用来包装C的random函数和srrandom函数。

package rand

/*
#include <stdlib.h>
*/
import "C"

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}

让我们看看这里发生了什么,从import语句开始。

rand包导入“C”,但你会发现在标准Go库中没有这个包。这是因为C是一个“伪包(pseudo-package)”,一个被cgo解释为引用C命名空间的特殊名称。

rand包包含对C包的四个引用:对C.randomC.srrandom的调用、C.uint(i)import "C"语句。

Random函数调用标准C库的random函数并返回结果。在C语言中,random返回一个long类型的值,cgo将其表示为C.long类型。必须将其转换为Go类型,才能由该包外的Go代码使用,使用普通的Go类型转换语句即可:

func Random() int {
    return int(C.random())
}

下面是一个等效函数,它使用一个临时变量来更明确地说明这种类型转换:

func Random() int {
    var r C.long = C.random()
    return int(r)
}

Seed函数在某种程度上起相反的作用。它获取一个常规的Go的int类型的变量,将其转换为C的unsigned int类型,并将其传递给C函数srrandom

func Seed(i int) {
    C.srandom(C.uint(i))
}

注意,cgo知道C语言的unsigned int类型用C.uint来表示;有关这些数值类型的名称的完整列表,请参阅cgo文档

我们还没有研究这个例子的一个细节是import语句上方的注释。

/*
#include <stdlib.h>
*/
import "C"

Cgo认识这一注释。任何以#cgo开头、后跟一个空格字符的行都将被删除;这些都成为cgo的指令。在编译Go包的C语言部分代码时,剩余的行用作C语言头部。在上例中,这些行只是一个#include语句,但它们几乎可以是任何C代码。在构建Go包的C语言部分时,#cgo指令用于为编译器和链接器提供标志。

有一个限制:如果你的程序使用任何//export指令,那么注释中的C代码可能只包括这种声明(extern int f();),而不是定义(int f() { return 1; })。你可以使用//export指令使Go函数可被C语言代码访问。

#cgo//export指令记录在cgo文档中。

字符串相关

与Go不同,C没有显式的字符串类型。C中的字符串由一个以零结尾的字符数组表示。

Go和C字符串之间的转换是通过C.CStringC.GoStringC.GoStringN函数完成的。这些转换返回字符串数据的一个拷贝。

下一个示例实现了一个Print函数,该函数使用C的stdio库中的fputs函数将字符串写到标准输出:

package print

// #include <stdio.h>
// #include <stdlib.h>
import "C"
import "unsafe"

func Print(s string) {
    cs := C.CString(s)
    C.fputs(cs, (*C.FILE)(C.stdout))
    C.free(unsafe.Pointer(cs))
}

Go的内存管理器看不见由C代码进行的内存分配。因此当你使用C.CString(或任何C内存分配)创建一个C字符串时,你必须记住在完成调用后使用C.free释放内存。

C.CString的调用返回一个指向char数组头部的指针,我们将其转换为一个unsafe.Pointer指针。并用C.free释放它指向的内存分配。cgo程序中的一个常见惯用法是在分配内存后立即defer释放(尤其是当后面的代码比单个函数调用更复杂时),例如以下对Print函数的重写:

func Print(s string) {
    cs := C.CString(s)
    defer C.free(unsafe.Pointer(cs))
    C.fputs(cs, (*C.FILE)(C.stdout))
}

构建cgo包

要构建cgo包,只需像往常一样使用go buildgo install即可。这些go工具能识别特殊的“C”导入,并自动使用cgo来处理。

cgo相关资源

cgo命令文档提供了关于C伪包和构建过程的更多详细信息。Go树中的cgo示例演示了更高级的概念。

最后,如果你想知道在内部这一切是如何工作的,请查看运行时包的cgocall.go的介绍性注释。

Gofix介绍

本文翻译自《Introducing Gofix》。

Russ Cox

2011/04/15

下一个Go版本将在几个基本Go包中进行API的重大更改。实现HTTP服务器处理程序调用net.Dial调用os.Open使用反射包的代码将不会生成成功,除非将其更新为使用新的API。现在我们的版本更加稳定,频率也降低了,这将是一种常见情况。每一个API更改都发生在不同的每周快照中,并且可能由它们的开发者自己单独管理;然而,这些更改加在一起,更新现有代码时就需要大量的手动工作。

Gofix是一种新工具,它减少了更新现有代码所需的工作量。它从源代码文件中读取程序代码,查找使用旧API的地方,将其重写为使用当前API,然后将程序代码写回源代码文件。并非所有API更改都保留了旧API的所有功能,因此gofix无法始终做到完美。当gofix无法重写使用旧API的代码时,它会打印一条警告,给出源代码文件名和行号,以便开发人员检查和重写相关代码。Gofix负责处理简单、重复、乏味的更改,这样开发人员就可以专注于真正值得关注的更改。

每次我们对API进行重大更改时,我们都会在gofix中添加代码,以尽可能机械地处理代码转换。当你更新到新的Go版本并且你的代码不再能构建成功时,只需在源代码目录上运行gofix即可。

你可以扩展gofix以支持对自己的API进行更改。gofix是一个简单的由插件驱动的程序,插件系统叫作fixes,其中每个插件(一个fix)处理一个特定的API更改。现在,编写一个新的修复程序需要对go/ast语法树进行一些扫描和重写,通常与API更改的复杂程度成比例。如果你想探索一下,netdialFixosopenFixhttpserverFixreflectFix都是说明性的例子,它们的复杂度依次增高。

当然,我们自己也编写Go代码,我们的代码和你的代码一样受到这些API更改的影响。通常,我们在更改API的同时编写gofix支持,然后使用gofix重写主(main)源代码树分支中的用法。我们使用gofix来更新其他Go代码库和我们的个人项目。当需要针对新的Go版本进行构建时,我们甚至会使用gofix来更新谷歌内部的源代码树。

例如,gofix可以重写fmt/print.go中的代码片段:

switch f := value.(type) {
case *reflect.BoolValue:
    p.fmtBool(f.Get(), verb, field)
case *reflect.IntValue:
    p.fmtInt64(f.Get(), verb, field)
// ...
case reflect.ArrayOrSliceValue:
    // Byte slices are special.
    if f.Type().(reflect.ArrayOrSliceType).Elem().Kind() == reflect.Uint8 {
        // ...
    }
// ...
}

为使用新的反射API:

switch f := value; f.Kind() {
case reflect.Bool:
    p.fmtBool(f.Bool(), verb, field)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
    p.fmtInt64(f.Int(), verb, field)
// ...
case reflect.Array, reflect.Slice:
    // Byte slices are special.
    if f.Type().Elem().Kind() == reflect.Uint8 {
        // ...
    }
// ...
}

上面几乎每一行都有细微的变化。要重写的地方虽然有很多,但几乎完全是机械的,这正是计算机擅长做的事情。

Gofix之所以成为可能,是因为Go的标准库支持将Go源文件解析为语法树,也支持将这些语法树打印回Go源代码。重要的是,Go打印库(printer)以官方推荐的格式打印程序源码(通常通过gofmt工具强制执行),允许gofix对Go程序进行机械地更改,而不会导错误的格式更改。事实上,创建gofmt的一个关键动机可能仅次于避免关于括号应该放在何处的争论,那就是让重写Go程序源码的工具变得简单,无论是创建还是使用,例如gofix。

Gofix已经变得不可或缺。特别是,如果没有自动转换,最近对反射API的更改将是不受欢迎的,并且难以修改之前程序源码里的反射API。Gofix使我们能够修复错误或完全重新思考API,而无需担心转换现有代码所需的成本。我们希望你能发现gofix这个工具是有用和方便的。

使用Gofmt工具格式化Go代码

本文翻译自《go fmt your code》。

Andrew Gerrand 2013/01/23

介绍

Gofmt是一个用来自动格式化Go源代码的工具。

Gofmt的代码:

  • 更易于编写:在编写时永远不要担心小的格式问题,
  • 更容易阅读:当所有代码看起来都一样时,你不需要在心里把别人的格式风格转换成你能理解的东西。
  • 更易于维护:对源码的细节更改不会导致对文件格式的不相关更改;diffs(差异)只显示真正的变化。
  • 无争议:永远不会再争论空格或括号位置这些问题了!

格式化你的代码

我们最近对野外(非官方)的Go软件包进行了一项调查,发现大约70%的软件包是根据gofmt规则格式化的。这比预期的要多,感谢所有使用gofmt的人,但如果能继续缩小差距就太好了。

要格式化Go代码,可以直接使用gofmt工具:

gofmt -w yourcode.go

或者你也可以使用“go fmt”命令:

go fmt path/to/your/package

为了帮助你保持代码的风格规范,Go存储库包含编辑器和版本控制系统的钩子,使你可以轻松地在代码上运行gofmt。

对于Vim用户,Go的Vim插件包含了在当前缓冲区上运行gofmt的Fmt命令。

对于emacs用户,go-mode.el提供了一个保存前执行gofmt的钩子,可以通过将此行添加到.emacs文件来安装:

(add-hook 'before-save-hook #'gofmt-before-save)

对于Eclipse或SublimeText用户,GoClipseGoSublime项目为这些编辑器添加了一个gofmt功能。

对于Git爱好者来说,misc/Git/precommit脚本是一个commit之前的钩子,可以防止提交格式错误的Go代码。如果你使用Mercurial,hgstyle插件提供了一个gofmt预提交钩子。

机器修改源代码

使用机器格式化代码最大的优点之一是可以机械地转换代码格式,而不会产生与格式化无关的东西。当你使用的代码库很庞大时,机械转换是无价的,因为它比手工进行大范围的更改更加全面,也更不容易出错。事实上,当工作规模很大时(就像我们在谷歌所做的那样),手动进行代码更改通常是不现实的。

使用机器操作Go代码的最简单方法是使用gofmt命令的-r标志。标志指定代码格式的重写规则:

pattern -> replacement

其中模式(pattern)和替换(replacement)都是有效的Go表达式。在该模式中,单字符小写标识符用作匹配任意子表达式的通配符,并且这些表达式在replacement中被替换为相同的标识符。

例如,最近对Go核心的更改重写了对bytes.Compare方法的一些使用,以改为使用更高效bytes.Equal方法。仅仅使用两个gofmt命令就修改成功:

gofmt -r 'bytes.Compare(a, b) == 0 -> bytes.Equal(a, b)'
gofmt -r 'bytes.Compare(a, b) != 0 -> !bytes.Equal(a, b)'

Gofmt还使用了gofix,它可以进行任意复杂的源代码转换。在早期,当我们经常对语言和库进行破坏性的更改时,Gofix是一个非常宝贵的工具。例如,在Go 1之前,内置的error接口还不存在,约定是使用os.Error类型。当我们引入error接口时,我们提供了一个gofix模块,它重写了对os.Error及其相关助手函数的所有引用,以使用error接口和新的errors。尝试手工更改代码可能会让人望而却步,但有了标准格式的代码,准备、执行和审查这一变化就相对容易,这几乎涉及到了现有的所有Go代码。

有关gofix的更多信息,请参阅本文