本文翻译自《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更改的复杂程度成比例。如果你想探索一下,netdialFix、osopenFix、httpserverFix和reflectFix都是说明性的例子,它们的复杂度依次增高。
当然,我们自己也编写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这个工具是有用和方便的。