Go: all:在包之间移动类型时支持渐进式代码修复

创建于 2016-12-01  ·  225评论  ·  资料来源: golang/go

原标题:提案:支持在包间移动类型时逐步修复代码

Go 应该添加为类型创建替代等效名称的功能,以便在代码库重构期间启用渐进式代码修复。 这是 Go 1.8 别名功能的目标,在 #16339 中提出,但在 Go 1.8 中被阻止。 因为我们没有为 Go 1.8 解决这个问题,所以它仍然是一个问题,我希望我们能为 Go 1.9 解决它。

在别名提案的讨论中,有很多关于为什么这种为类型创建备用名称的能力很重要的问题。 作为回答这些问题的新尝试,我撰写并发布了一篇文章“代码库重构(在 Go 的帮助下)” 。 如果您对动机有疑问,请阅读该文章。 (对于另一个更短的演示,请参阅 Robert 的Gophercon 闪电演讲。不幸的是,该视频直到 10 月 9 日才在线可用。更新,12 月 16 日:这是我的 GothamGo 演讲,它基本上是文章的初稿。)

这个问题_不是_提出一个具体的解决方案。 相反,我想从 Go 社区收集有关可能解决方案空间的反馈。 一种可能的方法是将别名限制为类型,如本文末尾所述。 可能还有其他我们应该考虑的。

请在此处发表有关类型别名或其他解决方案的想法作为评论。

谢谢你。

12 月 16 日更新:已发布类型别名的设计文档。
更新,1 月 9 日:接受提案,创建 dev.typealias 存储库,在 Go 1.9 周期开始时实施以进行实验。


讨论总结(最后更新于2017-02-02)

我们是否期望需要一个适用于所有声明的通用解决方案?

如果类型别名是 100% 必要的,那么 var 别名可能是 10% 必要的,func 别名是 1% 必要的,而 const 别名是 0% 必要的。 因为 const 已经有 = 并且 func 也可以合理地使用 =,所以关键问题是 var 别名是否足够重要以进行计划或实现。

