保持 Modules 的兼容性(下)

简介: 保持 Modules 的兼容性(下)

03

使用接口


有时,新功能需要更改公开接口:例如,需要使用新方法扩展接口。直接添加到接口是一个突破性的变化,但是,我们如何支持在公开的接口上采用新方法呢?


基本思想是使用新方法定义新接口,然后无论使用旧接口,动态检查所提供的类型是较旧类型还是较新的类型。


让我们用存档 archive/tar 包中的示例来说明这一点。tar.NewReader 接受 io.Reader,但随着时间的推移,Go 团队意识到,如果可以调用 Seek,从一个文件头跳到下一个文件头会更有效率。但是,他们不能向 io 添加 Seek 方法。读者:这将打破所有实现者 io.Reader


另一个排除的选择是改变 tar.NewReader 接受 io.ReadSeeker 而不是 

io.Reader,因为它支持 io.Reader 和 Seek (by way of io.Seeker)。但是,正如我们上面看到的,更改函数签名也是一个突破性的变化。


所以,他们决定保持 tar.NewReader 的签名不变,但类型检查(和支持) io.Seeker 在 tar.Reader 方法中。


package tar
type Reader struct {
  r io.Reader
}
func NewReader(r io.Reader) *Reader {
  return &Reader{r: r}
}
func (r *Reader) Read(b []byte) (int, error) {
  if rs, ok := r.r.(io.Seeker); ok {
    // Use more efficient rs.Seek.
  }
  // Use less efficient r.r.Read.
}

(有关实际代码,请参阅 reader. go。)


当您遇到要将方法添加到现有接口的情况下,您可能能够遵循此策略。首先使用新方法创建新接口,或使用新方法标识现有接口。接下来,确定需要支持它的相关函数,键入第二个接口的检查,并添加使用它的代码。


此策略仅在没有新方法的旧接口仍受支持时才有效,从而限制了模块的未来可扩展性。


在可能的情况下,最好完全避免此类问题。例如,在设计构造函数时,更喜欢返回具体类型。使用具体类型允许您在将来添加方法,而不会破坏用户,这与接口不同。该属性允许您的模块在将来更容易扩展。


提示:如果您确实需要使用接口,但不打算让用户实现它,可以添加不可导出的方法。这样可以防止包外部定义的类型在不嵌入的情况下满足接口,从而释放您以后添加方法的自由,而不会破坏用户实现。例如,请参阅

testing.TB 的 private() 函数。


type TB interface {
    Error(args ...interface{})
    Errorf(format string, args ...interface{})
    // ...
    // A private method to prevent users implementing the
    // interface and so future additions to it will not
    // violate Go 1 compatibility.
    private()
}

04

添加配置方法


到目前为止,我们已经讨论过公开中断的更改,其中更改类型或函数将导致用户的代码停止编译。但是,行为更改也会破坏用户,即使用户代码继续编译。例如,许多用户期望 json.Decoder 忽略 JSON 中不在参数结构中的字段。当 Go 团队想要在这种情况下返回错误时,他们必须小心。在没有选择加入机制的情况下这样做意味着许多依赖这些方法的用户可能会开始接收以前没有的错误。


因此,他们不是更改所有用户的行为,而是向解码器结构添加了配置方法:Decoder.DisallowUnknownFields。调用此方法会选择用户加入新行为,但这样做会保留现有用户的旧行为。


05

维护结构兼容性


我们在上面看到,对函数签名的任何更改都是一个突破性的变化。结构的情况要好得多。如果具有导出的结构类型,则几乎总是可以添加字段或删除未导出的字段,而不会破坏兼容性。添加字段时,请确保其零值有意义并保留旧行为,以便不影响字段的现有代码继续工作。


回想一下,网络包的作者在 Go 1.11 中添加了 ListenConfig,因为他们认为可能会有更多选择。事实证明他们是对的。在 Go 1.13 中,添加了 KeepAlive 字段,以允许禁用保持活动状态或更改其周期。默认值为 0 将保留启用默认期间保持活动的原始行为。


有一种微妙的方式,新字段可以意外中断用户代码。如果结构中的所有字段类型都是可比较的,即这些类型的值可以与 == 和 != 进行比较,并用作映射键,则整个结构类型也是可比较的。在这种情况下,添加不可比较类型的新字段将使整体结构类型不可比较,从而破坏比较该结构类型的值的任何代码。


为了保持结构的可比性,不要向其添加非可比字段。您可以为此编写一个测试,或者依靠即将推出的 gorelease 工具来捕获它。


首先要防止比较,请确保结构具有非可比字段。它可能已经有一个 - 没有切片,映射或函数类型是可比的 - 但如果没有,可以添加一个类似:

type Point struct {
        _ [0]func()
        X int
        Y int
}


func() 类型无法比较,零长度数组不占用任何空间。我们可以定义一个类型来阐明我们的意图:

type doNotCompare [0]func()
type Point struct {
        doNotCompare
        X int
        Y int
}


你应该在你的结构中使用 donotCompare 吗?如果您已经定义了要用作指针的结构,也就是说,它有指针方法,也许还有返回指针的 NewXXX 构造函数,那么添加 doNotCompare 字段可能太过标准了。指针类型的用户了解该类型的每个值都是不同的:如果他们想要比较两个值,他们应该比较指针。


如果您正在定义一个结构,旨在直接用作值,如我们的 Point 示例,则您通常希望它是可比较的。在不常见的情况下,您有一个不需要比较的值结构,然后添加一个 doNotCompare 字段将为您提供以后更改结构的自由,而不必担心打破比较。缺点是,该类型不能用作 map 键。


06

总结


从头开始规划 API 时,请仔细考虑 API 将来对新更改的可扩展性。当您确实需要添加新功能时,请记住以下规则:添加、不更改或删除,请记住异常情况 — 接口、函数参数和返回值不能以向后兼容的方式添加。


如果您需要大幅更改 API,或者 API 在添加更多功能时开始失去重点,则可能是需要创建新的主要版本。但大多数时候,进行向后兼容的更改很容易,并避免给用户带来痛苦。





目录
相关文章
|
4月前
|
JavaScript
es5和es6的区别
es5和es6的区别
46 0
|
4月前
|
存储 JavaScript 前端开发
|
JSON 前端开发 JavaScript
ES6、ES7、ES8、ES9、ES10、ES11新特性3
ES6、ES7、ES8、ES9、ES10、ES11新特性
|
前端开发
ES6、ES7、ES8、ES9、ES10、ES11新特性2
ES6、ES7、ES8、ES9、ES10、ES11新特性
|
1月前
|
JavaScript 前端开发 Java
​ES7、ES8、ES9、ES10、ES11、ES12都增加了哪些新特性?
​ES7、ES8、ES9、ES10、ES11、ES12都增加了哪些新特性?
|
前端开发 JavaScript Java
ES6、ES7、ES8、ES9、ES10、ES11新特性1
ES6、ES7、ES8、ES9、ES10、ES11新特性
|
4月前
|
前端开发 JavaScript Java
ES7、ES8、ES9、ES10、ES11、ES12都增加了那些新特性?(二)
ES7、ES8、ES9、ES10、ES11、ES12都增加了那些新特性?(二)
|
4月前
|
JSON 前端开发 数据格式
ES7、ES8、ES9、ES10、ES11、ES12都增加了那些新特性?(一)
ES7、ES8、ES9、ES10、ES11、ES12都增加了那些新特性?(一)
105 0
|
11月前
|
SQL API Go
保持 Modules 的兼容性(上)
保持 Modules 的兼容性
23 0