正如@rogpeppe (https://github.com/golang/go/issues/16339#issuecomment-258771806)和@ianlancetaylor (https://github.com/golang/go/issues/16339#issuecomment-233644777)所争论的那样在最初的别名提议中,正如文章中提到的,变异的全局变量通常是一个错误。 将解决方案复杂化以适应通常是错误的内容可能没有意义。 (事实上​​,如果我们能弄清楚怎么做,从长远来看,Go 会朝着要求全局变量不可变的方向发展,我不会感到惊讶。)

因为更丰富的 var 别名可能不足以进行规划,所以这里的正确选择似乎是只关注类型别名。 这里的大多数评论似乎都同意。 我不会列出所有人。

我们是否需要新的语法(= vs => vs export)?

新语法最有力的论据是现在或将来都需要支持 var 别名(https://github.com/golang/go/issues/18130#issuecomment-264232763 by @Merovius)。 计划不使用 var 别名似乎没问题(请参阅上一节)。

如果没有 var 别名,重用 = 比引入新语法更简单,无论是 => 就像别名提案中的一样,~(https://github.com/golang/go/issues/18130#issuecomment-264185142 by @joegrasse),还是导出(https://github.com/golang/go/issues/18130#issuecomment-264152427 by @cznic)。

使用 = in 也将完全匹配 Pascal 和 Rust 中类型别名的语法。 就其他语言具有相同的概念而言,使用相同的语法是很好的。

展望未来,未来可能会有一个 Go,其中也存在 func 别名(参见 @nigeltao 的 https://github.com/golang/go/issues/18130#issuecomment-264324306),然后所有声明都将允许相同的形式:

const C2 = C1
func F2 = F1
type T2 = T1
var V2 = V1

其中唯一不能建立真正别名的是 var 声明,因为 V2 和 V1 可以在程序执行时独立重新定义(与不可变的 const、func 和类型声明不同)。 由于变量的一个主要原因是允许它们变化,因此该例外至少很容易解释。 如果 Go 转向不可变的全局变量,那么即使是那个异常也会消失。

需要明确的是,我不是在这里建议使用 func 别名或不可变的全局变量,只是解决此类未来添加的含义。

@jimmyfrasche建议 (https://github.com/golang/go/issues/18130#issuecomment-264278398) 为除 consts 之外的所有内容使用别名,这样 const 将成为例外而不是 var:

const C2 = C1 // no => form
func F2 => F1
type T2 => T1
var V2 => V1
var V2 = V1 // different from => form

const 和 var 不一致似乎比 var 不一致更难解释。

这可以是仅工具或编译器的更改而不是语言更改吗?

当然值得询问是否可以纯粹通过提供给编译器的辅助信息启用渐进​​式代码修复(例如,@btracey 的 https://github.com/golang/go/issues/18130#issuecomment-264205929)。

或者,如果编译器可以在编译之前应用某种基于规则的预处理来转换输入文件(例如,https://github.com/golang/go/issues/18130#issuecomment-264329924 by @tux21b)。

不幸的是,不,这种变化真的不能被限制在这种情况下。 至少有两个编译器(gc 和 gccgo)需要协调,但任何其他分析程序的工具也是如此,例如 go vet、guru、goimports、gocode(代码完成)等。

正如@bcmills所说(https://github.com/golang/go/issues/18130#issuecomment-264275574),“所有实现都必须支持的‘非语言变化’机制是事实上的语言变化——它只是一个文档较差的文档。”

别名还有哪些其他用途?

我们知道以下内容。 鉴于特别是类型别名被认为足够重要以包含在 Pascal 和 Rust 中,可能还有其他的。

  1. 别名(或只是类型别名)可以创建扩展其他包的插入式替换。 例如,请参阅https://go-review.googlesource.com/#/c/32145/ ,尤其是提交消息中的解释。

  2. 别名(或只是类型别名)可以将一个包结构化为一个小的 API 表面,但是一个大的实现是一个包的集合,以获得更好的内部结构,但仍然只提供一个供客户端导入和使用的包。 在https://github.com/golang/go/issues/16339#issuecomment -232813695 中描述了一个有点抽象的例子。

  3. 协议缓冲区具有“导入公共”功能,其语义在生成的 C++ 代码中实现起来很简单,但在生成的 Go 代码中不可能实现。 这让 C++ 和 Go 客户端之间共享的协议缓冲区定义的作者感到沮丧。 类型别名将为 Go 提供一种实现此功能的方法。 实际上, import public

  4. 缩写长名称。 本地(未导出或非包范围)别名可能有助于缩写长类型名称,而不会引入全新类型的开销。 与所有这些用途一样,最终代码的清晰度将强烈影响这是否是建议用途。

类型别名的提案还需要解决哪些其他问题?

列出这些以供参考。 在本节中不尝试解决或讨论它们,尽管稍后讨论了一些并在下面的单独部分中进行了总结。

  1. 在 godoc 中处理。 (https://github.com/golang/go/issues/18130#issuecomment-264323137 by @nigeltao和 https://github.com/golang/go/issues/18130#issuecomment-264326437 by @jimmyfrasche)

  2. 可以在以别名命名的类型上定义方法吗? (https://github.com/golang/go/issues/18130#issuecomment-265077877 by @ulikunitz)

  3. 如果允许别名到别名,我们如何处理别名循环? (https://github.com/golang/go/issues/18130#issuecomment-264494658 by @thwd)

  4. 别名应该能够导出未导出的标识符吗? (https://github.com/golang/go/issues/18130#issuecomment-264494658 by @thwd)

  5. 嵌入别名时会发生什么(如何访问嵌入字段)? (https://github.com/golang/go/issues/18130#issuecomment-264494658 by @thwd ,也是#17746)

  6. 别名是否可用作构建程序中的符号? (https://github.com/golang/go/issues/18130#issuecomment-264494658 by @thwd)

  7. Ldflags 字符串注入:如果我们引用别名怎么办? (https://github.com/golang/go/issues/18130#issuecomment-264494658 by @thwd;只有在有 var 别名时才会出现这种情况。)

版本控制本身是解决方案吗?

“在这种情况下,版本控制可能是完整的答案,而不是类型别名。”
(https://github.com/golang/go/issues/18130#issuecomment-264573088 by @iainmerrick)

正如文章中

能否解决更大的重构问题?

https://github.com/golang/go/issues/18130#issuecomment -265052639 中, @niemeyer指出将 os.Error 移动到错误实际上有两个更改:名称更改但定义也是如此(当前错误方法曾经是一个字符串方法)。

@niemeyer建议,也许我们可以找到更广泛的重构问题的解决方案,即修复包之间移动的类型作为特殊情况,但也处理方法名称更改等问题,他提出了一个围绕“适配器”构建的解决方案。

评论中有相当多的讨论,我无法在这里轻松总结。 讨论还没有结束,但目前还不清楚“适配器”是否适合语言或在实践中实现。 很明显,适配器至少比类型别名复杂一个数量级。

适配器也需要一个连贯的解决方案来解决下面提到的子类型问题。

可以在别名类型上声明方法吗?

当然别名不允许绕过通常的方法定义限制:如果一个包定义了类型 T1 = otherpkg.T2,它不能在 T1 上定义方法,就像它不能直接在 otherpkg.T2 上定义方法一样。 也就是说,如果type T1 = otherpkg.T2,那么func (T1) M() 就等价于func (otherpkg.T2) M() ,今天是无效的,仍然无效。 但是,如果一个包定义了类型 T1 = T2(都在同一个包中),那么答案就不太清楚了。 在这种情况下, func (T1) M() 等价于 func (T2) M(); 因为后者是允许的,所以有一个论据来允许前者。 当前的设计文档没有在这里施加限制(与一般避免限制保持一致),因此 func (T1) M() 在这种情况下是有效的。

https://github.com/golang/go/issues/18130#issuecomment -267694112 中, @jimmyfrasche建议改为定义“在方法定义中不使用别名”将是一个明确的规则,避免需要知道 T 的定义以确定 func (T) M() 是否有效。 在https://github.com/golang/go/issues/18130#issuecomment -267997124 中, @rsc指出即使在今天也有某些 T 对 func (T) M() 无效: https://play .golang.org/p/bci2qnldej。 在实践中,这不会出现,因为人们编写了合理的代码。

我们将牢记这种可能的限制,但要等到有强有力的证据表明需要时再引入。

有没有更简洁的方法来处理嵌入,更一般地说是字段重命名?

https://github.com/golang/go/issues/18130#issuecomment -267691816 中, @Merovius指出,在包移动期间更改其名称的嵌入类型将导致问题,因为该新名称最终必须在使用网站。 例如,如果用户类型 U 有一个嵌入的 io.ByteBuffer 移动到 bytes.Buffer,那么当 U 嵌入 io.ByteBuffer 时,字段名称是 U.ByteBuffer,但是当 U 更新为引用 bytes.Buffer 时,字段名称必然更改为 U.Buffer。

https://github.com/golang/go/issues/18130#issuecomment -267710478 中, @neild指出如果必须删除对 io.ByteBuffer 的引用,至少有一个解决方法:定义 U 的包 P 也可以定义 'type ByteBuffer = bytes.Buffer' 并将该类型嵌入到 U 中。然后 U 仍然有一个 U.ByteBuffer,即使在 io.ByteBuffer 完全消失之后。

https://github.com/golang/go/issues/18130#issuecomment -267703067 中, @bcmills提出了字段别名的想法,以允许一个字段在逐步修复期间具有多个名称。 字段别名将允许定义类似type U struct { bytes.Buffer; ByteBuffer = Buffer }而不必创建顶级类型别名。

https://github.com/golang/go/issues/18130#issuecomment -268001111 中, @rsc提出了另一种可能性:“使用此名称嵌入此类型”的一些语法,以便可以嵌入字节。 Buffer 作为字段名称 ByteBuffer,不需要顶级类型或备用名称。 如果存在,那么类型名称可以从 io.ByteBuffer 更新为 bytes.Buffer ,同时保留原始名称(并且不会引入第二个,也不会引入笨拙的导出类型)。

一旦我们有更多的证据表明大规模重构被字段更改名称的问题所阻碍,这些似乎都值得探索。 正如@rsc所写的那样,“如果类型别名帮助我们达到缺乏字段别名是大规模重构的下一个大障碍的地步,那将是进步!”

有人建议限制在嵌入字段中使用别名或更改嵌入名称以使用目标类型的名称,但这些会使别名引入破坏现有定义,然后必须以原子方式修复,从根本上阻止任何逐步修复。 @rsc :“我们在 #17746 中详细讨论了这个问题。我最初支持嵌入式 io.ByteBuffer 别名为 Buffer,但上述论点让我确信我错了。 @jimmyfrasche特别做了一些好事关于代码的争论不会根据嵌入事物的定义而改变。我认为完全禁止嵌入别名是站不住脚的。”

使用反射对程序有什么影响?

使用反射查看别名的程序。 在https://github.com/golang/go/issues/18130#issuecomment -267903649 中, @atdiar指出如果程序使用反射来查找定义类型的包甚至名称一个类型,它会在类型移动时观察变化,即使留下了转发别名。 在https://github.com/golang/go/issues/18130#issuecomment -268001410 中, @rsc证实了这一点并写道“就像嵌入的情况一样,它并不完美。与嵌入的情况不同,我没有任何除了可能不应该使用反射编写代码之外的答案对这些细节非常敏感。”

今天使用 vendored 包也改变了反射看到的包导入路径,我们还没有意识到由这种歧义引起的重大问题。 这表明程序通常不会以使用别名破坏的方式检查reflect.Type.PkgPath。 即便如此,这也是一个潜在的差距,就像嵌入一样。

程序和插件分开编译有什么影响?

https://github.com/golang/go/issues/18130#issuecomment -268524504 中, @atdiar提出了对目标文件的影响和单独编译的问题。 在https://github.com/golang/go/issues/18130#issuecomment -268560180中, @rsc回复说这里应该不需要修改:如果X导入Y,Y修改后重新编译,那么X需要修改也要重新编译。 在没有别名的情况下,这在今天是正确的,并且在使用别名的情况下仍然如此。 单独编译意味着能够在不同的步骤中编译 X 和 Y(编译器不必在同一个调用中处理它们),而不是可以在不重新编译 X 的情况下更改 Y。

总和类型或某种子类型是替代解决方案吗?

https://github.com/golang/go/issues/18130#issuecomment -264413439 中, @iand建议“可替换类型”、“可以替换函数参数、返回值等中的命名类型的类型列表。 ”。 在https://github.com/golang/go/issues/18130#issuecomment -268072274 中, @j7b建议使用代数类型“所以我们也得到了一个空接口等效于编译时类型检查作为奖励”。 这个概念的其他名称是和类型和变体类型。

一般来说,这不足以允许使用渐进式代码修复移动类型。 有两种方法可以考虑这个问题。

https://github.com/golang/go/issues/18130#issuecomment -268075680 中, @bcmills采取了具体的方式,指出代数类型的表示与原始类型不同,这使得无法处理总和和原始的可以互换:后者有类型标签。

https://github.com/golang/go/issues/18130#issuecomment -268585497 中, @rsc采用理论方法,在https://github.com/golang/go/issues/18130#issuecomment -265211655 上扩展@gri指出在渐进式代码修复中,有时您需要 T1 作为 T2 的子类型,有时反之亦然。 两者成为彼此的子类型的唯一方法是让它们成为相同的类型,这并非偶然地是类型别名的作用。

作为切线,除了不能解决渐进式代码修复问题外,代数类型/和类型/联合类型/变体类型本身很难添加到 Go 中。 看
常见问题解答Go 1.6 AMA 讨论以了解更多信息。

https://github.com/golang/go/issues/18130#issuecomment -265206780 中, @thwd表明,由于 Go 在具体类型和接口之间存在子类型关系(bytes.Buffer 可以看作 io.Reader 的子类型) 和接口之间(io.ReadWriter 是 io.Reader 的子类型,以同样的方式),使接口“递归协变(根据当前的方差规则)到它们的方法参数”将解决这个问题,前提是所有未来的包只使用接口,而不是像结构这样的具体类型(“也鼓励良好的设计”)。

作为解决方案,存在三个问题。 首先,它有上面的子类型问题,所以它不能解决渐进式代码修复。 其次,正如@thwd在此建议中指出的那样,它不适用于现有代码。 第三,强制在任何地方使用接口实际上可能不是好的设计,并会引入性能开销(参见@Merovius和 https://github 的例如 https://github.com/golang/go/issues/18130#issuecomment-265211726 .com/golang/go/issues/18130#issuecomment-265224652 @zombiezen)。

限制

本节收集了建议的限制以供参考,但请记住,限制会增加复杂性。 正如我在https://github.com/golang/go/issues/18130#issuecomment -264195616 中所写的那样,“我们应该只在实际体验无限制、更简单的设计后才实施这些限制,这有助于我们了解限制是否会带来足够的支付其成本的好处。”

换句话说,任何限制都需要有证据证明它可以防止一些严重的误用或混淆。 由于我们还没有实施解决方案,因此没有这样的证据。 如果经验确实提供了这种证据,那么这些将值得返回。

限制? 标准库类型的别名只能在标准库中声明。

(https://github.com/golang/go/issues/18130#issuecomment-264165833 和 https://github.com/golang/go/issues/18130#issuecomment-264171370 by @iand)

关注点是“已重命名标准库概念以适应自定义命名约定的代码”,或“跨多个包的长意大利面条式别名链,最终返回标准库”,或“将接口{}和错误之类的东西别名化” .

如前所述,该限制将不允许上述涉及 x/image/draw 的“扩展包”情况。

不清楚为什么标准库应该是特殊的:任何代码都会存在问题。 此外, interface{} 和 error 都不是标准库中的类型。 将限制重新表述为“别名预定义类型”将不允许别名错误,但需要别名错误是本文中的激励示例之一。

限制? 别名目标必须是包限定标识符。

(https://github.com/golang/go/issues/18130#issuecomment-264188282 by @jba)

这将使得在包中重命名类型时无法创建别名,这可能会被广泛使用以需要逐步修复(https://github.com/golang/go/issues/18130#issuecomment-264274714 by @ bcmills)。

它也不允许出现文章中的别名错误。

限制? 别名目标必须是与别名同名的包限定标识符。

(在 Go 1.8 中的别名讨论中提出)

除了上一节限制包限定标识符的问题外,强制名称保持不变将不允许本文中从 io.ByteBuffer 到 bytes.Buffer 的转换。

限制? 应该以某种方式阻止别名。

“如何将别名隐藏在导入后面,就像“C”和“不安全”一样,以进一步阻止它的使用?同样,我希望别名语法变得冗长,并作为继续重构的脚手架脱颖而出.” - https://github.com/golang/go/issues/18130#issuecomment -264289940 @xiegeo

“我们是否也应该自动推断别名类型是遗留类型并且应该被新类型替换?如果我们强制执行 golint、godoc 和类似工具将旧类型可视化为已弃用,它将非常显着地限制类型别名的滥用。并且混淆功能被滥用的最终问题将得到解决。” - https://github.com/golang/go/issues/18130#issuecomment -265062154 @rakyll

在我们知道它们会被错误使用之前,劝阻使用似乎还为时过早。 可能有很好的非临时用途(见上文)。

即使在代码修复的情况下,根据导入图施加的约束,旧类型或新类型可能是转换期间的别名。 作为别名并不意味着该名称已被弃用。

已经有一种机制可以将某些声明标记为已弃用(请参阅 @jimmyfrasche 的 https://github.com/golang/go/issues/18130#issuecomment-265294564)。

限制? 别名必须以命名类型为目标。

“别名不应该应用于未命名类型。从一种未命名类型转移到另一种类型时,它们不是“代码修复”故事。允许未命名类型使用别名意味着我不能再将 Go 教为简单的命名和未命名类型。” - https://github.com/golang/go/issues/18130#issuecomment -276864903 @davecheney

在我们知道它们会被错误使用之前,劝阻使用似乎还为时过早。 未命名的目标可能有很好的用途(见上文)。

正如设计文档中所述,我们确实希望更改术语以使情况更清晰。

FrozenDueToAge Proposal Proposal-Accepted

最有用的评论

@cznic@iand等:请注意 _restrictions 会增加复杂性

仅仅由于假设的误用而对设计试验实施限制通常是错误的。 这发生在别名提案讨论中,它使试验中的别名无法处理文章中的io.ByteBuffer => bytes.Buffer转换。 撰写本文的部分目的是定义一些我们知道我们希望能够处理的情况,以便我们不会无意中将它们限制在外。

再举一个例子,很容易误用参数来禁止非指针接收器,或者禁止非结构类型的方法。 如果我们做了其中任何一个,你就不能用 String() 方法创建枚举来打印自己,而且你不能让http.Headers既是一个普通的映射又提供辅助方法。 通常很容易想象误用; 引人注目的积极用途可能需要更长的时间才能出现,因此为实验创造空间很重要。

再举一个例子,指针与值方法的原始设计和实现没有区分 T 和 *T 上的方法集:如果你有一个 *T,你可以调用值方法(接收者 T),如果你有一个T,你可以调用指针方法(接收器*T)。 这很简单,没有任何限制可以解释。 但随后的实际经验告诉我们,允许对值进行指针方法调用会导致一类特定的令人困惑、令人惊讶的错误。 例如,你可以写:

var buf bytes.Buffer
io.Copy(buf, reader)

和 io.Copy 会成功,但 buf 里面什么也没有。 我们必须在解释该程序运行不正确的原因或解释该程序无法编译的原因之间做出选择。 无论哪种方式都会有问题,但我们站在避免错误执行的一边。 即便如此,我们仍然必须写一篇关于为什么设计有一个洞

再次请记住,限制会增加复杂性。 像所有复杂性一样,限制需要有充分的理由。 在设计过程的这个阶段,考虑可能适用于特定设计的限制是很好的,但我们可能应该只在实际体验无限制、更简单的设计后才实施这些限制,这有助于我们了解限制是否会带来足够的好处以支付其成本。

所有225条评论

我喜欢这看起来多么统一。

const OldAPI => NewPackage.API
func  OldAPI => NewPackage.API
var   OldAPI => NewPackage.API
type  OldAPI => NewPackage.API

但是由于我们几乎可以逐渐移动大多数元素,也许是最简单的
解决方案 _is_ 只是为了允许=类型。

const OldAPI = NewPackage.API
func  OldAPI() { NewPackage.API() }
var   OldAPI = NewPackage.API
type  OldAPI = NewPackage.API

所以首先,我只想感谢你写的这么出色的文章。 我认为最好的解决方案是使用赋值运算符引入类型别名。 这不需要新的关键字/运算符,使用熟悉的语法,并且应该可以解决大型代码库的重构问题。

正如 Russ 的文章所指出的,任何类似别名的解决方案都需要优雅地解决https://github.com/golang/go/issues/17746https://github.com/golang/go/issues/17784

谢谢你写那篇文章。

我发现使用赋值运算符的仅类型别名是最好的:

type OldAPI = NewPackage.API

我的理由:

  • 它更简单。
    替代解决方案 => 根据其操作数具有微妙的不同含义,这对于 Go 来说是不合适的。
  • 它专注而保守。
    手头的类型问题已解决,您无需担心想象通用解决方案的复杂性。
  • 是审美。
    我觉得它看起来更讨人喜欢。

以上所有这些:结果简单、专注、保守和美观,让我很容易想象它是 Go 的一部分。

如果解决方案仅限于类型,则语法

type NewFoo = old.Foo

正如@rsc的文章中所讨论的,之前已经考虑过,对我来说看起来非常好。

如果我们希望能够对常量、变量和函数做同样的事情,我的首选语法是(如之前所提议的)

package newfmt

import (
    "fmt"
)

// No renaming.
export fmt.Printf // Note: Same as `export Printf fmt.Printf`.

export (
        fmt.Sprintf
        fmt.Formatter
)

// Renaming.
export Foo fmt.Errorf // Foo must be exported, ie. `export foo fmt.Errorf` would be invalid.

export (
    Bar fmt.Fprintf
    Qux fmt.State
)

如前所述,缺点是引入了一个新的、仅限顶级的关键字,尽管在技术上可行且完全向后兼容,但这一点无可否认。 我喜欢这种语法,因为它反映了导入的模式。 在我看来,只有在允许进口的同一部分才允许出口,即。 在 package 子句和任何 var、type、constant 或 function TLD 之间。

重命名标识符将在包范围内声明,但是,新名称在上面声明它们的包中不可见(上面示例中的 newfmt ),这与往常一样是不允许的。 鉴于前面的示例,TLD

var v = Printf // undefined: Printf.
var Printf int // Printf redeclared, previous declaration at newfmt.go:8.

在导入包中,重命名标识符通常是可见的,就像(newftm 的)包块的任何其他导出标识符一样。

package foo

import "newfmt"

type bar interface {
    baz(qux newfmt.Qux) // qux type is identical to fmt.State.
}

总之,这种方法不会在 newfmt 中引入任何新的本地名称绑定,我相信这至少避免了 #17746 中讨论的一些问题,并完全解决了 #17784。

我的第一个偏好是只有类型的type NewFoo = old.Foo

如果需要更通用的解决方案,我同意@cznic 的观点,即专用关键字优于新运算符(尤其是方向性令人困惑的非对称运算符 [1])。 话虽如此,我不认为export关键字传达了正确的含义。 语法和语义都不反映importalias呢?

我理解为什么@cznic不希望在声明它们的包中可以访问新名称,但是,至少对我来说,这种限制让人感到意外和人为(尽管我完全理解其背后的原因)。

[1] 我已经使用 Unix 近 20 年了,但我仍然无法在第一次尝试时创建符号链接。 在我阅读了手册之后,我通常即使在第二次尝试时也会失败。

我想提出一个额外的限制:标准库类型的类型别名只能在标准库中声明。

我的理由是,我不想使用已重命名标准库概念以适应自定义命名约定的代码。 我也不想处理跨多个包的长意大利面条式别名链,这些别名最终回到标准库。

@iand :该约束将阻止使用此功能将任何内容迁移到标准库中。 例如,当前将Context迁移到标准库中。 Context老家应该成为标准库中Context的别名。

@quentinmit不幸的是,这是真的。 它还限制了此 CL https://go-review.googlesource.com/#/c/32145/ 中golang.org/x/image/draw 的用例

我真正担心的是人们将interface{}error东西别名化

如果决定引入一个新的运营商,我想建议~ 。 在英语中,它通常被理解为“类似于”、“大约”、“大约”或“大约”。 正如上面的@4ad所说, =>是一个具有混淆方向性的不对称运算符。

例如:

const OldAPI ~ NewPackage.API
func  OldAPI ~ NewPackage.API
var   OldAPI ~ NewPackage.API
type  OldAPI ~ NewPackage.API

@iand如果我们将右侧限制为包限定标识符,那么这将消除您的具体担忧。

这也意味着您不能为当前包中的任何类型或长类型表达式(如map[string]map[int]interface{}设置别名。 但这些用途与逐步修复代码的主要目标无关,所以也许它们并没有太大的损失。

@cznic@iand等:请注意 _restrictions 会增加复杂性

仅仅由于假设的误用而对设计试验实施限制通常是错误的。 这发生在别名提案讨论中,它使试验中的别名无法处理文章中的io.ByteBuffer => bytes.Buffer转换。 撰写本文的部分目的是定义一些我们知道我们希望能够处理的情况,以便我们不会无意中将它们限制在外。

再举一个例子,很容易误用参数来禁止非指针接收器,或者禁止非结构类型的方法。 如果我们做了其中任何一个,你就不能用 String() 方法创建枚举来打印自己,而且你不能让http.Headers既是一个普通的映射又提供辅助方法。 通常很容易想象误用; 引人注目的积极用途可能需要更长的时间才能出现,因此为实验创造空间很重要。

再举一个例子,指针与值方法的原始设计和实现没有区分 T 和 *T 上的方法集:如果你有一个 *T,你可以调用值方法(接收者 T),如果你有一个T,你可以调用指针方法(接收器*T)。 这很简单,没有任何限制可以解释。 但随后的实际经验告诉我们,允许对值进行指针方法调用会导致一类特定的令人困惑、令人惊讶的错误。 例如,你可以写:

var buf bytes.Buffer
io.Copy(buf, reader)

和 io.Copy 会成功,但 buf 里面什么也没有。 我们必须在解释该程序运行不正确的原因或解释该程序无法编译的原因之间做出选择。 无论哪种方式都会有问题,但我们站在避免错误执行的一边。 即便如此,我们仍然必须写一篇关于为什么设计有一个洞

再次请记住,限制会增加复杂性。 像所有复杂性一样,限制需要有充分的理由。 在设计过程的这个阶段,考虑可能适用于特定设计的限制是很好的,但我们可能应该只在实际体验无限制、更简单的设计后才实施这些限制,这有助于我们了解限制是否会带来足够的好处以支付其成本。

此外,我希望我们可以就尝试什么做出初步决定,然后在 Go 1.9 周期开始时(最好是周期开始的那一天)准备好进行实验。 有更多的时间进行实验会有很多好处,其中包括有机会了解特定限制是否具有吸引力。 别名的一个错误是直到 Go 1.8 周期接近尾声才提交完整的实现。

关于原始别名提议的一件事是,在预期用例(启用重构)中,别名类型的实际使用应该只是暂时的。 在 protobuffer 示例中,一旦逐步修复结束,io.BytesBuffer 存根就会被删除。

如果别名机制应该只是暂时看到,它真的需要语言更改吗? 也许可以有一种机制来为gc提供“别名”列表。 gc 可以临时进行替换,下游代码库的作者可以在修复合并时逐渐删除此文件中的项目。 我意识到这个建议也有棘手的后果,但它至少鼓励了一种临时机制。

我不会参与关于语法的自行车棚(我基本上不在乎),只有一个例外:如果决定添加别名并且决定将它们限制为类型,请使用始终可扩展到至少var的语法funcconst (所有提议的语法结构都允许所有,除了type Foo = pkg.Bar )。 原因是,虽然我同意var别名产生差异的情况可能很少见,但我不认为它们不存在,因此相信我们可能会在某个时候决定添加他们也是。 那时我们肯定希望所有别名声明保持一致,如果它是type Foo = pkg.Barvar Foo => pkg.Bar会很糟糕。

我也有点争论拥有所有四个。 原因是

1)一个区别var ,我有时会使用它。 例如,我经常公开一个全局var Debug *log.Logger ,或者重新分配像http.DefaultServeMux这样的全局单例来拦截/删除向其中添加处理程序的包的注册。

2)我也认为,虽然func Foo() { pkg.Bar() }func Foo => pkg.Bar func Foo() { pkg.Bar() }做同样的事情,但后者的意图更清晰(特别是如果你已经知道别名的话)。 它清楚地表明“这并不是真的要在这里”。 因此,虽然技术上相同,但别名语法可以用作文档。

不过,这不是我要死的山; 现在单独使用 type-aliases 对我来说没问题,只要以后有扩展它们的选项。

我也很高兴这就是这样写的。 它总结了我一段时间以来对 API 设计和稳定性的一些看法,将来也将作为链接人们的简单参考:)

但是,我也想强调,在那里,与文档不同的别名所涵盖的其他用例(以及 AIUI 是此问题的更一般意图,即找到一些解决方案来解决逐步修复)。 我高兴社区可以就启用逐步修复的概念达成一致,但如果决定采用与别名不同的决定来实现它,我也认为在这种情况下应该同时讨论是否支持以及如何支持诸如 protobuf 公共导入或x/image/draw使用不同解决方案的直接替换包的用例(两者都有点接近我的心)。 @btracey关于别名的 go-tool/gc 标志的提议就是一个例子,我认为,虽然它相对较好地涵盖了逐步修复,但对于其他用例来说并不是真正可以接受的。 你真的不能指望每个想要编译使用x/image/draw来传递这些标志的东西的人,他们应该能够go get

@jba

@iand如果我们将右侧限制为包限定标识符,那么这将消除您的具体担忧。

这也意味着您不能对当前包中的任何类型使用别名,[...]。 但这些用途与逐步修复代码的主要目标无关,所以也许它们并没有太大的损失。

在包内重命名(例如,更惯用或更一致的名称)当然是一种可能合理地想要进行的重构,如果包被广泛使用,则需要逐步修复。

我认为仅限于包限定名称是错误的。 (仅限导出名称的限制可能更容易容忍。)

@btracey

也许可以有一种机制为 gc 提供“别名”列表。 gc 可以临时进行替换,下游代码库的作者可以在修复合并时逐渐删除此文件中的项目。

gc的机制要么意味着代码只能在修复过程中使用gc构建,或者该机制必须得到其他编译器的支持(例如gccgollgo )。 所有实现都必须支持的“非语言更改”机制是事实上的语言更改 - 它只是文档较差的一种。

@btracey@bcmills ,而不仅仅是编译器:任何分析源代码的工具,如大师或其他人构建的任何工具。 无论您如何切片,这肯定是一种语言变化。

好的谢谢。

另一种可能性是除了 consts 之外的所有东西的别名( @rsc请原谅我提出限制!)

对于常量, =>实际上只是写=一种更长的方式。 没有新的语义,就像类型和变量一样。 没有像 funcs 那样保存击键。

这至少可以解决#17784。

反对意见是,工具可以以不同的方式处理案例,并且可以作为意图的指标。 这是一个很好的反驳,但我认为它并没有超过这样一个事实,即它基本上是做完全相同的事情的两种方法。

也就是说,我现在只使用类型别名就可以了,它们当然是最重要的。 我绝对同意@Merovius ,我们应该强烈考虑保留将来添加 var 和 func 别名的选项,即使这些不会发生一段时间。

如何隐藏导入后面的别名,就像“C”和“不安全”一样,以进一步阻止它的使用? 同样,我希望别名语法变得冗长,并作为持续重构的脚手架脱颖而出。

为了稍微打开设计空间,这里有一些想法。 他们没有充实。 它们可能很糟糕和/或不可能; 希望主要是在其他人身上引发新的/更好的想法。 如果有任何兴趣,我们可以进一步探索。

(1) 和 (2) 的动机是以某种方式使用转换而不是别名。 在 #17746 中,别名遇到了关于同一类型有多个名称的问题(或多种拼写相同名称的方式,取决于您将别名视为像 #define 还是像硬链接)。 使用转换通过保持类型不同来回避这一点。

  1. 添加更多自动转换。

当您调用fmt.Println("abc")或写入var e interface{} = "abc""abc"会自动转换为interface{} 。 我们可以更改语言,以便当您声明type T struct { S }并且 T 没有非提升方法时,编译器将根据需要自动在 S 和 T 之间转换,包括在其他结构中递归。 然后 T 可以作为 S 的事实上的别名(反之亦然),用于逐步重构的目的。

  1. 添加一种新的“看起来像”类型。

type T ~S声明一个新类型 T,它是一个“看起来像 S”的类型。 更准确地说,T 是“可转换为类型 S 或从类型 S 转换的任何类型”。 (和往常一样,语法可以在后面讨论。)像接口类型一样,T 不能有方法; 要使用 T 基本上做任何事情,您需要将其转换为 S(或可转换为/从 S 转换的类型)。 与接口类型不同,没有“具体类型”,S 到 T 和 T 到 S 之间的转换不涉及表示变化。 对于逐步重构,这些“看起来像”的类型将允许作者编写接受新旧类型的 API。 (“看起来像”类型基本上是一种高度受限的简化联合类型。)

  1. 类型标签

奖金超级可怕的想法。 (请不要告诉我这很糟糕——我知道。我只是想激发其他人的新想法。)如果我们引入类型标签(如结构标签),并使用特殊类型标签来设置和控制别名,比如type T S "alias:\"T\"" 。 类型标签还有其他用途,它为包作者提供了更多别名规范的范围,而不仅仅是“这种类型是别名”; 例如,代码的作者可以指定嵌入行为。

如果我们再次尝试别名,可能值得考虑“godoc 做什么”,类似于“iota 做什么”和“嵌入做什么”的问题。

具体来说,如果我们有

type  OldAPI => NewPackage.API

并且 NewPackage.API 有一个文档注释,我们是否希望在“type OldAPI”旁边复制/粘贴该注释,我们是否希望不对其进行注释(godoc 自动提供链接或自动复制/粘贴),或者将还有其他一些约定吗?

有点无关紧要,虽然主要动机是并且应该支持渐进式代码修复,但一个次要用例(回到别名提案,因为这是一个具体的提案)可能是在呈现单个函数时避免双重函数调用开销由多个依赖于构建标签的实现支持。 我现在只是挥手,但我觉得别名在最近的https://groups.google.com/d/topic/golang-nuts/wb5I2tjrwoc/discussion “避免包中的函数调用开销与 go+asm 实现”讨论。

@nigeltao re godoc,我认为:

无论如何,它应该始终链接到原始文件。

如果别名上有文档,无论如何都应该显示这些文档。

如果别名上没有文档,很容易让 godoc 显示原始文档,但是如果别名也更改了名称,则类型名称将是错误的,文档可能引用不在当前包中的项目,并且,如果它用于逐步重构,当您查看 X 时,可能会显示一条消息“已弃用:使用 X”。

但是,对于大多数用例来说,这可能并不重要。 这些是可能出错的事情,而不是会出错的事情。 其中一些可以通过 linting 检测到,例如重命名的别名和意外复制弃用警告。

我不确定之前是否已经发布了以下想法,但是主要基于工具的“gofix”/“gorename”之类的方法是什么? 详细说明:

  • 任何包都可以包含一组重写规则(例如映射pkg.Ident => otherpkg.Ident
  • 这些重写规则可以在任意 go 文件中用//+rewrite ...标签指定
  • 这些重写规则不仅限于 ABI 兼容更改,还可以做其他事情(例如pkg.MyFunc(a) => pkg.MyFunc(context.Contex(), a)
  • gofix 之类的工具可用于将所有转换应用于当前存储库。 这使得包的用户可以轻松更新他们的代码。
  • 没有必要为了编译成功而调用 gofix 工具。 仍然希望使用依赖项 X 的旧 API(以保持与 X 的新旧版本兼容)的库仍然可以这样做。 go build 命令应该即时应用转换(在包 X 的重写标签中指定),而无需更改磁盘上的文件。

最后一步可能会使编译器复杂化/减慢一点,但它基本上只是一个预处理器,无论如何重写规则的数量应该保持很小。 所以,今天足够的头脑风暴:)

使用别名来避免函数调用开销似乎是一种解决编译器无法内联非叶函数的技巧。 我不认为实现缺陷应该影响语言规范。

@josharian虽然你不打算将它们作为完整的建议,但让我回应(即使只是,这样任何受到你启发的人都可以考虑直接的批评):

  1. 并没有真正解决问题,因为转换并不是真正的问题。 x/net/context.Context是可分配/可转换/可分配给context.Context 。 问题是高阶类型; 即类型func (ctx x/net/context.Context)func (ctx context.Context)是不同的,即使参数是可赋值的。 因此,对于解决问题的 1, type T struct { S }需要表示TS是相同的类型。 这意味着,毕竟您只是为别名使用了不同的语法(只是这种语法已经具有不同的含义)。

  2. 高阶类型再次出现问题,因为可分配/可转换类型不一定具有相同的内存表示(如果它们有,解释可能会发生显着变化)。 例如, uint8可以转换为uint64 ,反之亦然。 但这意味着,例如使用type T ~uint8 ,编译器不知道如何调用func(T) ; 是否需要在堆栈上压入 1、2、4 或 8 个字节? 可能有办法解决这个问题,但对我来说听起来很复杂(而且比别名更难理解)。

谢谢,@Merovius。

  1. 是的,我在这里错过了界面满意度。 你是对的,这不起作用。

  2. 我想到了“具有相同的记忆表示”。 来回敞篷车显然不是正确的解释——谢谢。

@uluyol是的,这主要是关于编译器无法内联非叶函数,但是对于对非叶的内联调用是否应出现在堆栈跟踪、runtime.Callers 等中,显式别名可能并不令人惊讶。

无论如何,正如我所说,这是一个小切线。

@josharian类似问题: [2]uintptrinterface{}具有相同的内存表示; 所以只依靠内存表示将允许规避类型安全。 uint64float64具有相同内存的代表性可转换回往复,但仍然会导致非常奇怪的结果至少,如果你不知道哪个是哪个。

不过,您可能会摆脱“相同的基础类型”。 不确定这会有什么影响。 例如,如果在字段中使用类型,那可能会导致错误。 如果您有type S1 struct { T1 }type S2 struct { T2 }T1T2具有相同的基础类型),那么在type L1 ~T1都可以作为type S struct { L1 } ,但由于T1T2仍然具有不同(虽然看起来相似)的底层类型,使用type L2 ~S1你不会有S2看起来像S1并且不能用作L2

因此,您必须在规范中的很多地方用“相同的底层类型”替换或修改“相同的类型”才能使这项工作有效,这看起来很笨拙,并且可能会对类型安全产生不可预见的后果。 “相似”类型似乎也比别名具有更大的滥用和混淆可能性,恕我直言,这似乎是反对别名的主要论点。

如果有人可以为它想出一个简单的规则,但没有这些问题,它绝对应该被视为一种替代方法:)

@josharian的想法之后,这是他的数字 2 的变体:

允许指定“可替代类型”。 这是一个类型列表,可以在函数参数、返回值等中替换命名类型。编译器将允许使用命名类型或其任何替代项的参数调用函数。 替代类型必须具有与命名类型兼容的定义。 兼容在这里意味着在声明中允许其他替代类型后,相同的内存表示和相同的声明。

一个直接的问题是这种关系的方向性与反转依赖图的别名提议相反。 仅此一点就可能使其不可行,但我在这里提出它,因为其他人可能会想到解决这个问题的方法。 一种方法可能是将替代品声明为 //go 注释,而不是通过导入图。 通过这种方式,它们可能变得更像宏。

相反,这种方向性的逆转有一些优点:

  • 可替换类型的集合由新包的作者控制,他更能保证语义
  • 原始包中不需要更改代码,因此客户在开始使用新包之前不必更新

将此应用于上下文重构:标准库上下文包将声明context.Context可以替换为golang.org/x/net/context.Context 。 这意味着任何接受 context.Context 的用法也可以接受golang.org/x/net/context.Context代替。 但是,返回 Context 的 context 包中的函数将始终返回context.Context

该提议绕过了嵌入问题 (#17746),因为嵌入类型的名称永远不会改变。 但是,可以使用替代类型的值来初始化嵌入类型。

@iand @josharian您要求某种协变类型的变体。

@josharian ,感谢您的建议。

Re type T struct { S } ,这看起来像是别名的不同语法,不一定是更清晰的语法。

Re type T ~S ,我要么不确定它与别名的区别,要么不确定它如何帮助重构。 我猜在重构中(比如 io.ByteBuffer -> bytes.Buffer),你会写:

package io
type ByteBuffer ~bytes.Buffer

但是,如果像您所说的那样,“要对 T 执行任何操作,您需要将其转换为 S”,那么所有使用 io.ByteBuffer 执行任何操作的代码仍然会中断。

Re type T S "alias" :上面@bcmills提出的一个关键点是,对于类型有多个等效名称是一种语言变化,无论它是如何拼写的。 所有编译器都需要知道 io.ByteBuffer 和 bytes.Buffer 是相同的,任何分析甚至类型检查代码的工具也是如此。 在我看来,您建议的关键部分类似于“也许我们应该提前计划其他添加内容”。 也许吧,但目前还不清楚字符串是否是描述这些的最佳方式,而且我们也不清楚我们想要在没有明确需求的情况下设计语法(如 Java 通用注释)。 即使我们确实有一个通用形式,我们仍然需要仔细考虑我们引入的任何新语义的所有含义,而且大多数仍然是需要更新所有工具的语言更改(不可否认,除了 gofmt)。 总的来说,继续寻找最清晰的方式来编写我们需要的表单似乎更简单,而不是创建一种或另一种元语言。

@Merovius FWIW,我想说 [2]uintptr 和 interface{} 没有相同的内存表示。 interface{} 是 [2]unsafe.Pointer 而不是 [2]uintptr。 uintptr 和指针是不同的表示。 但我认为您的总体观点是正确的,我们并不一定要允许直接转换那种东西。 我的意思是,您也可以从 interface{} 转换为 [2]*byte 吗? 这比这里需要的要多得多。

@jimmyfrasche@nigeltao ,关于 godoc:我同意我们也需要尽早工作。 我同意我们不应该硬编码“新功能 - 无论它是什么 - 将仅用于代码库重构”的假设。 它可能还有其他重要用途,例如 Nigel 被发现用于帮助编写带有别名的绘图扩展包。 正如吉米所说,我希望不推荐使用的东西会在他们的文档注释中明确标记为不推荐使用。 我确实考虑过如果没有文档注释会自动生成文档注释,但是没有什么明显的说法不应该从语法中已经很清楚(一般来说)。 举一个具体的例子,考虑旧的 Go 1.8 别名。 给定的

type ByteBuffer => bytes.Buffer

我们可以合成一个文档注释,说“ByteBuffer 是 bytes.Buffer 的别名”,但这对于显示定义来说似乎是多余的。 如果今天有人写“type X struct{}”,我们不会合成“X 是 struct{} 的命名类型”。

@iand ,谢谢。 听起来您的提议要求新包的作者从旧包中编写确切的定义,然后还需要将两者联系起来的声明,例如(组成语法):

package old
type T { x int }

package new
import "old"
type T1 { x int }
substitutable T1 <- old.T

我同意导入逆转是有问题的,它本身可能是一个阻碍,但让我们跳过这一点。 在这一点上,代码库似乎处于脆弱状态:现在包 new 可以通过在包 old 中添加结构字段的更改来破坏。 给定可替换的行,T1 只有一个可能的定义:与 old.T 完全相同。 如果这两种类型仍然有不同的定义,那么您还必须担心方法:方法实现也需要匹配吗? 如果不是,当您将 T 放入 interface{} 然后使用类型断言作为 T1 将其拉出并调用 M() 时会发生什么? 你有T1.M吗? 如果把它作为接口{M()}拉出来,不直接命名T1,调用M()怎么办? 你有TM吗? 由于在源树中同时包含两个定义的歧义,会造成很多复杂性。

当然,您可以说可替换行使其余部分变得多余,并且不需要对类型 T1 或任何方法进行定义。 但这与编写(使用旧别名语法)基本相同type T1 => old.T

回到导入图的问题,虽然文章中的例子都是根据新代码定义旧代码,但是如果包图是这样的,新的必须导入旧的,那么将重定向放在过渡期间的新包。

我认为这表明在任何这样的转换中,新包的作者和旧包的作者之间可能没有有用的区别。 最终,目标是将代码添加到新代码并从旧代码中删除,因此两个作者(如果他们不同)都需要参与其中。 并且两者在中间也需要某种协调的兼容性,无论是显式(某种重定向)还是隐式(类型定义必须完全匹配,如可替换性要求)。

@rsc破坏场景表明任何类型别名都需要是双向的。 即使在之前的别名提议下,新包中的任何更改都可能破坏任意数量的包,这些包恰好为该类型设置了别名。

@iand如果只有一个定义(因为另一个定义“与 _that_ 相同”),则不必担心它们不同步。

在#13467 中, @joegrasse指出,如果该提案提供了一种机制,当在多个包中使用 cgo 时,允许相同的 C 类型成为相同的 Go 类型,那就太好了。 这与此问题所针对的问题完全不同,但这两个问题都与类型别名有关。

是否有任何关于别名的提议/接受/拒绝限制/限制的摘要? 浮现在脑海中的一些问题是:

  • RHS 是否总是完全合格?
  • 如果允许别名到别名,我们如何处理别名循环?
  • 别名应该能够导出未导出的标识符吗?
  • 嵌入别名时会发生什么? (如何访问嵌入字段)
  • 别名是否可用作构建程序中的符号?
  • ldflags 字符串注入:如果我们引用别名怎么办?

@rsc我不想过多地转移话题,但在别名提议下,如果“新”删除了“旧”依赖的字段,则意味着“旧”的客户现在无法编译。

但是,在替代提案下,我认为可以安排只有新旧同时使用的客户端才会中断。 为此,只有当编译器检测到在“新”包中使用“旧”类型时,才必须验证替换指令。

@thwd我认为还没有好的文章。 我的笔记:

  • 别名循环不是问题。 在包交叉别名的情况下,由于导入循环,循环已经被禁止。 在非包交叉别名的情况下,它们显然需要被禁止,这与初始化顺序中的循环非常相似。 就我个人而言,我想要别名的别名,因为我认为它们不应该仅限于逐步修复用例(见我上面的评论),如果包 A 可能被某人移动类型而破坏,那将是可悲的使用别名打包 B(假设x/image/draw.Image别名draw.Image ,然后有人决定通过别名将draw.Imageimage.Draw ,假设它是安全的。突然x/image/draw中断,因为不允许使用别名的别名)。
  • 我认为以前别名的支持者已经同意别名导出未导出的标识符可能是一个坏主意,因为它可能导致奇怪。 实际上,这意味着未导出标识符的别名是无用的,可能会被完全禁止。
  • 嵌入问题 AFAIK 尚未解决。 在#17746 中有一个完整的讨论,我希望如果/何时/在决定继续使用别名之前,这个讨论将继续进行(但仍然有替代解决方案的可能性或决定不将逐步修复作为目标根本)

@iand ,关于“只有同时使用新旧客户端的客户端才会崩溃”,这是唯一有趣的案例。 混合客户端使其成为渐进式代码修复。 仅使用新代码或仅使用旧代码的客户端今天仍可使用。

还有一些其他需要考虑的事情,我还没有在其他地方看到提到过:

由于这里的一个明确目标是允许在大型分散式代码库中进行大型渐进式重构,因此在某些情况下,库所有者想要进行某种清理,这将需要未知数量的客户端来更改其代码(在最终的“停用旧的 API”步骤)。 一种常见的方法是添加弃用警告,但 Go 编译器没有任何警告。

如果没有任何类型的编译器警告,库所有者如何确信完成重构是安全的?

一个答案可能是某种版本控制方案——它是带有新的不兼容 API 的库的新版本。 在这种情况下,版本控制可能是完整的答案,而不是类型别名。

或者,如何允许库作者添加“弃用警告”,这实际上会导致客户端编译 _error_,但使用显式算法进行重构,他们需要执行? 我在想象这样的事情:

Error: os.time is obsolete, use time.time instead. Run "go upgrade" to fix this.

对于类型别名,我猜重构算法只是“用 NewType 替换 OldType 的所有实例”,但可能有细微之处,我不确定。

无论如何,这将允许库作者尽最大努力警告所有客户他们的代码即将被破坏,并在完全删除旧 API 之前为他们提供一种简单的方法来修复它。

@iainmerrick有这些漏洞:golang/lint#238 和 golang/gddo#456

解决渐进式代码修复问题,如@rsc的文章中所述,减少到需要一种使两种类型可以互换的方法(因为 vars、funcs 和 consts 存在变通方法)。

这需要工具或语言更改。

由于使两种类型可互换,根据定义,改变语言的工作方式,任何工具都将是一种在编译器之外模拟等效性的机制,可能是通过将旧类型的所有实例重写为新类型。 但这意味着这样的工具必须重写您不拥有的代码,例如使用 golang.org/x/net/context 而不是 stdlib 上下文包的供应商包。 更改的规范要么必须在单独的清单文件中,要么必须在机器可读的注释中。 如果您不运行该工具,则会出现构建错误。 这一切都变得一团糟。 似乎一个工具会产生和它解决的一样多的问题。 这仍然是使用这些包的每个人都必须处理的问题,尽管由于一部分是自动化的,因此会更好一些。

如果语言改变了,代码只需要由维护者修​​改,对于大多数人来说,事情就可以了。 帮助维护者的工具仍然是一种选择,但它会简单得多,因为源是规范,并且只有包的维护者需要调用它。

正如@griesemer指出的(我不记得在哪里,有很多关于这个的线程)Go 已经有别名,比如byteuint8 ,当你导入一个包时两次,使用不同的本地名称,放入同一个源文件中。

在语言中添加一种显式别名类型的方法只是允许我们使用已经存在的语义。 这样做可以以可管理的方式解决实际问题。

语言更改仍然是一件大事,很多事情需要解决,但我认为在这里做这件事最终是正确的。

据我所知,一个“房间里的大象”是这样一个事实,即对于类型别名,引入它们将允许非临时(即“非重构”)使用。 我已经看到了顺便提到的那些(例如,“在不同的包中重新导出类型标识符以简化 API”)。 保持先前提案的良好传统,请在“影响”小节下列出所有已知的类型别名的替代用法。 这也应该带来好处,即激发人们的想象力,以发明其他可能的替代用途,并在当前的讨论中将它们引入。 就像现在一样,该提案似乎假装作者完全不知道类型别名的其他可能用途。 此外,关于再导出,Rust/OCaml 可能对它们的工作方式有一些经验。

附加问题:请澄清类型别名是否允许向新包中的类型添加方法(可以说是破坏封装)? 另外,新包是否可以访问旧结构的私有字段?

附加问题:请澄清类型别名是否允许向新包中的类型添加方法(可以说是破坏封装)? 另外,新包是否可以访问旧结构的私有字段?

别名只是类型的另一个名称。 它不会改变类型的包。 所以不要回答你的两个问题(除非新包==旧包)。

@akavel截至目前,根本没有任何提案。 但是我们确实知道在 Go 1.8 别名试验期间出现的两个有趣的可能性。

  1. 别名(或只是类型别名)可以创建扩展其他包的插入式替换。 例如,请参阅https://go-review.googlesource.com/#/c/32145/ ,尤其是提交消息中的解释。

  2. 别名(或只是类型别名)可以将一个包结构化为一个小的 API 表面,但是一个大的实现是一个包的集合,以获得更好的内部结构,但仍然只提供一个供客户端导入和使用的包。 在https://github.com/golang/go/issues/16339#issuecomment -232813695 中描述了一个有点抽象的例子。

别名的潜在目标很棒,但听起来我们对重构代码的目标仍然不太诚实,尽管它是该功能的第一动力。 一些建议建议锁定名称,我还没有看到它提到类型通常也会通过这种重构来改变它们的表面。 甚至在别名中经常提到的os.Error => error的例子也忽略了os.Error有一个String方法而不是Error的事实。 如果我们只是移动类型并重命名它,则无论如何都会破坏所有错误处理代码。 这在重构过程中很常见。旧方法被重命名、移动、删除,我们不希望它们在新类型中,因为它会保留与新代码的不兼容。

为了提供帮助,这里有一个种子想法:如果我们根据适配器而不是别名来看待问题会怎样? 适配器会给现有类型一个替代名称_和接口_,并且它可以在以前看到原始类型的地方不加修饰地使用。 适配器需要明确定义它支持的方法,而不是假设存在与底层适配类型相同的接口。 这很像现有的type foo bar行为,但有一些额外的语义。

io.ByteBuffer

例如,这是一个解决io.ByteBuffer情况的示例框架,暂时使用临时“adapts”关键字:

type ByteBuffer adapts bytes.Buffer

func (old *ByteBuffer) Write(b []byte) (n int, err error) {
        buf := (*bytes.Buffer)(old)
        return buf.Write(b)
}

(... etc ...)

因此,使用该适配器后,此代码将全部有效:

func newfunc(b *bytes.Buffer) { ... }
func oldfunc(b *io.ByteBuffer) { ... }

func main() {
        var newvar bytes.Buffer
        var oldvar io.BytesBuffer

        // New code using the new type obviously just works.
        newfunc(&newvar)

        // New code using the old type receive the underlying value that was adapted.
        newfunc(&oldvar)

        // Old code using the old type receive the adapted value unchanged.
        oldfunc(&oldvar)

        // Old code gets new variable adapted on the way in. 
        oldfunc(&newvar)
}

newfuncoldfunc是兼容的。 两者实际上都接受*bytes.Bufferoldfunc *io.BytesBuffer在进入的过程中将其调整为

操作系统错误

同样的逻辑可能也适用于接口,尽管它的编译器实现有点棘手。 这是os.Error => error的示例,它处理方法已重命名的事实:

package os

type Error adapts error

func (e Error) String() string { return error(e).Error() }

但是,这种情况需要进一步考虑,因为方法例如:

func (v *T) Read(b []byte) (int, os.Error) { ... }`

将返回一个具有String方法的类型,因此我们通常希望以相反的方向进行调整,以便可以逐渐修复代码。

_更新:需要进一步思考。_

嵌入问题

就将该功能拖出 1.8 的嵌入错误而言,适配器的结果更加清晰,因为它们不仅仅是同一事物的新名称:如果嵌入了适配器,则使用的字段名称是适配器如此旧的逻辑仍然有效,并且访问该字段将使用适配器接口,除非明确地传递到采用基础类型的上下文中。 如果嵌入了未适应的类型,则通常会发生。

Kubernetes,码头工人

帖子中所述的问题似乎是上述问题的变体,并已通过提案解决。

变量,常量

在这种情况下调整变量或常量没有多大意义,因为我们无法真正将方法与它们直接关联。 他们的类型会被适应或不适应。

戈多克

我们会明确说明事物是一个适配器,并像往常一样显示它的文档,因为它包含一个独立于适配事物的接口。

句法

请挑选一些好东西。 ;)

@iainmerrick @zombiezen

我们是否也应该自动推断别名类型是遗留类型并且应该被新类型替换? 如果我们强制执行 golint、godoc 和类似工具来将旧类型可视化为已弃用,它将非常显着地限制类型别名的滥用。 别名功能被滥用的最终问题将得到解决。

两个观察:

1. 类型引用的语义取决于支持的重构用例

Gustavo 的提议表明,需要在类型引用的用例和由此产生的语义上做更多的工作。

罗斯的新提议包括一个新的语法type OldAPI = newpkg.newAPI 。 但是语义是什么? 用遗留的公共方法或字段扩展 OldAPI 是不可能的吗? 假设 yes 作为答案,要求 newAPI 支持 OldAPI 的所有公共方法和字段以保持兼容性。 请注意,假设修改包的可见性约束不在表中,则必须重写依赖于私有字段和方法的 OldAPI 包中的任何代码以仅使用公共 newAPI。

替代路径是允许为 OldAPI 定义其他方法。 这可以减轻 NewAPI 提供所有公共旧方法的负担。 但这会使 OldAPI 成为与 NewAPI 不同的类型。 必须维护两种类型值之间的某种形式的可赋值性,但规则会变得复杂。 允许添加字段会导致更加复杂。

2. NewAPI的包不能导入OldAPI的包

OldAPI 的重新定义要求包含 OldAPI 定义的包 O 使用 NewAPI 导入包 N。 这意味着包 N 不能导入 O。也许它是如此明显以至于没有被提及,但在我看来它是重构用例的一个重要约束。

更新:包 N 不能依赖包 O。例如,它不能导入导入 O 的包。

@niemeyer诸如重命名方法之后台调用旧方法(反之亦然),b) 逐渐将所有用户更改为新方法,c) 删除旧方法。 您可以将其与类型别名结合使用。 之所以关注类型移动,是因为这是唯一确定的事情,目前还不可能。 所有其他已识别的更改都是可能的,即使它们可能使用多个步骤(例如,更改方法的参数集而不重命名)。 我相信选择表面积较小(需要理解的东西较少)的修复程序是可取的。

@rakyll 就个人而言,如果我认为别名对非重构的东西有用(比如包装器包,我发现它是一个很好的用例),我只会使用它们,弃用警告是该死的。 我会对人为地削弱它们并使它们对我的用户感到困惑的人很生气,但我不会气馁。

我认为在某些时候需要辩论我们是否真的考虑包装器包、protobuf 公共导入或公开内部包 API 这样一件坏事(而且我不知道如何最好地辩论这种主观的东西,而没有一方只是重复一遍又一遍,他们是不可读的,另一个说“不,他们不是”。在我看来,这里没有很多客观的论点)。

我至少(显然)认为它们是一件好事,而且我也认为添加语言功能并人为地将其限制为仅用于一个用例是一件坏事; 正交的、设计良好的语言允许您使用尽可能少的功能做尽可能多的事情。 您希望您的功能尽可能地扩展“可能程序的跨度向量空间”,因此添加一个仅向空间添加一个点的功能对我来说似乎很奇怪。

在开发任何类型别名提案时,我希望记住另一个略有不同的用例。

尽管我们在本期讨论的主要用例是类型 _replacement_,但类型别名对于使代码体摆脱对类型的依赖也非常有用。

例如,假设一个类型被证明是“不稳定的”(即它不断被改变,可能是不兼容的方式)。 然后它的一些用户可能想要迁移到“稳定”的替换类型。 我正在考虑在 github 等上进行开发,其中类型的所有者及其用户不一定密切合作或就稳定性目标达成一致。

其他示例是单一类型是唯一阻止删除对大型或有问题的包的依赖的情况,例如发现许可证不兼容的情况。

所以这里的过程是:

  1. 定义类型别名
  2. 更改相关代码体以使用类型别名
  3. 用类型定义替换类型别名。

在这个过程结束时,将有两种独立的类型,它们可以自由地朝着自己的方向发展。

请注意,在此用例中:

  • 无法更改包含原始类型定义的包以在那里添加类型别名(因为所有者不太可能同意这一点)
  • 原始类型没有被弃用(尽管在“断奶”类型的过程中,它可能会在代码体中被视为这样)。

@Merovius在您删除或重命名旧方法的那一刻,您会立即杀死使用它的每个客户端。 如果您愿意这样做,那么添加语言功能以防止一次性损坏的整个重要练习是没有实际意义的。 对于移动代码,我们不妨说完全相同的事情:只需一次重命名每个调用站点上的类型。 完毕。 这两个操作都是简单的原子重命名,它们的共同点是它们假定完全访问调用站点中的每一行代码。 谷歌可能就是这种情况,但作为大型开源应用程序和库的维护者,这不是我生活的世界。

在大多数情况下,我发现这种批评是不公平的,因为 Go 团队不遗余力地使项目对外部各方具有包容性,但是当您假设您可以访问调用给定包的每一行代码时,这就是一个围墙与开源社区的上下文不匹配的花园。 至少可以说,添加仅适用于围墙花园内的语言级重构功能是不典型的。

@niemeyer我显然没有说清楚。 在任何情况下,我都不主张删除旧的 API,我只是指出,我们想要使用类型别名启用的任何工作流程都已经可以使用重命名方法(无论是否同时进行)。 所以,无论你想做什么,为了

  1. 添加新 API,可与旧 API 互换
  2. 逐渐将消费者转变为新的 API
    3a. 一旦一切都迁移或弃用期用完,请删除旧的 API
    3b. 通过永久保留这两个 API 来提供无限的稳定性(例如参见文章的这一部分

你似乎在争论做 3a 还是 3b。 但是我指出的是, 1. 已经可以用于方法名称,但不能用于类型,这就是它的意义所在。

不过,我现在意识到我想我误解了你 :) 你可能已经指出,os.Error 是不同的接口定义,所以这个举动并没有真正成功。 我认为这是真的; 如果您禁止删除 API,类型别名将无法重命名接口类型的方法。

也许你可以为我澄清一些关于你的适配器想法的事情:这不是也允许使用(例如,在 os.Error 情况下)任何 fmt.Stringer 作为 os.Error 吗?

无论如何,适配器的想法似乎值得进一步发展,即使我对此略有怀疑。 但是有一种方法可以在不破坏可能的实现者和/或消费者的情况下逐步重构接口是一个很好的目标。

@niemeyer是的,您也提出了一个关于错误更改方法名称的好观点。 这引入了许多复杂情况,这不是我在这里要解决的问题。 因为只有一小部分提到 error/os.Error 的代码实际上调用了该方法,所以移动是比方法更改更痛苦的部分。 我认为我们可以将方法重命名视为一个独立于更改代码位置的问题。 如果这一举措发生在今天,我们可以无缝地进行包重组,但仍然坚持使用旧的方法名称,那仍然是重大的进步。 将这个问题集中在代码位置上是为了尝试简化。

我同意,如果有一些通用修复可以处理这两种变化,那就太好了。 我不明白那个修复是什么。 特别是我不明白类型切换如何与您描述的适配器一起工作:在类型切换期间值是否会以某种方式自动转换? 反射呢? 只有一种具有两个名称的类型可以避免因具有两种自动来回转换的类型而出现的许多问题。

@rsc是的,适配器会在每种情况下始终自动转换,因此类型开关也不interface{}

@Merovius我上面的两条评论正是您仍在提出的观点。 如果你今天移动一个类型,你会破坏需要修复的代码。 如果重命名方法,则会破坏需要修复的代码。 如果你删除一个方法,改变它们的参数,你就会破坏需要修复的代码。 在任何这些情况下重构代码时,需要在每个调用站点中的损坏情况下以原子方式完成修复,以便事情继续工作。 允许类型移动但完全不受影响是重构的一种非常有限的情况,IMO 不能证明语言功能的合理性。

@niemeyer那将处理具体类型。 .(interface{String() string}).(interface{Error() string})的类型断言或任何特定的接口部分发生了什么变化? 检查是否必须以某种方式考虑两种可能的基础类型?

@niemeyer否。可以非原子地重命名方法。 例如,将方法从A.FooA.Bar ,请执行

  1. 添加方法A.Bar作为A.Foo的包装器
  2. 将用户迁移到通过任意多次提交仅调用A.Bar
  3. 要么删除A.Foo ,要么不删除,这取决于您是否愿意强制弃用。

可以非原子地更改函数参数。 例如添加一个参数x intfunc Foo() ,做

  1. 添加func FooWithInt(x int) { Foo(); // use x somehow; }
  2. 通过任意多次提交迁移用户添加参数
  3. 如果您不愿意强制弃用(或者您不为拥有 WithInt 所困扰),那么您就完成了。 否则将 Foo 修改为func Foo(x int) { FooWithInt(x) }
  4. 通过任意多次提交使用s/FooWithInt/Foo/g迁移用户。
  5. 删除FooWithInt

除了移动类型(以及严格来说,vars)都适用。 不需要原子性。 您要么在强制弃用时破坏兼容性,要么不这样做,但这与原子性完全正交。 使用两个不同的名称来指代同一个事物的能力是,什么允许您在进行基本任意更改时避开原子性,并且您在除类型之外的所有情况下都具有这种能力。 是的,要采取实际行动,而不是修改,您需要愿意强制弃用(因此打破潜在未知代码的构建,这意味着这需要广泛而及时的公告)。 但即使您不是,使用更方便的名称或其他有用的包装(参见 x/image/draw)来扩充 API 的能力取决于通过新名称引用旧事物的能力,反之亦然。

今天移动类型和今天重命名函数之间的区别在于,在前一种情况下,您实际上需要进行原子更改,而对于后者,您可以通过独立的回购和提交逐步进行更改。 不是作为“我将进行 s/Foo/Bar/ 的提交”,而是有一个过程可以做到。

反正。 显然,我不知道我们在哪里,彼此交谈。 我发现@rsc的文档很清楚地传达了我的 POV,并没有真正

@rsc我可以看到两个合理的答案。 接口携带进入的类型,适配器或其他类型的简单一种,并且在接口断言时应用通常的语义。 另一个是,如果该值不满足接口但底层值满足,则该值可能是未适配的。 前者更简单,可能足以满足我们所想到的重构用例,而后者可能更符合我们也可以将其类型断言到底层类型的想法。

@Merovius当然,只要_您实际上没有重命名它_并强制调用站点改用新的 API,就可以重命名方法。 同样,只要_您实际上不移动它,_并强制调用站点改用新的 API,就可以移动类型。 多年来,我们都一直在做这两件事,以保持旧代码的工作。

@niemeyer但同样:对于类型,您甚至无法以体面的方式添加内容。 请参见 x/图像/绘图。 并不是每个人都对稳定有如此绝对的看法; 我,我自己,可以说“在 6,12,... 个月内 $function,$type,... 将消失,请确保您在那个时候从它迁移”,然后只是破坏未维护的代码设法遵循弃用通知(如果有人认为他们需要对 API 的长期支持,他们肯定可以找人付费提供)。 我什至声称大多数人对稳定性没有那种绝对的看法。 查看最近对语义版本的推动,这只有在您确实想要打破兼容性的选项时才真正有意义。 并且该文档很好地论证了,即使在那种情况下,您如何仍能从逐步修复的能力中获益,以及它如何缓解(如果不能从根本上解决钻石依赖性问题)。

由于您对稳定性的立场是绝对的,因此您可能会忽略大多数别名的使用案例以进行逐步修复。 但我认为,对于大多数围棋社区来说,这是不同的,他们需要破损,并在它们确实发生时使它们尽可能顺利地使用。

@niemeyer @rsc @Merovius我一直在关注你的讨论(以及整个讨论),我想在这篇文章的中间公然抨击这篇文章。

我们对这个问题迭代得越多,我们就越接近某种形式的扩展协方差语义。 所以,这里有一个想法:我们已经定义了从具体类型到接口以及接口之间的子类型语义(“is-a”)。 我的建议是使接口递归协变(根据当前的方差规则)到它们的方法参数。

这并不能解决所有当前包的问题。 但它可以解决所有未来尚未编写的包的问题,​​因为 API 的“可移动部分”可以是接口(也鼓励良好的设计)。

我认为我们可以通过(ab)以这种方式使用接口来解决所有需求。 我们正在打破 Go 1.0 吗? 我不知道,但我认为我们不是。

@thwd我认为您需要通过“使接口递归协变”来更准确地定义您的意思。 通常,在子类型化中,方法参数需要以逆变方式改变,结果以协变方式改变。 此外,根据您的说法,这不会解决具体(非接口)类型的任何现有问题。

@thwd我不同意,接口(甚至是协变的)是解决这些问题中的任何一个的好方法(仅适用于它的非常具体的实例)。 要使它们合而为一,您需要将 API 中的所有内容都设为接口(因为您永远不知道在某个时候您可能想要移动/更改什么),包括 vars/consts/funcs/... 我不认为总而言之,这是一个很好的设计(我已经在 J​​ava 中看到了这一点。这让我很恼火)。 如果某物是结构体,只需将其设为结构体即可。 其他一切只会在你的包和每个反向依赖中增加奇怪的语法开销,几乎没有任何好处。 这也是开始时保持清醒的唯一方法; 从简单开始,稍后转向更一般的内容。 到目前为止,我所看到的 API 中的许多复杂情况都来自人们过度思考 API 设计和规划比以往任何时候都需要的通用性方式。 然后,在 80%(这个数字显然是谎言)的情况下,根本没有任何反应,因为没有“干净的 API 设计”。

(要明确:我并不是说协变接口不是一个好主意。我只是说它们不是解决这些问题的好方法)

为了补充@Merovius的观点,我看到的许多渐进式代码修复都采取了将普遍有用的非接口类型从更大的包中移出的形式。 考虑以下:

package foo

type Authority struct {
  Host string
  Port int
}

随着时间的推移, foo 包会增长,并且它最终获得了比只需要Authority类型的人真正想要的更多的责任(和代码大小)。 因此,有一种方法可以创建一个只包含Authorityfooauthority包,并且让foo.Authority现有用户仍然工作,这是一个理想的用例。 请注意,任何仅考虑接口类型的解决方案在这里都无济于事。

@Merovius你的最后一条评论完全是主观的,是针对我个人而不是我的建议。 这不会有好的结局,所以我将在此停止讨论。

@griesemer @Merovius我同意你们俩的看法。 那么,为了结束循环,我们可以同意,到目前为止的讨论已经将我们引向了一些子类型/协方差的概念。 此外,它的任何实现都不应导致运行时间接。 这就是@niemeyer所提议的(如果我理解他的话)。 但我很想阅读更多的想法。 我也会考虑这个问题。

@niemeyer @Merovius的评论中没有任何 _ad hominem_。 他声称“你对稳定的立场是绝对的”是对你的立场的观察,而不是你,并且是从你的一些陈述中得出的合理推论,比如

删除或重命名旧方法的那一刻,就会立即杀死使用它的每个客户端。

当然,只要您不实际重命名方法并强制调用站点使用新的 API,就可以重命名方法。 同样,只要您不实际移动类型,就可以移动类型,并强制调用站点改用新的 API。 多年来,我们都一直在做这两件事,以保持旧代码的工作。

我从这些陈述中得到了与 Merovius 相同的印象——你不会同情某事暂时弃用,然后最终将其删除; 你致力于让代码在野外无限期地工作; “你对稳定的立场是绝对的”。 (为了避免进一步的误解,我用“你”来指代你的想法,而不是你的个性。)

@niemeyer您建议的adapts声明似乎与 Haskell 类型类中的instance密切相关。 粗略地将其翻译为 Go,它可能看起来像:

package os

type Error interface {
  String() string
}

instance error Error (
  func (e error) String() string { return e.Error() }
)

不幸的是(正如@zombiezen 所指出的),目前尚不清楚这对非接口类型有何帮助。

我也不清楚它如何与函数类型(参数和返回值)交互; 例如, adapts的语义如何帮助将Context迁移到标准库?

我从这些陈述中得到了与梅罗维乌斯相同的印象——你对暂时贬低某些东西并不同情

@jba这些是绝对的事实,而不是绝对的意见。 如果您删除一个方法或一个类型,使用它的 Go 代码会中断,因此这些更改需要以原子方式完成。 不过,我的建议是关于代码的逐步重构,这是这里的主题并暗示弃用。 然而,这种弃用的过程不是同情的问题。 我有多个公共 Go 包,每个包都有数以千计的依赖项,并且由于这种逐渐演变,我有多个独立的 API。 当我们破坏 API 时,如果我们希望不会让人们发疯,最好分批进行此类破坏,而不是流式传输它们。 当然,除非您住在有围墙的花园中,并且可以联系每个呼叫站点进行修复。 但我在重复我自己......所有这些都可以在上面的原始提案中以更清晰的方式阅读。

@Merovius

就个人而言,如果我认为别名对非重构的东西有用(比如包装器包,我认为这是一个很好的用例),我只会使用它们,弃用警告是该死的。

我们维护包含大量新 API 和已弃用 API 的包,并且在没有对旧(别名)类型的状态进行明确解释的情况下使用别名不会帮助逐步修复代码,只会导致增加的 API 表面的压倒性。 我同意@niemeyer 的观点,即我们的解决方案需要满足分布式开发人员社区的需求,该社区目前没有任何其他信号,只有自由格式的 godoc 文本表明 API 已“弃用”。 添加语言功能来帮助弃用旧类型是该线程的主题,因此它自然会导致旧(别名)类型的状态是什么的问题。

我很乐意在不同的主题下讨论类型别名,例如提供对类型或部分包的扩展,但不在此线程上。 在考虑之前,该主题本身有各种特定于封装的问题需要解决。

一个特定的操作符或暗示别名类型被某种程度的替换可能是健康的,可以向用户传达他们需要切换的信息。 这种可区分性将使工具能够自动报告被替换的 API。

需要明确的是,对于标准库之外的类型,弃用政策在技术上是不可能的。 从别名包的角度来看,类型只是旧的。 鉴于我们永远无法在生态系统中强制执行此操作,我仍然希望看到标准库别名严格暗示弃用(通过适当的弃用通知暗示)。

我还建议我们在并行讨论中标准化弃用的概念,并在我们的核心工具(golint、godoc 等)中为它们提供支持。 缺少弃用通知是 Go 生态系统中最大的问题,并且比逐步修复代码的问题更为普遍。

@rakyll我对使用计算机可读的弃用通知表示同情; 我只是反对 a) 别名和 b) 将它们作为编译器警告发出的概念。

对于 a),除了我想有效地将​​别名用于移动以外的其他事情之外,它也仅适用于一小部分弃用。 例如,假设我想在几个版本中从函数中删除一些参数; 我不能使用别名,真的,因为新 API 的签名会有所不同,但我仍然想宣布这一点。 对于 b),恕我直言编译器警告普遍不好。 我认为这主要与 go 已经在做的事情一致,所以我认为这不需要理由。

我同意您所说的关于弃用通知的所有内容。 显然,已经有一个语法:#10909,所以下一步让它更有用是通过在 godoc 中突出显示它们来增强工具支持,并检查警告它们的使用(比如 go vet、golint 或一个单独的工具)。

@rakyll我同意 stdlib 应该从保守使用类型别名开始,如果它们被引入。


侧边栏:

那些不知道 Go 和相关工具中弃用评论状态的人的背景,因为它相当分散:

正如@Merovius上面提到的,有一个标准约定将项目标记为已弃用,#10909,请参阅https://blog.golang.org/godoc-documenting-go-code

TL;DR:在已弃用项目的文档中创建一段以“已弃用:”开头的段落,并解释替换内容。

godoc 有一个被接受的提议,以更有用的方式显示已弃用的项目:#17056。

@rakyll建议 golint 在使用不推荐使用的项目时发出警告:golang/lint#238。


即使 stdlib 对在 stdlib 中使用别名采取保守立场,我也不认为类型别名的存在意味着(以任何机械检测或视觉指示的方式)旧类型已被弃用,即使如果它总是意味着在实践中。

这样做将意味着以下之一:

  • 扫描其他 stdlib 包以查看是否有任何类型(未明确标记为已弃用)在其他地方使用别名
  • 将所有 stdlib 别名硬编码到自动化工具中
  • 仅在您已经查看其替代品时报告旧类型已弃用,这无助于发现

当由于旧类型已被弃用而引入类型别名时,需要通过将旧类型标记为已弃用来处理,并引用新类型,无论如何。

这通过允许它更简单和更通用来使更好的工具存在:它不需要特殊情况,甚至不需要知道类型别名:它只需要匹配文档注释中的“已弃用:”。

stdlib 中的别名仅用于弃用的官方(如果可能是临时的)政策是好的,但只能使用标准弃用注释并禁止其他用途使其通过代码审查来强制执行。

@niemeyer我之前的回复因断电而丢失:( 乱序:

但我在重复自己..

FWIW,我发现你最后的回复很有帮助。 它使我确信,我们比以前看起来(而且在您看来仍然如此)更加一致。 不过,似乎在某处仍然存在误传。

不过,我的建议是关于代码的逐步重构

我认为这是没有争议的。 :) 我从一开始就同意,你的提议是一个有趣的替代方案,可以考虑解决这个问题。 令我困惑的是这样的陈述:

如果您删除一个方法或一个类型,使用它的 Go 代码会中断,因此这些更改需要以原子方式完成。

我仍然想知道你在这里的推理是什么。 我理解原子性单位是一次提交。 有了这个假设,我就是不明白为什么您确信删除方法或类型不能首先在依赖存储库中单独的、任意数量的提交中发生,然后,一旦不再有明显的用户(并且大量弃用)间隔已过)该方法或类型在上游提交中被删除(不会破坏任何东西,因为没有人再依赖了)。 我同意围绕反向依赖项存在一定的模糊因素,这些因素不符合弃用原则,或者您无法找到(或合理修复),但对我而言,这似乎与手头的问题基本无关; 每当您应用重大更改时,无论您如何尝试编排它,您都会遇到这个问题。

而且,公平地说:像这样的句子并没有真正帮助混淆

当然,除非您住在有围墙的花园中,并且可以联系每个呼叫站点进行修复。

如果我说的任何话给你的印象是这就是我要争论的观点,我希望你能退后一步,也许重新阅读它,假设我完全站在开源的立场上争论社区(如果你不相信我,请随意查阅我以前对这个主题的贡献;我总是第一个指出这更像是一个社区问题,而不是一个 monorepo 问题。Monorepos 有办法解决这个问题,正如你所指出的)。

反正。 我觉得这和你一样令人筋疲力尽。 不过,我希望我能在某个时候理解你的立场。

同时讨论是否以及如何支持诸如 protobuf 公共导入之类的东西......
我认为在某些时候需要辩论我们是否真的认为包装器包、protobuf 公共导入或公开内部包 API 是一件坏事

nit:我认为不需要将 protobuf 公共导入作为特殊的次要用例提及。 它们是为渐进式代码修复而设计的,正如内部设计文档甚至公共文档中明确提到的那样,因此它们已经属于此问题所描述的问题的范围。 另外,我相信类型别名足以实现 protobuf 公共导入。 (proto 编译器生成变量,但它们在逻辑上是常量,因此“var Enum_name =imported.Enum_name”应该就足够了。)

@Merovius感谢您的富有成效的回应。 让我尝试提供一些上下文:

我仍然想知道你在这里的推理是什么。 我理解原子性单位是一次提交。 有了这个假设,我简直不明白为什么您确信方法或类型的删除不能首先单独发生,

从来没有说过这不可能发生。 让我退后一步,更清楚地重述一遍。

我们可能都同意最终目标有两个:我们想要可以工作的软件,我们想要改进软件,以便我们可以继续以一种理智的方式工作。 后者中的一些正在打破变化,使其与前一个目标不一致。 所以存在紧张,这意味着甜蜜点所在有一些主观性。 我们辩论的有趣部分就在这里。

寻找最佳位置的一种有用方法是考虑人为干预。 也就是说,一旦你做了一些需要人们手动修改代码以使其正常工作的事情,惯性就会发生。 所有依赖代码库的相关部分都需要很长时间才能完成这个过程。 我们要求忙碌的人做在大多数情况下他们不想打扰的事情。

另一种看待这个甜蜜点的方法是工作软件的可能性。 我们要求人们不要使用已弃用的方法的次数并不重要。 如果它很容易访问并且现在解决了他们的问题,那么大多数开发人员只会使用它。 这里常见的反驳是:_哦,但是当它坏了就是他们的问题!_但这与既定的目标背道而驰:我们想要工作软件,而不是正确的。

因此,希望这可以让我们更深入地了解为什么简单地移动一个类型似乎没有帮助。 为了让人们在新家中实际使用这种新型,我们需要人工干预。 当人们克服手动更改代码的麻烦时,最好进行干预以_使用新类型_,而不是在即将到来的将来很快再次更改的东西。 如果我们确实解决了添加语言功能以帮助重构的麻烦,那么理想情况下,它会允许人们逐渐将他们的代码_转移到新类型,_而不是简单地转移到新家,原因如上所述。

感谢您的解释。 我想我现在更了解您的立场并同意您的假设(即,无论如何人们都会使用已弃用的东西,因此提供任何可能的帮助来指导他们进行替换是最重要的)。 FWIW,我处理这个问题的天真计划(无论我们将采用哪种逐步修复的解决方案)是一个类似 go-fix 的工具,可以在弃用期间自动逐包迁移代码,但我坦率地承认帽子我还没有尝试在实践中如何以及是否有效。

@niemeyer我不相信您的建议在不严重破坏 Go 类型系统的情况下可行。

考虑一下这段代码带来的困境:

package old
import "new"
type A adapts new.A
func (a A) NewA() {}

package new
type A struct{}
func (a A) OldA() {}

package main
import (
    "new"
    "old"
    "reflect"
)
func main() {
    oldv := reflect.ValueOf(old.A{})
    newv := reflect.ValueOf(new.A{})
    if oldv.Type() == newv.Type() {
        // The two types are equal, therefore they must
        // have exactly the same method set, so either
        // oldv doesn't have the OldA method or newv doesn't
        // have the NewA method - both of which imply a contradiction
        // in the type system.
    } else {
         // The two types are not equal, which means that the
         // old adapted type is not fully compatible with the old
         // one. Any type that includes either new.A or new.B will
         // be incompatible as one of its components will likewise be
         // unequal, so any code that relies on dynamic type checking
         // will fail when presented with the type that's not using the
         // expected version.
    }
 }

反射包的当前公理之一是,如果两种类型相同,则它们的reflect.Type 值相等。 这是 Go 的运行时类型转换效率的基础之一。 据我所知,没有办法在不破坏这一点的情况下实现“adapts”关键字。

@rogpeppe请参阅上面与@rsc关于反射的对话。 这两种类型不一样,所以在被问到时,reflect 只会说实话并提供适配器的详细信息。

@niemeyer如果这两种类型不一样,那么我认为我们不能在包之间移动类型时支持渐进式代码修复。 例如,假设我们想要制作一个保持类型兼容性的新图像包。

我们可能会这样做:

package newimage
import "image"
type RGBA adapts image.RGB
func (r *RGBA) At(x, y) color.Color {
    return (*image.Buffer)(r).At(x, y)
}
etc for all the methods

鉴于逐步修复代码的目标,我认为可以合理地期望
在新包中创建的图像与现有功能兼容
使用旧的图像类型。

为了论证起见,让我们假设 image/png 包有
已转换为使用 newimage 但 image/jpeg 尚未转换。

我相信我们应该期望这段代码能够工作:

img, err := png.Decode(r)
if err != nil { ... }
err = jpeg.Encode(w, img, nil)

但是,由于它对 *image.RGBA 进行了类型断言,而不是 *newimage.RGBA,
它会失败 AFAICS,因为类型不同。

假设我们使上面的类型断言成功,类型是否为*image.RGBA
或不。 这将打破当前的不变量:

reflect.TypeOf(x) == reflect.TypeOf(x.(anyStaticType))

也就是说,使用静态类型断言不会仅仅断言 a 的静态类型
价值,但有时它实际上会改变它。

假设我们认为这没问题,那么大概我们还需要
可以将适配类型转换为任何与其兼容的接口
适配类型支持,否则新代码或旧代码都会停止
在转换为兼容的接口类型时工作
他们正在使用的类型。

这导致了另一种矛盾的情况:

// oldInterface is some interface with methods that
// are only supported by the old type.
type oldInterface interface {
    OldMethod()
}
var x = interface{} = newpackage.Type{}
switch x.(type) {
case oldInterface:
    // This would fail because the newpackage.Type
    // does not implement OldMethod, even though we
    // we just supposedly checked that x implements OldMethod.
    reflect.TypeOf(x).Method("OldMethod")
}

总的来说,我认为有两种相同但不同的类型
会导致非常难以解释的类型系统和意外的不兼容性
在使用动态类型的代码中。

我支持“类型 X = Y”的提议。 解释起来很简单,不
过多地破坏类型系统。

@rogpeppe :我相信@niemeyer的建议是将适配类型隐式转换为其基类型,类似于@josharian早期建议

为了逐步重构,它还必须隐式转换具有适应类型参数的函数; 从本质上讲,它需要为语言添加协方差。 这当然不是不可能完成的任务——许多语言确实允许协变,特别是对于具有相同底层结构的类型——但它确实给类型系统增加了很多复杂性,特别是对于接口类型

正如您所指出的,这确实会导致一些有趣的边缘情况,但它们本身并不一定是“矛盾的”:

type oldInterface interface {
    OldMethod()
}
var x = interface{} = newpackage.Type{}
switch y := x.(type) {
case oldInterface:
    reflect.TypeOf(y).Method("OldMethod")  // ok
    reflect.TypeOf(x).Method("NewMethod")  // ok

    // This would fail because y has been implicitly converted to oldInterface.
    reflect.TypeOf(y).Method("NewMethod")

    // This would fail because accessing OldMethod on newpackage.Type requires
    // a conversion to oldInterface.
    reflect.TypeOf(x).Method("OldMethod")
}
// This would fail because accessing OldMethod on newpackage.Type requires
// a conversion to oldInterface.

这对我来说仍然是矛盾的。 当前模型是一个非常简单的模型:接口值具有定义良好的底层静态类型。 在上面的代码中,我们推断出有关该底层类型的一些信息,但是当我们查看该值时,它看起来不像我们推断的那样。 在我看来,这是对语言的严重(且难以解释)变化。

这里的讨论似乎要结束了。 根据@egonelbrehttps://github.com/golang/go/issues/16339#issuecomment -247536289 中的建议,我更新了原始问题评论(在顶部)以包含讨论的链接摘要,因此远的。 每次我更新摘要时,我都会发布一条新评论,就像这样。

总的来说,这里的情绪似乎是针对类型别名而不是广义别名。 Gustavo 的适配器想法可能会取代类型别名,但也可能不会。 目前看起来有点复杂,尽管在讨论结束时可能会达到一个更简单的形式。 我建议继续讨论一段时间。

我仍然不相信可变全局变量“通常是一个错误”(并且在它们是错误的情况下,竞争检测器是找到这种错误的首选工具)。 我要求,如果该参数用于证明缺乏可扩展语法是合理的,则实施 vet-check 来检查 - 例如 - 检查代码中全局变量的分配,而不是 init() 或其声明只能访问。 我天真地认为这并不是特别难实现,并且运行它应该没有太多工作 - 例如 - 所有 godoc.org 注册的包以查看可变全局变量的用例是什么以及我们是否这样做考虑所有这些错误。

(我也想相信,如果 go 增长不可变的全局变量,它们应该是 const 声明的一部分,因为这就是它们的概念,并且因为这将向后兼容,但我承认这可能会导致例如,在数组类型中可以使用哪种表达式的复杂性,并且需要更多思考)

重新“限制?标准库类型的别名只能在标准库中声明。” -- 值得注意的是,这将阻止x/image/draw的插入用例,这是一个已经表示有兴趣使用别名的现有包。 我也可以很好地想象,例如,路由器包等以类似的方式(挥手)使用别名到net/http中。

我也同意反驳的所有限制,即我赞成没有任何限制。

@Merovius ,可变的 _exported_ 全局变量怎么样? 确实,未导出的全局变量可能没问题,因为包中的所有代码都知道如何正确处理它。 不太明显的是,导出的可变全局变量是否有意义。 我们自己在标准库中多次犯过这个错误。 例如,没有完全安全的方法来更新 runtime.MemProfileRate。 您能做的最好的事情是在程序的早期设置它,并希望您导入的任何包都不会启动可能正在分配内存的初始化 goroutine。 你可能对 var 与 const 的看法是对的,但我们可以改天再谈。

关于 x/image/draw 的好点。 将在下次更新时添加到摘要中。

我非常想组装一个具有代表性的 Go 代码语料库,我们可以对其进行分析以回答您提出的问题。 几周前我开始尝试这样做,但遇到了一些问题。 这比看起来应该做的工作要多一些,但拥有该数据集非常重要,我希望我们能做到。

@rsc你关于这个话题的 GothamGo 演讲已经发布在 youtube https://www.youtube.com/watch?v=h6Cw9iCDVcU 上,这将是对第一篇文章的一个很好的补充。

在“类型别名的提案还需要解决哪些其他问题?” 部分指定“可以在以别名命名的类型上定义方法吗?”的答案会很有帮助。 是一个硬不。 我意识到这违背了本节的精神,但我注意到,在很多关于别名的对话中,在这里和其他地方,有些人会立即拒绝这个概念,因为他们相信别名必然会允许这样做,从而导致问题比它解决的要多。 它隐含在定义中,但明确提及它会缩短很多不必要的来回。 尽管这可能属于别名的新提案中的别名常见问题解答,但这应该是该线程的结果。

@Merovius任何导出的包全局可变变量都可以通过包级 getter 和 setter 函数来模拟。

给定版本 n 的包p

package p
var Global = 0

在版本 n+1 中可以引入 getter 和 setter 并且不推荐使用该变量

package p
//Deprecated: use GetGlobal and SetGlobal.
var Global = 0
func GetGlobal() int {
    return Global
}
func SetGlobal(n int) {
   Global = n
}

和版本 n + 2 可以取消导出Global

package p
var global = 0
func GetGlobal() int {
    return global
}
func SetGlobal(n int) {
   global = n
}

(练习留给读者:您还可以将global访问权限包装在 n + 2 的互斥锁中,并弃用GetGlobal()以支持更惯用的Global() 。)

这不是一个快速修复,但它确实减少了问题,因此只有 func 别名(或其当前的解决方法)对于逐步代码修复是绝对必要的。

@rsc您在摘要中遗漏的别名的一个小用法:缩写长名称。 (可能是 Pascal 的唯一动机,它最初没有像包这样的大型编程功能。)虽然它是微不足道的,但它是未导出别名有意义的唯一用例,因此也许值得一提。

@jimmyfrasche你是对的。 我不喜欢使用 getter 和 setter 的想法(就像我不喜欢将它们用于结构字段一样),但您的分析当然是正确的。

关于别名的非修复使用(例如制作插入式替换包)有一点需要说明,但我承认它削弱了 var-aliases 的情况。

@Merovius同意所有观点。 我也不高兴,但必须遵循逻辑v☹v

@niemeyer您能否阐明适配器如何帮助迁移新旧都具有名称相同但签名不同的方法的类型。 向方法添加参数或更改参数的类型似乎是代码库的常见演变。

@rogpeppe请注意,这正是今天发生的情况:

type two one

这使得onetwo独立类型,无论是反射还是在interface{} ,这就是你所看到的。 您还可以在onetwo 。 上面的适配器提议只是使适配器的最后一步自动化。 您可能出于多种原因不喜欢该提案,但这并不矛盾。

@iandtype two one ,这两种类型具有完全独立的方法集,因此匹配名称没有什么特别之处。 在迁移旧代码库之前,它们将继续使用以前类型(现在是适配器)下的旧签名。 使用新类型的新代码将使用新签名。 将新类型的值传递给旧代码会自动适应它,因为编译器知道后者是前者的适配器,因此使用相应的方法集。

@niemeyer似乎这些未完全指定的适配器背后隐藏着很多复杂性。 在这一点上,我认为类型别名的简单性对它们非常有利。 我坐下来列出了仅针对类型别名需要更新的所有内容,这是一个很长的列表。 适配器的列表肯定会更长,我仍然不完全了解所有细节。 如果你想制定一个完整的建议,我想建议我们现在做类型别名,然后再决定相对较重的适配器(但我再次怀疑那里没有潜伏的龙) .

@jimmyfrasche关于别名的方法,当然别名不允许绕过通常的方法定义限制:如果一个包定义了类型 T1 = otherpkg.T2,它不能在 T1 上定义方法,就像它不能直接在 otherpkg.T2 上定义方法一样。 但是,如果一个包定义了类型 T1 = T2(都在同一个包中),那么答案就不太清楚了。 我们可以引入限制,但(目前)还没有明显的需求。

更新了顶级讨论摘要。 变化:

  • 添加了 GothamGo 视频的链接
  • 根据@jba,添加了“缩写长名称”作为可能的用途。
  • 根据@Merovius,添加 x/image/draw 作为反对标准库限制的参数。
  • 根据@jimmyfrasche,添加了更多关于别名方法的文本。

添加设计文档: golang.org/design/18130-type-alias

与一周前的情况一样,类型别名似乎仍然存在普遍共识。 罗伯特和我起草了一份正式的设计文档,我刚刚签入(上面的链接)。

提案流程之后,请在_此处_就此问题发表对提案的实质性评论。 Spelling/grammar/etc 可以去 Gerrit codereview 页面https://go-review.googlesource.com/#/c/34592/。 谢谢。

我想重新考虑“对嵌入的影响”。 它限制了渐进式代码修复的类型别名的可用性。 也就是说,如果p1想要重命名类型type T1 = T2并且包p2p1.T2嵌入到结构中,他们将永远无法将该定义更新为p1.T1 ,因为导入器p3可能会按名称引用嵌入的结构。 p2然后不能在不破坏p3情况下切换到p1.T1 p3p3无法将名称更新为p1.T1 ,而不会破坏当前的p2

一种解决方法是,a) 通常将任何兼容性/弃用期承诺限制为不按名称引用嵌入字段的代码,或 b) 添加单独的弃用阶段,因此p1添加type T1 = T2并弃用T2 ,然后p2 s2.T2按名称弃用(例如) p2进口商都将被修复不这样做,然后p2进行切换。

现在,理论上,这个问题可以无限地递归; p4可能会导入p3 ,它本身嵌入了p2 ; 在我看来, p3也需要有一个弃用期,以按名称引用两次嵌入的字段? 在这种情况下,最内层的弃用期变得无穷小,或者最外层变为无穷大。 但即使不考虑这个问题是递归的,在我看来,b) 也很难计时( p2的弃用期需要完全包含在p1的弃用期中至少选择 2T,以便版本对齐)。

a) 对我来说也似乎不切实际; 例如,如果一个类型嵌入了一个*byte.Buffer并且我想设置该字段(或将该缓冲区传递给某个其他函数),则根本无法做到这一点,而不按名称引用它(使用结构初始化程序除外没有名称,这也失去了兼容性保证:))。

我理解与byterune作为别名兼容的吸引力。 但是,就我个人而言,我会将其放在次要位置,以保留类型别名对逐步修复的有用性。 获得两者的想法的(可能是坏的)示例是,对于导出的名称,允许使用任何别名来引用嵌入字段和未导出的名称(本质上仅限于同一个包,因此在作者的更多控制下) ) 保持当前提议的语义? 是的,我也不喜欢这种区别。 也许有人有更好的主意。

@rsc重新使用别名的方法

如果你有一个类型 S 是类型 T 的别名,两者都定义在同一个包中,并且你允许在 S 上定义方法,如果 T 是在不同包中定义的 pF 的别名怎么办? 虽然这显然也应该失败,但在源代码的执行、实现和可读性方面有一些微妙之处需要考虑(如果 T 与 S 位于不同的文件中,则不清楚是否可以通过查看 T 上的方法来定义一个方法T) 的定义。

规则——如果你有type T = S ,那么你不能在T上声明方法——是绝对的,从源代码中的那一行可以清楚地看出它适用,而不必调查S,就像您在别名情况下的别名一样。

此外,允许本地类型别名上的方法混淆了类型别名和类型定义之间的区别。 由于无论如何这些方法都会在 S 和 T 上定义,因此它们只能写在一个上的限制并不限制可以表达的内容。 它只是让事情变得更简单、更统一。

@jimmyfrasche如果我们正在编写type T1 = T2并且 T2 在同一个包中,那么我们可能会弃用名称 T2。 在这种情况下,我们希望在 godoc 中尽可能少地出现 T2。 所以我们想将所有方法声明为func (T1) M()

@jba一个 godoc 更改以报告在该别名上声明的别名的方法将满足该要求,而不会改变源代码的可读性。 一般来说,如果 godoc 在涉及别名和/或嵌入时显示类型的完整方法集会很好,特别是当类型来自另一个包时。 这个问题应该用更智能的工具来解决,而不是更多的语言语义。

@jba在那种情况下,你为什么不改变别名的方向? type T2 = T1已经允许你在T1上定义具有相同包结构的方法; 唯一的区别是reflect包报告的类型名称,您可以通过在添加别名之前将名称敏感的调用站点修复为名称不敏感来开始迁移。

@jimmyfrasche提案文件中

“由于 T1 只是 T2 的另一种写法,它没有自己的方法声明集。相反,T1 的方法集与 T2 的相同。至少在最初的试验中,对使用 T1 作为方法声明没有限制在同一声明中使用 T2 提供的接收器类型

使用 pF 作为方法接收器类型永远不会有效。

@mdempsky我不是很清楚,但我确实说过它无效。

我的观点是,仅通过查看该特定代码行,不太清楚它是否有效。

鉴于type S = T ,您还必须查看T以确保它不是另一个包中的类型别名的别名。 唯一的好处是复杂性。

始终禁止别名上的方法更简单、更易于阅读,并且不会丢失任何内容。 我不认为会经常出现令人困惑的情况,但是当您没有获得任何其他地方或通过不同但等效的方法无法更好地处理的东西时,没有必要引入这种可能性。

@Merovius

如果 p1 想要重命名类型类型 T1 = T2 并且包 p2 将 p1.T2 嵌入到结构中,则它们将永远无法将该定义更新为 p1.T1,因为导入器 p3 可能会按名称引用嵌入的结构。

今天在许多情况下,可以通过将匿名字段更改为命名字段并显式转发方法来解决此问题。 但是,这不适用于未导出的方法。

另一种选择可能是添加第二个功能来进行补偿。 如果您可以在不使其匿名(或显式重命名)的情况下采用字段的方法集,则即使基础类型已更改,也将允许字段名称保持不变。

考虑到您示例中的声明:

package p2

type S struct {
  p1.T2
}

一种补偿功能可能是“字段别名”,它遵循类似的语法来键入别名:

package p2

type S struct {
  p1.T1
  T2 = T1  // field T2 is an alias for field T1.
}

var s S  // &s.T2 == &s.T1

另一个补偿功能可能是“委托”,它会明确采用匿名字段的方法集:

package p2

type S struct {
  T2 p1.T1 delegated  // T2 is a field of type T1.
  // The method set of S includes the method set of T1 and forwards those calls to field T2.
}

我想我自己更喜欢字段别名,因为它们还可以实现另一种逐步修复:重命名结构的字段而不引入指针别名或一致性错误。

@Merovius主要问题是当类型被别名重命名时。

我还没有完全考虑到这一点——只是顺便说一句,只是一个随机的想法:

如果你在你的包中引入一个别名来命名并嵌入它呢?

我不知道这是否能解决任何问题,但也许它会花一些时间来打破循环?

@bcmills我没想到这种解决方法,谢谢。 我认为,关于未导出方法的警告似乎(对我来说)在实践中很少出现,以至于它不会影响我的总体意见(除非我不完全理解它。如果您认为这有用,请随时澄清)。 我不认为增加更多的变化是合理的(或一个好主意)。

@Merovius我想

即使它们被导出,显式转发方法也是乏味的,并且会破坏其他类型的重构(例如,将方法添加到嵌入类型并期望嵌入它的类型继续满足相同的接口)。 重命名结构字段也属于启用逐步代码修复的一般范围。

@Merovius

如果 p1 想要重命名类型类型 T1 = T2 并且包 p2 将 p1.T2 嵌入到结构中,则它们将永远无法将该定义更新为 p1.T1,因为导入器 p3 可能会按名称引用嵌入的结构。 p2 则不能在不破坏 p3 的情况下切换到 p1.T1; p3 无法将名称更新为 p1.T1,而不会中断当前的 p2。

如果我理解你的例子,我们有:

package p1

type T2 struct {}
type T1 = T2
package p2

import "p1"

type S struct {
  p1.T2
  F2 string // see below
}

我相信这只是我们希望重命名结构字段的一般情况的一个具体示例; 如果我们想将 S.F2 重命名为 S.F1,同样的问题也适用。

在这种特定情况下,我们可能会更新包 p2 以使用 p1 的具有本地类型别名的新 API:

package p2

import "p1"

type T2 = p1.T1

type S struct {
  T2
}

当然,这不是一个好的长期解决方案。 我认为没有任何方法可以解决 p2 需要更改其导出的 API 以消除 T2 名称的事实,但是,这将与任何字段重命名的方式相同。

只是关于“在包之间移动类型”的注释。 那个公式是不是有点问题?

据我了解,该提案允许通过新名称“引用”位于另一个包中的对象定义。

它不会移动对象定义,是吗? (除非首先使用别名编写代码,在这种情况下,用户可以自由更改别名所指的位置,就像在 draw pkg 中一样)。

@atdiar引用不同包中的类型可以用作移动类型的步骤。 是的,别名不会移动类型,但它可以用作这样做的工具。

@Merovius这样做可能会破坏反射和插件。

@atdiar对不起,我不明白你想说什么。 您是否阅读了该线程的原始评论、其中链接的有关逐步修复的文章以及到目前为止的讨论? 如果你想在讨论中添加一个迄今为止尚未考虑的论点,我相信你需要更清楚。

最后,一个有用且写得很好的提案。 我们需要类型别名,我在创建没有类型别名的单个 API 方面遇到了很大的问题,到目前为止,我必须以一种我不太喜欢的方式来编写代码来实现这一点。 这应该包含在 go v1.8 中,但永远不会太晚,所以继续 1.9。

@Merovius
我明确地谈论包之间的“移动类型”。 它改变了对象定义。 例如,在 pkg reflect 中,一些信息与定义对象的包相关联。
如果移动定义,它可能会中断。

@kataras这与好的文档和评论

再次@atdiar ,请阅读原始评论和到目前为止的讨论中的文章。 移动类型以及如何解决您的问题是该线程的主要关注点。 如果您认为 Russ 的文章没有充分解决您的顾虑,请具体说明为什么他的解释不令人满意。 :)

@kataras虽然我个人同意,但我认为简单地断言我们发现此功能的重要性并没有特别的帮助。 需要提出建设性的论点来解决人们的担忧。 :)

@Merovius我已阅读文档。 它没有回答我的问题。 我想我已经足够明确了。 它与阻止我们实施以前的别名提案的同一问题有关。

@atdiar我,至少,不明白。 您是说移动类型会破坏事物; 提议是关于如何通过逐步修复来避免这种破坏,通过使用别名,然后更新每个反向依赖,直到没有代码使用旧类型,然后删除旧类型。 我不明白,在这些假设下,你的断言“反射和插件”是如何被破坏的。 如果你想质疑这些假设,那已经讨论过了。

我也没有看到任何阻止别名进入 1.8 的问题与您所说的有关。 据我所知,相应的问题是 #17746 和 #17784。 如果您指的是嵌入问题(这可能被解释为与破损或反射有关,尽管我不同意),那么正式提案中会解决这个问题(不过,请参见上文,我认为建议的解决方案值得更多讨论)你应该具体说明为什么你不相信它。

所以,我很抱歉,但不,你不够具体。 您是否有您所指的“阻止我们实施前别名提案的同一问题”的问题编号,这与您迄今为止提到的内容有关,以帮助理解? 你能举一个你所说的破坏的具体例子吗(参见这个上行线程的例子;给出一系列包、类型定义和一些代码,并描述它在按照建议转换时是如何破坏的)? 如果您希望解决您的疑虑,您确实需要首先帮助他人了解它们。

@Merovius因此,在传递依赖项的情况下,这些依赖项之一正在查看reflect.Type.PkgPath(),会发生什么?
这与嵌入问题中发生的问题相同。

@atdiar抱歉,鉴于迄今为止在该线程中的讨论以及该提案的内容,我不认为这是一个可以理解的问题。 我现在将跳出这个特定的子线程,并给其他可能更好地理解您的反对意见的人解决它的机会。

让我简明扼要地重新表述一下:

鉴于类型定义对其自身位置进行了硬编码,因此问题在于类型相等性。
由于可以在运行时测试类型相等性,因此我看不出移动类型是多么容易做到。

我只是提出一个警告,即“移动类型”的这种用例可能会在远处破坏大量的包。 与插件类似的担忧。

(如果并行可以使事情更清晰,那么更改包中指针类型的相同方式会破坏许多其他包。)

@atdiar同样,这个问题是关于分两步移动类型,首先弃用旧位置并更新反向依赖关系,然后_移动类型。 _当然_如果你只是移动类型,事情会崩溃,但这根本不是这个问题的内容。 这是关于启用一个渐进的、多步骤的解决方案来做到这一点。 如果您担心此处提出的任何解决方案都无法启用此多步骤过程,请准确描述一种情况,即没有合理的逐步修复提交顺序可以防止损坏。

@尼迈耶

这使得一两个独立的类型,无论是反射还是在接口下{},就是这样
你看。 您也可以在一和二之间转换。 上面的适配器建议只是最后
适配器的步骤自动。 您可能出于多种原因不喜欢该提案,但没有什么
对此自相矛盾。

你不能在两者之间转换

 func() one

func() two

@Merovius您不可能考虑更改存在于野外的代码修复包的所有导入程序。 而且我不太热衷于在这里开始研究包版本控制。

需要明确的是,我并不反对别名提议,而是“在包之间移动类型”的表述,这意味着一个尚未证明安全的用例。

@jimmyfrasche关于别名方法有效性的可预测性:

func (t T) M()有时有效,有时无效。 它不会出现太多,因为人们不会经常突破这些界限。 也就是说,它在实践中运行良好。 https://play.golang.org/p/bci2qnldej。 无论如何,这都在_可能_限制的列表中。 像所有可能的限制一样,它增加了复杂性,我们希望在增加复杂性之前看到具体的现实证据。

@Merovius ,重新嵌入名称:

我同意情况并不完美。 但是,如果我有一个充满对 io.ByteBuffer 的引用的代码库,并且我想将其移动到 bytes.Buffer,那么我希望能够引入

package io
type ByteBuffer = bytes.Buffer

_without_ 更新对 io.ByteBuffer 的任何现有引用。 如果所有嵌入 io.ByteBuffer 的地方都自动将字段名称更改为 Buffer 作为将类型定义替换为别名的结果,那么我已经破坏了世界并且没有逐步修复。 反之,如果一个内嵌的io.ByteBuffer的名字仍然是ByteBuffer,那么用户可以在自己的逐步修复中一次更新一个(可能需要做多个步骤;同样不理想)。

我们在 #17746 中详细讨论了这个问题。 我最初支持嵌入的 io.ByteBuffer 别名为 Buffer,但上述论点使我确信我错了。 @jimmyfrasche特别提出了一些很好的论据,即代码不会根据嵌入事物的定义而改变。 我认为完全禁止嵌入别名是站不住脚的。

请注意,在您的示例中 p2 中有一个解决方法。 如果 p2 真的想要一个名为 ByteBuffer 的嵌入字段而不引用 io.ByteBuffer,它可以定义:

type ByteBuffer = bytes.Buffer

然后嵌入一个 ByteBuffer(即 p2.ByteBuffer)而不是 io.ByteBuffer。 这也不是完美的,但这意味着维修可以继续。

这绝对是不完美的情况,并且该提案通常不涉及字段重命名。 可能是嵌入不应该对底层名称敏感,应该有某种“将 X 嵌入为名称 N”的语法。 也可能是我们应该稍后添加字段别名。 两者似乎都是先验的合理想法,两者应该是分开的,后来根据需求的真实证据评估的提案。 如果类型别名可以帮助我们达到缺少字段别名是大规模重构的下一个大障碍的地步,那将是进步!

(/cc @neild和 @bcmills)

@atdiar ,是的,reflect 确实会看穿这些类型的更改,如果代码依赖于 reflect 的结果,它就会崩溃。 就像嵌入的情况一样,它并不完美。 与嵌入的情况不同,我没有任何答案,除了可能不应该使用反射编写代码对这些细节非常敏感。

@rsc我的想法是 a) 禁止在同一个结构中嵌入别名和它定义的类型(以防止 b)中的歧义,b)允许在源代码中通过任一名称引用一个字段,c)选择一个或生成的类型信息/反射等中的另一个(不关心哪个)。

我会毫不犹豫地声称,这有助于避免我试图描述的那种破损,同时也为需要选择的情况做出明确的选择; 而且,就我个人而言,与不破坏依赖反射的代码相比,我更不关心不破坏依赖反射的代码。

我现在不确定我是否理解你的 ByteBuffer 论点,但我也在一个漫长的工作日结束,所以不需要进一步解释,如果我觉得它没有说服力,我最终会回复:)

@Merovius我认为在引入更复杂的规则之前尝试简单的规则并看看我们能走多远是有意义的。 如果需要,我们可以稍后添加(a)和(b); (c) 无论如何都是给定的。

我同意也许 (b) 在某些情况下是个好主意,但在其他情况下可能不是。 如果您在前面提到的“将一个包 API 构建为多个实现包”用例中使用类型别名,那么您可能不希望嵌入别名来暴露另一个名称(它可能在内部包中并且否则大多数用户无法访问)。 我希望我们可以积累更多的经验。

@rsc

也许将有关别名的包级别信息添加到目标文件会有所帮助。
(同时考虑到 go 插件是否必须保持正常工作。)

@Merovius @rsc

a) 禁止在同一个结构体中同时嵌入别名和定义类型

请注意,在许多情况下,由于嵌入与方法集交互的方式,这已经被禁止。 (如果嵌入的类型有一个非空的方法集并且其中一个方法被调用,程序将无法编译:https://play.golang.org/p/XkaB2a0_RK。)

因此,添加禁止双重嵌入的明确规则似乎只会在一小部分情况下产生影响; 对我来说似乎不值得复杂。

为什么不将类型别名作为代数类型来处理,并支持一组类型的别名,这样我们也可以得到一个空接口等效于编译时类型检查作为奖励,a la

type Stringeroonie = {string,fmt.Stringer}

@j7b

为什么不将类型别名作为代数类型来代替并支持一组类型的别名

别名在语义和结构上与原始类型等效。 代数数据类型不是:在一般情况下,它们需要额外的类型标签存储。 (Go 接口类型已经携带该类型信息,但结构和其他非接口类型没有。)

@bcmills

这可能是错误的推理,但我认为可以解决这个问题,因为 T 类型的别名 A 相当于将 A 声明为 interface{} 并让编译器在声明类型 A 的变量的范围内透明地将 A 类型的变量转换为 T ,我认为这主要是线性编译时成本,明确,并为编译器管理的伪类型创建基础,包括使用type T =语法的代数,并且可能还允许在编译时实现像不可变引用这样的类型就用户代码而言,它只是界面的“幕后”。{}

这种思路的缺陷可能是无知的产物,由于我无法提供概念的实际证明,我很高兴接受它的缺陷和推迟。

@j7b即使 ADT 解决了一个逐步修复问题,他们也会创建自己的; 在不破坏依赖关系的情况下添加或删除 ADT 的任何成员是不可能的。 所以,本质上你会创造出比你解决的更多的问题。

您在 interface{} 之间透明转换的想法也不适用于[]interface{}等高阶类型。 最终你最终会失去 go 的一项优势,那就是让用户控制数据布局,而不是做包装一切的 java 事情。

ADT 不是这里的解决方案。

@Merovius我很确定代数类型构造是否包含重命名(这将与相同的合理定义一致)它是一个解决方案,该接口{} 可以作为编译器管理的投影和选择形式的代理描述,我不确定数据布局是如何相关的,也不知道如何定义“高阶”类型,如果可以声明类型,则类型只是一种类型,而 []interface{} 只是一种类型。

除此之外,我很肯定type T =有可能以直观、有用的方式重载,而不是重命名,代数类型和公开的不可变引用似乎是最明显的应用,所以我希望规范最终说明语法指示编译器管理的元或伪类型,并考虑编译器管理的类型可能有用的所有方式以及最能表达这些用途的语法。 由于新语法在用作限定词时不需要关注全局保留字集,因此type A = alias Type将是清晰且可扩展的。

@j7b

除此之外,我肯定类型 T = 有可能以直观、有用的方式重命名,而不是重命名,

我当然希望不会。 Go 在今天(大部分)很好地正交,并且保持这种正交性是一件好事。

今天,在 Go 中声明类型 T 的方式是type T def ,其中def是新类型的定义。 如果要实现代数数据类型(又名标记联合),我希望它们遵循该语法而不是类型别名的语法。

我喜欢对类型别名提出不同的观点(支持),这可能会提供对重构之外的替代用例的一些见解:

让我们退后一步,假设我们没有type T <a type>形式的常规旧 Go 类型声明,而只有type A = <a type>类型的别名声明。

(为了使图片完整,我们还假设方法以某种方式以不同方式声明 - 不是通过与用作接收器的命名类型的关联,因为我们不能。例如,可以想象具有方法字面意义的类类型的概念内部,所以我们不需要依赖命名类型来声明方法。结构相同但方法不同的两个这样的类型将是不同的类型。对于这个思想实验,这里的细节并不重要。)

我声称在这样的世界中,我们可以编写与现在编写的代码几乎相同的代码:我们使用(别名)类型名称,因此我们不必重复自己,并且类型本身确保我们在类型中使用数据- 安全的方式。

换句话说,如果 Go 是这样设计的,我们可能也会大体上过得很好。

更重要的是,在这样的世界中,因为如果类型在结构上相同(无论名称如何),则类型是相同的,所以我们现在重构的问题不会首先出现,并且不需要任何语言的变化。

但是我们不会拥有当前 Go 中的安全机制:我们无法为类型引入名称并声明它现在应该是一种新的、不同的类型。 (不过,重要的是要记住,它本质上是一种安全机制。)

在其他编程语言中,创建与现有类型不同的新类型的概念称为“品牌化”:类型 get 是附加在其上的品牌,使其与所有其他类型不同。 例如,在 Modula-3 中,有一个特殊的关键字BRANDED来实现这一点(例如, TYPE T = BRANDED REF T0会创建一个新的、不同的 T0 引用)。 在 Haskell 中,类型前的new一词具有类似的效果。

回到我们的替代 Go 世界,我们可能会发现我们处于重构没有问题的位置,但我们希望提高代码的安全性,以便type MyBuffer = []bytetype YourBuffer = []byte表示不同的类型,这样我们就不会意外使用错误的类型。 我们可能会建议为这个目的引入一种类型品牌。 例如,我们可能想写type MyBuffer = new []byte ,甚至type MyBuffer = new YourBuffer ,这样 MyBuffer 现在是与 YourBuffer 不同的类型。

这实质上是我们现在所面临的双重问题。 碰巧的是,在 Go 中,从第一天起,我们总是在“品牌”类型获得名称后立即使用它们。 换句话说, type T <a type>实际上是type T = new <a type>

总结一下:在现有的 Go 中,命名类型总是“品牌”类型,我们缺乏类型名称的概念(我们现在称之为类型别名)。 在其他几种语言中,类型别名是常态,必须使用“品牌化”机制来创建明确的新的不同类型。

关键是这两种机制本质上都是有用的,并且通过类型别名我们最终可以同时支持它们。

@griesemer该功能的扩展是最初的别名建议,理想情况下应该清理重构。 我担心只有类型别名会因为其范围受限而造成难以重构的边缘情况。

在这两个提案中,我想知道是否不需要链接器的协作,因为正如您所解释的,名称是 Go 中类型定义的一部分。

我对目标代码一点也不熟悉,所以这只是一个想法,但似乎可以将自定义部分添加到目标文件中。 如果有机会,可以保留一种展开的链表,在链接时填充类型名称及其别名,这可能会有所帮助。 运行时将拥有它需要的所有信息,而无需牺牲单独的编译。

这个想法是运行时应该能够动态返回给定类型的不同别名,以便错误消息保持清晰(因为别名会在运行代码和编写代码之间引入命名差异)。

跟踪别名使用的另一种方法是在大的情况下有一个具体的版本控制故事,以便能够像上下文包那样跨包“移动”对象定义。 但这完全是另一个问题。

最后,保留接口的结构等价和类型的名称等价仍然是一个好主意。
鉴于可以将类型视为具有更多约束的接口这一事实,似乎应该/可以通过保留切片类型名称字符串的每个包切片来实现声明别名。

@atdiar我不确定你说“单独编译”时我的意思。 如果包 P 导入 io 和 bytes,那么这三个都可以作为单独的步骤进行编译。 但是,如果 io 或 bytes 发生变化,则必须重新编译 P。 _不_这种情况,您可以更改 io 或字节,然后只使用旧的 P 编译。即使在插件模式下,也是如此。 由于跨包内联等影响,即使是对 io 或字节实现的非 API 可见更改也会更改有效 ABI,这就是必须重新编译 P 的原因。 类型别名不会使这个问题变得更糟。

@j7d ,在类型系统级别,求和类型或任何类型的子类型(正如前面讨论中的其他人所建议的那样)仅有助于某些类型的用途。 确实,我们可以将 bytes.Buffer 视为 io.Reader 的子类型(“缓冲区是阅读器”,或者在您的示例中“字符串是 Stringeroonie”)。 使用这些构建更复杂的类型时会出现问题。 本评论的其余部分讨论 Go 类型,但在子类型级别讨论它们的基本关系,而不是 Go 语言实际实现的内容。 不过,Go 必须实现与基本关系一致的规则。

类型构造函数(一种说“使用类型的方法”的奇特方式)如果保留子类型关系是协变的,如果它反转关系则是逆变的。

在函数结果中使用类型是协变的。 一个 func() Buffer “是一个” func() Reader,因为返回一个 Buffer 意味着你已经返回了一个 Reader。 在函数参数中使用类型是_not_协变的。 一个 func(Buffer) 不是一个 func(Reader),因为 func 需要一个 Buffer,而有些 Readers 不是 Buffers。

在函数参数中使用类型是逆变的。 一个func(Reader)就是一个func(Buffer),因为func只需要一个Reader,而一个Buffer就是一个Reader。 在函数结果中使用类型是_not_逆变的。 一个 func() Reader 不是一个 func() Buffer,因为 func 返回一个 Reader,有些 Readers 不是 Buffers。

将两者结合起来,func(Reader) Reader 不是 func(Buffer) Buffer,反之亦然,因为要么参数不起作用,要么结果不起作用。 (沿着这些行的唯一组合是 func(Reader) Buffer 是 func(Buffer) Reader。)

通常,如果 func(X1) X2 是 func(X3) X4 的(子类型),那么 X3 一定是 X1 的(子类型),类似地,X2 是 X4 的(子类型)。 在使用别名的情况下,我们希望 T1 和 T2 可以互换,只有当 T1 是 T2 的子类型并且_T2 是 T1 的子类型时,func(T1) T1 才是 func(T2) T2 的子类型。 这基本上意味着 T1 是与 T2 相同的类型,而不是更通用的类型。

我使用了函数参数和结果,因为这是典型的例子(也是一个很好的例子),但同样的情况也适用于构建复杂结果的其他方法。 通常,您会得到输出的协方差(如 func() T 或 <-chan T 或 map[...]T)和输入的逆变(如 func(T) 或 chan<- T 或 map[T ]...) 和输入+输出的强制类型相等(如 func(T) T,或 chan T,或 *T,或 [10]T,或 []T,或 struct {Field T},或变量T型)。 实际上,正如您从示例中看到的那样,Go 中最常见的情况是输入 + 输出。

具体来说,[]Buffer 不是 []Reader(因为您可以将 File 存储到 []Reader 但不能存储到 []Buffer),[]Reader 也不是 []Buffer(因为从 [] Reader 可能返回一个 File,而从 []Buffer 获取必须返回一个 Buffer)。

从这一切得出的结论是,如果要解决通用代码修复问题,使代码既可以使用 T1 也可以使用 T2,就不能使用任何使 T1 只是 T2 的子类型的方案(反之亦然)。 每个都需要是另一个的子类型——也就是说,它们需要是相同的类型——否则这些列出的一些用途将是无效的。

也就是说,子类型不足以解决渐进式代码修复问题。 这就是为什么类型别名为同一类型引入一个新名称,以便 T1 = T2,而不是尝试子类型化。

此评论也适用于@iand两周前关于某种“可替代类型”的建议,基本上是对@griesemer的回复的扩展。

更新了顶级讨论摘要。 变化:

  • 删除了 TODO 以更新适配器讨论的摘要,这似乎已经淡出。
  • 添加了嵌入和字段重命名的讨论摘要。
  • 将“别名方法”的摘要移至设计问题列表之外的单独部分,并扩展为包括最近的评论。
  • 添加了使用反射对程序的影响的讨论摘要。
  • 添加了单独编译的讨论摘要。
  • 添加了各种基于子类型的方法的讨论摘要。

@rsc关于单独编译,我的评论是关于类型定义是否需要保留其别名列表(由于单独的编译要求,这在大规模上难以处理)还是每个别名都涉及迭代构建以下别名列表导入图,都与类型定义中提供的给定初始类型名称相关。 (以及如何以及在何处保留该信息,以便运行时可以访问它)。

@atdiar系统中的任何地方都没有这样的别名列表。 运行时无权访问它。 别名在运行时不存在。

@rsc嗯,对不起。 我坚持使用 head 中的初始别名建议,并且正在考虑 func 的别名(同时讨论类型的别名)。 在这种情况下,代码中的名称和运行时的名称之间会存在差异。
在这种情况下,使用 runtime.Frame 中的信息进行日志记录需要一些重新思考。
别管我。

@rsc感谢您重新总结。 嵌入的字段名称仍然让我感到厌烦; 提出的所有变通方法都依赖于永久的kludges 来保留旧名称。 尽管此评论中更重要的一点,即这是重命名字段的特殊情况,这也是不可能的,但让我相信这确实应该被视为(并解决)作为一个单独的问题。 为请求/提案/讨论打开一个单独的问题以支持字段重命名以进行逐步修复(可能在同一个 go 版本中解决)是否有意义?

@Merovius ,我同意字段重命名的逐步代码修复看起来像是序列中的下一个问题。 为了开始讨论,我认为有人需要收集一组真实世界的例子,这样我们才能有一些证据表明这是一个普遍存在的问题,并检查潜在的解决方案。 实际上,我认为同一版本不会发生这种情况。

从两周后回来。 讨论似乎收敛了。 甚至两周前的讨论更新也相当小。

我建议我们:

  • 接受类型别名提案作为上述问题的暂定解决方案,
    前提是可以在 Go 1.9(2 月 1 日)开始时准备好供人们尝试的实现。
  • 创建一个 dev.typealias dev 分支,以便现在(1 月)可以审查 CL,并在 Go 1.9 开始时将其合并到 master 中。
  • 在 Go 1.9 冻结开始时做出关于保留类型别名的最终决定(就像我们在 Go 1.8 周期中对通用别名所做的那样)。

+1

我很欣赏这种变化背后的讨论历史。 假设它已实现。 毫无疑问,它将成为语言的一个相当边缘的细节,而不是一个核心特性。 因此,它增加了与实际使用频率不成比例的语言和工具的复杂性。 它还增加了可能无意中滥用语言的更多表面积。 出于这个原因,过于谨慎是一件好事,我很高兴到目前为止已经有很多讨论。

@Merovius :抱歉编辑我的帖子! 我以为没有人在读书。 最初在此评论中,我表达了一些怀疑,即当已经存在诸如gorename工具之类的工具时,是否需要进行这种语言更改。

@jcao219这之前已经讨论过,但令人惊讶的是,我似乎无法在这里快速找到。 在原始线程中详细讨论了一般别名 #16339 和相关的 golang-nuts 线程。 简而言之:这种工具只解决如何准备修复提交,而不是如何对更改进行排序以防止损坏。 更改是由工具还是由人完成对问题无关紧要,目前没有不会破坏某些代码或其他代码的提交序列(此问题的原始评论和相关文档在-深度)。

对于更自动化的工具(例如集成到 go 工具或类似工具中),原始评论在标题“这可以是仅工具或编译器的更改而不是语言更改吗?”下解决了这个问题。

总之,假设更改已实施。 毫无疑问,它将成为语言的一个相当边缘的细节,而不是一个核心特性。

我想表示怀疑。 :) 我不认为这已成定局。

@Merovius

我想表示怀疑。 :) 我不认为这已成定局。

我想我的意思是使用这个功能的人主要是重要的 Go 包的维护者,有很多依赖的客户端。 换句话说,它使那些已经是围棋专家的人受益。 同时,它提供了一种诱人的方法,使新 Go 程序员的代码可读性降低。 重命名长名称的用例是个例外,但自然的 Go 类型名称通常不会太长或太复杂。

就像点导入功能的情况一样,教程和文档最好在提及此功能时附上使用指南的声明。

例如,假设我想使用"github.com/gonum/graph/simple".DirectedGraph ,并且我想用digraph为其别名以避免键入simple.DirectedGraph ,那会不会很好用例? 还是应该将这种重命名限制为由 protobuf 之类的东西生成的不合理的长名称?

@jcao219 ,本页顶部的讨论摘要回答了您的问题。 特别是,请参阅以下部分:

  • 这可以是仅工具或编译器的更改而不是语言更改吗?
  • 别名还有哪些其他用途?
  • 限制(开始该部分的一般说明)

对于 Go 专家与 Go 新程序员的更一般的观点,Go 的一个明确目标是使在大型代码库中编程更容易。 您是否是专家与您所使用的代码库的大小在某种程度上无关。(也许您只是在开始一个别人开始的新项目。您可能仍然需要做这种工作。)

好的,基于这里的一致/安静,我会(正如我上周在 https://github.com/golang/go/issues/18130#issuecomment-268614964 中建议的那样)将此提案标记为已批准并创建一个 dev.typealias 分支.

优秀的总结有一个部分“类型别名的提案还需要解决哪些其他问题?” 在提案被宣布为接受后,有什么计划来解决这些问题?

CL https://golang.org/cl/34986提到了这个问题。

CL https://golang.org/cl/34987提到了这个问题。

CL https://golang.org/cl/34988提到了这个问题。

@ulikunitz重新解决问题(设计文档中的所有这些引用都假定“类型 T1 = T2”):

  1. 在 godoc 中处理。 设计文档规范了对 godoc 的最小更改。 一旦进入,我们就可以看到是否需要额外的支持。 也许,但也许不是。
  2. 可以在以别名命名的类型上定义方法吗? 是的。 设计文档:“由于 T1 只是 T2 的另一种写法,它没有自己的方法声明集。相反,T1 的方法集与 T2 的相同。至少对于初始试验,对方法声明没有限制使用 T1 作为接收器类型,前提是在同一声明中使用 T2 将是有效的。”
  3. 如果允许别名到别名,我们如何处理别名循环? 没有循环。 设计文档:“在类型别名声明中,与类型声明相反,T2 绝不能直接或间接引用 T1。”
  4. 别名应该能够导出未导出的标识符吗? 是的。 设计文档:“T2的形式没有限制:可以是任何类型,包括但不限于从其他包导入的类型。”
  5. 嵌入别名时会发生什么(如何访问嵌入字段)? 该名称取自别名(程序中的可见名称)。 设计文档: https: //golang.org/design/18130-type-alias#effect -on-embedding。
  6. 别名是否可用作构建程序中的符号? 否。设计文档:“类型别名在运行时大多不可见。” (答案由此而来,但没有明确指出。)
  7. Ldflags 字符串注入:如果我们引用别名怎么办? 没有 var 别名,因此不会出现这种情况。

CL https://golang.org/cl/35091提到了这个问题。

CL https://golang.org/cl/35092提到了这个问题。

CL https://golang.org/cl/35093提到了这个问题。

@rsc非常感谢您的澄清。

让我们假设:

package a

import "b"

type T1 = b.T2

据我了解,T1 与 b.T2 本质上相同,因此是非本地类型,无法定义新方法。 然而,标识符 T1 在包 a 中重新导出。 这是正确的解释吗?

@ulikunitz是正确的

T1 表示与 b.T2 完全相同的类型。 这只是一个不同的名字。 是否导出某些内容仅基于其名称(与其表示的类型无关)。

为了使@griesemer的回复明确:是的,T1 是从包 a 导出的(因为它是 T1,而不是 t1)。

CL https://golang.org/cl/35099提到了这个问题。

CL https://golang.org/cl/35100提到了这个问题。

CL https://golang.org/cl/35101提到了这个问题。

CL https://golang.org/cl/35102提到了这个问题。

CL https://golang.org/cl/35104提到了这个问题。

CL https://golang.org/cl/35106提到了这个问题。

CL https://golang.org/cl/35108提到了这个问题。

CL https://golang.org/cl/35120提到了这个问题。

CL https://golang.org/cl/35121提到了这个问题。

CL https://golang.org/cl/35129提到了这个问题。

CL https://golang.org/cl/35191提到了这个问题。

CL https://golang.org/cl/35233提到了这个问题。

CL https://golang.org/cl/35268提到了这个问题。

CL https://golang.org/cl/35269提到了这个问题。

CL https://golang.org/cl/35670提到了这个问题。

CL https://golang.org/cl/35671提到了这个问题。

CL https://golang.org/cl/35575提到了这个问题。

CL https://golang.org/cl/35732提到了这个问题。

CL https://golang.org/cl/35733提到了这个问题。

CL https://golang.org/cl/35831提到了这个问题。

CL https://golang.org/cl/36014提到了这个问题。

这是现在掌握,在 Go 1.9 开放之前。 请随时在 master 上同步并尝试一下。 谢谢。

重定向自 #18893

package main

import (
        "fmt"
        "q"
)

func main() {
        var a q.A
        var b q.B // i'm a named unnamed type !!!

        fmt.Printf("%T\t%T\n", a, b)
}

你期待看到什么?

deadwood(~/src) % go run main.go
q.A     q.B

你看到了什么?

deadwood(~/src) % go run main.go
q.A     []int

讨论

别名不应应用于未命名的类型。 从一种未命名的类型转移到另一种类型的过程中,他们并不是“代码修复”的故事。 允许未命名类型的别名意味着我不能再将 Go 教为简单的命名和未命名类型。 相反我不得不说

哦,除非它是一个别名,在这种情况下你必须记住它_可能是_一个未命名的类型,即使你从另一个包导入。

更糟糕的是,它将使人们能够发布可读性反模式,例如

type Any = interface{}

请不要让未命名的类型别名。

@davecheney

从一种未命名的类型转移到另一种类型的过程中没有“代码修复”的故事。

不对。 如果您想将方法参数的类型从命名类型更改为未命名类型,反之亦然怎么办? 第一步是添加别名; 第 2 步是更新实现该方法的类型以使用新类型; 第 3 步是删除别名。

(确实,您今天可以通过两次重命名方法来做到这一点。双重重命名充其量是乏味的。)

更糟糕的是,它将使人们能够发布可读性反模式,例如
type Any = interface{}

人们今天已经可以写type Any interface{} 。 在这种情况下,别名会带来什么额外的危害?

人们今天已经可以编写任何类型的接口{}。{} 在这种情况下,别名会带来什么额外的危害?

我称它为反模式,因为这正是它的本质。 type Any interface{} ,因为它_编写_代码的人键入的内容更短一些,这对他们来说更有意义。

另一方面,在阅读 Go 代码方面有经验并且本能地识别interface{}就像他们在镜子中的脸一样的 _all_ 读者必须学习并重新学习Any每个变体、 ObjectT ,并将它们映射到诸如type Any interface{}type Any map[interface{}]interface{}type Any struct{}内容。

你确定你同意常见的 Go 习语的包特定名称对可读性是净负面的吗?

你确定你同意常见的 Go 习语的包特定名称对可读性是净负面的吗?

我确实同意,但是由于有问题的示例(迄今为止我遇到的最常见的反模式)可以在没有别名的情况下完成,我不明白该示例与类型别名的建议有何关系。

反模式在没有类型别名的情况下是可能的这一事实意味着我们必须已经教育 Go 程序员避免它,无论是否存在未命名类型的别名。

而且,事实上,类型别名允许从已经存在的代码库中_逐渐删除_该反模式。

考虑:

package antipattern

type Any interface{}  // not an alias

type Widget interface{
  Frozzle(Any) error
}

func Bozzle(w Widget) error {
  …
}

今天, antipattern.Bozzle会在他们的Widget实现中使用antipattern.Any ,并且没有办法通过逐步修复来删除antipattern.Any 。 但是使用类型别名, antipattern包的所有者可以像这样重新定义它:

// Any is deprecated; please use interface{} directly.
type Any = interface{}

现在调用者可以逐渐从Any迁移到interface{} ,允许antipattern维护者最终删除它。

我的观点是没有理由为未命名类型设置别名,所以
不允许此选项将继续强调不适合
实践。

相反,允许未命名类型的别名启用不是一个,而是两个
这种反模式的形式。

2017 年 2 月 2 日星期四,Bryan C. Mills通知@ github.com 写道:

你肯定同意常见的 Go 习语的包特定名称是
对可读性的净负面影响?

我确实同意,但因为有问题的例子(迄今为止最常见的
我遇到的那个反模式的发生)可以在没有
别名,我不明白该示例与
类型别名。

反模式在没有类型别名的情况下是可能的,这意味着
我们必须已经教育 Go 程序员避免它,无论是否
可以存在未命名类型的别名。


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/18130#issuecomment-276872714或静音
线程
https://github.com/notifications/unsubscribe-auth/AAAcA6BGrFjjTi7eW1BPp7o81XIekbGXks5rYWr-gaJpZM4LBBEL
.

@davecheney我认为我们还没有任何证据表明能够为任意类型的文字命名是有害的。 这也不是一个意外的“惊喜”功能——它已经在设计文档中详细讨论过。 在这一点上,使用它一段时间并看看它会把我们引向何方是有意义的。

作为一个反例,有一些公共 API 使用类型文字只是因为 API 不想将客户端限制为特定类型(例如,请参阅 https://golang.org/pkg/go/types/#Info )。 拥有该显式类型文字可能是有用的文档。 但与此同时,不得不到处重复相同类型的文字可能会很烦人; 并且实际上是可读性的障碍。 能够方便地谈论IntSet而不是map[int]struct{}没有被锁定在那个并且只有IntSet定义在我看来是一个加分项。 这就是type IntSet = map[int]struct{}完全正确的地方。

最后,我想回顾一下https://github.com/golang/go/issues/18130#issuecomment -268411811,以防你错过了。 使用=无限制类型声明实际上是“基本”类型声明,我很高兴我们终于在 Go 中拥有它们。

也许type intSet = map[int]struct{} (未导出)是使用未命名类型别名的更好方法,但这听起来像是 CodeReviewComments 和推荐的编程实践的领域,而不是限制该功能。

也就是说, %T是在调试或探索类型系统时查看类型的便捷工具。 我想知道是否应该有一个包含别名的类似格式动词? q.B = []int@davecheney的例子中。

@nathany你是如何实现这个动词的? 别名信息在运行时不存在。 (就reflect包而言,别名与它别名的事物具有_相同的类型_。)

@bcmills我认为可能是这种情况......😞

我想静态分析工具和编辑器插件仍在图片中以帮助处理别名,所以没关系。

2017 年 2 月 2 日下午 5:01,“Nathan Youngman”通知@ github.com 写道:

也就是说,%T 是一个方便的工具,可以在调试或探索
类型系统。 我想知道是否应该有一个类似的格式动词
包括别名? @davecheney 中的 qB = []int
https://github.com/davecheney的示例。

我认为更好的解决方案是向大师添加查询模式来回答这个问题
题:

哪些是在整个 GOPATH(或给定的包)中声明的别名
命令行上的这个给定类型?

我不担心滥用别名未命名类型,但潜在的
相同未命名类型的重复别名。

@davecheney我将您的建议添加到顶部讨论摘要的“限制”部分。 与所有限制一样,我们的一般立场是限制会增加复杂性(见上文注释),我们可能需要看到广泛危害的实际证据才能引入限制。 仅仅改变你教授 Go 的方式是不够的:我们对语言所做的任何改变都需要改变你教授 Go 的方式。

正如设计文档和邮件列表中所指出的,我们正在研究更好的术语以使解释更容易。

@minux ,就像@bcmills指出的那样,别名信息在运行时不存在(完全是设计的基础)。 无法实现“包含别名的 %T”。

2017 年 2 月 2 日晚上 8:33,“Russ Cox”通知@github.com写道:

@minux https://github.com/minux ,如@bcmills
https://github.com/bcmills指出,别名信息不存在
在运行时(完全是设计的基础)。 没有办法
实现一个“包括别名的%T”。

我建议使用 Go guru (https://golang.org/x/tools/cmd/guru) 查询模式
用于反向别名映射,它基于静态代码分析。 它
别名信息在运行时是否可用并不重要。

@minux ,哦,我明白了,你是通过电子邮件回复的,Github 使引用的文本看起来像你自己写的文本。 我正在回复你从内森·扬曼那里引用的文本,认为这是你的。 对困惑感到抱歉。

关于术语和教学,我发现品牌类型背景@griesemer发布的信息非常丰富。 感谢那。

在解释类型和类型转换时,小地鼠们最初认为我在谈论类型别名,这可能是因为熟悉其他语言。

无论最终的术语是什么,我都可以想象在命名(品牌)类型之前引入类型别名,特别是因为在任何书籍或课程中引入byterune之后可能会声明新的命名类型。 但是,我确实想注意@davecheney不鼓励反模式的担忧。

对于type intSet map[int]struct{}我们说map[int]struct{}是 _underlying_ 类型。 我们称type intSet = map[int]struct{}两边是什么? 别名和别名类型?

至于%T ,我已经需要解释byterune导致uint8int32 ,所以这不是不同的。

如果有的话,我认为类型别名将使byterune更容易解释。 IMO,挑战在于知道何时使用命名类型与类型别名,然后能够进行交流。

@nathany我认为首先引入“别名类型”很有意义-尽管我不一定会使用该术语。 新引入的“别名”声明只是不做任何特殊事情的常规声明。 左边的标识符和右边的类型是一回事,它们表示相同的类型。 我什至不确定我们是否需要别名或别名类型(我们不称常量名称为别名,常量值称为别名常量)。

传统的(非别名)类型声明做了更多的工作:它首先从右边的类型创建一个新类型,然后将左边的标识符绑定到它。 因此,右侧的标识符和类型不相同(它们仅共享相同的基础类型)。 这显然是一个更复杂的概念。

对于这些新创建的类型,我们确实需要一个新术语,因为现在任何类型都可以有一个名称。 我们需要能够引用它们,因为存在引用它们的规范规则(类型标识、可分配性、接收器基本类型)。

这是描述这一点的另一种方式,它在教学环境中可能很有用:一个类型可以是有色或无色的。 所有预先声明的类型和所有类型文字都是未着色的。 创建新颜色类型的唯一方法是通过传统的(非别名)类型声明,首先使用全新的、从未使用过的颜色(剥离旧颜色,如果有的话,完全在过程中)在绑定左边的标识符之前。 同样,标识符和(隐式和不可见创建的)有色类型是相同的,但它们与写在右边的(不同颜色或无色)类型不同。

使用这个类比,我们还可以重新制定各种其他现有规则:

  • 有颜色的类型总是与任何其他类型不同(因为每个类型声明都使用全新的、从未使用过的颜色)。
  • 方法只能与着色的接收器基本类型相关联。
  • 类型的底层类型是剥离了所有颜色的类型。
    等等。

我们不称常量名称为别名,常量值称为别名常量

好点👍

我不确定有色与无色的类比是否更容易理解,但它确实表明有不止一种方法可以解释这些概念。

传统的命名/品牌/彩色类型当然需要更多解释。 特别是当可以使用现有命名类型声明命名类型时。 需要记住一些相当细微的差异。

type intSet map[int]struct{} // a new type with an underlying type map[int]struct{}

type myIntSet intSet // a new type with an underlying type map[int]struct{}

type otherIntSet = intSet // just another name (alias) for intSet, add methods to intSet (only in the same package)

type literalIntSet = map[int]struct{} // just another name for map[int]struct{}, no adding methods

不过也并非不可逾越。 假设这在 Go 1.9 中出现,我怀疑我们将看到几本 Go 书籍的第二版。 😉

我经常参考 Go 规范以获得公认的术语,所以我很好奇最后选择了哪些术语。

对于这些新创建的类型,我们确实需要一个新术语,因为现在任何类型都可以有一个名称。

一些想法:

  • “distinguished”或“distinct”(例如,可以与其他类型区分开来)
  • “独特”(例如,它是一种与所有其他类型不同的类型)
  • “具体”(例如,它是运行时中存在的实体)
  • “可识别”(例如,类型具有标识)

@bcmills我们一直在考虑杰出的、独特的、独特的、品牌的、彩色的、定义的、非别名等类型。 “具体”具有误导性,因为界面也可以着色,而界面是抽象类型的化身。 “可识别”似乎也具有误导性,因为“struct{int}”与任何显式(非别名)命名类型一样可识别。

我建议反对:

  • “有色”(在非编程环境中,“有色类型”一词带有强烈的种族偏见含义)
  • “非别名”(令人困惑,因为别名的目标可能是也可能不是以前所谓的“命名类型”)
  • “已定义”(别名也已定义,它们只是定义为别名)

“品牌”可能有用:它带有“类型如牛”的含义,但这并没有让我觉得它本质上是坏的。

到目前为止,独特和独特似乎是突出的选择。

它们简单易懂,无需大量额外的上下文或知识。 如果我不知道区别,我想我至少会对它们的含义有一个大致的了解。 对于其他选择,我不能这么说。

一旦你学会了这个术语就没有关系了,但是一个有内涵的名字可以避免不必要的障碍来内化这个区别。

这是自行车棚论证的定义。 罗伯特在https://go-review.googlesource.com/#/c/36213/上有一个待处理的 CL,这看起来非常好。

CL https://golang.org/cl/36213提到了这个问题。

我想再次提出go fix

需要明确的是,我并不是在建议“删除”别名。 也许它是一些有用的东西,适合其他工作,那是另一回事。

标题是关于移动类型的,这是非常重要的 IMO。 我不想让这个问题困惑。 我们的目标是处理项目中的某种界面更改。 当我们谈到界面的变化时,并不是希望所有用户最终都使用这两个界面(旧的和新的)一样,这就是我们说“代码逐步修复”的原因。 我们希望用户删除/更改旧的用法。

我仍然认为工具是修复代码的最佳方法,类似于@tux21b建议的想法。 例如:

$ cat "$GOROOT"/RENAME
# This file could be used for `go fix`
[package]
x/net/context=context
[type]
io.ByteBuffer=bytes.Buffer

$ go fix -rename "$GOROOT"/RENAME [packages]
# -- or --
# use a standard libraries rename table as default
$ go fix -rename [packages]
# -- or --
# include this fix as default
$ go fix [packages]

@rsc在这里但我认为在这个工作流程中不是这样:如果有一个过时的包(例如一个依赖项)使用了不推荐使用的包名称/路径,例如x/net/context ,我们可以首先修复代码,就像文档说如何通过文本格式的可配置表格将代码迁移到新版本,而不是硬编码。 然后你可以使用任何你喜欢的工具,就像新版本的 Go 一样。 有一个副作用:它会修改代码。

@LionNatsu ,我认为你是对的,但我认为这是一个单独的问题:我们是否应该采用包约定来向潜在客户解释如何以机械方式更新他们的代码以响应 API 更改? 也许吧,但我们必须弄清楚这些约定是什么。 你能针对这个话题开一个单独的问题,回到这个对话吗? 谢谢。

CL https://golang.org/cl/36691提到了这个问题。

有了这个建议,我现在可以创建这个包:

package safe

import "unsafe"

type Pointer = unsafe.Pointer

它允许程序在不直接导入unsafe情况下创建unsafe.Pointer值:

package main

import "safe"

func main() {
    x := []int{4, 9}
    y := *(*int)(safe.Pointer(uintptr(safe.Pointer(&x[0])) + 8))
    println(y)
}

原始别名声明设计文档将此称为明确支持。 在这个较新的类型别名提案中没有明确说明,但它有效。

在别名声明问题上,其理由是:_“我们允许 unsafe.Pointer 别名的原因是已经可以定义一个具有 unsafe.Pointer 作为基础类型的类型。”_ https://github.com/ golang/go/issues/16339#issuecomment -232435361

虽然这是真的,但我认为允许unsafe.Pointer的别名引入了一些新东西:程序现在可以创建unsafe.Pointer值而无需显式导入 unsafe。

为了在这个提议之前编写上面的程序,我必须将 safe.Pointer 转换成一个导入 unsafe 的包。 这可能会使审计程序的使用不安全变得有点困难。

@crawshaw ,你之前不能这样做吗?

package safe

import (
  "reflect"
  "unsafe"
)

func Pointer(p interface {}) unsafe.Pointer {
  switch v := reflect.ValueOf(p); v.Kind() {
  case reflect.Uintptr:
    return unsafe.Pointer(uintptr(v.Uint()))
  default:
    return unsafe.Pointer(v.Pointer())
  }
}

我相信这将允许编译完全相同的程序,同时在包main同样缺少导入。

(它不一定是一个有效的程序: uintptr -to- Pointer转换包括一个函数调用,所以它不符合unsafe包约束“这两次转换中必须出现在相同的表达,只有它们之间的间隔算术”。不过,我怀疑有可能构建一个等价的,有效的程序,而不用导入unsafemain通过使使用诸如reflect.SliceHeader类的东西。)

似乎导出隐藏的不安全类型只是添加到审计中的另一条规则。

是的,我想指出直接别名 unsafe.Pointer 使代码更难以审计,足以让我希望没有人最终这样做。

@crawshaw根据我的评论,在我们使用类型别名之前也是如此。 以下内容有效:

package a

import "unsafe"

type P unsafe.Pointer
package main

import "./a"
import "fmt"

var x uint64 = 0xfedcba9876543210
var h = *(*uint32)(a.P(uintptr(a.P(&x)) + 4))

func main() {
    fmt.Printf("%x\n", h)
}

也就是说,在包 main 中,即使没有unsafe包并且a.P不是别名,我也可以使用a.P进行不安全的算术。 这总是可能的。

你还指的是别的什么吗?

我的错。 我以为那行不通。 (我的印象是应用于 unsafe.Pointer 的特殊规则不会传播到从它定义的新类型。)

规范实际上并不清楚这一点。 查看 go/types 的实现,结果证明我的初始实现完全需要unsafe.Pointer ,而不仅仅是某些碰巧具有unsafe.Pointer底层类型的类型。 我刚刚发现 #6326,这是我将 go/types 更改为符合 gc 的时候。

也许我们应该禁止在常规类型定义中使用它,也禁止unsafe.Pointer别名。 我看不出有什么好的理由允许它,它确实损害了必须为不安全代码导入unsafe的明确性。

这发生过。 我认为这里不会留下任何东西。

此页面是否有帮助?
0 / 5 - 0 等级