Go拒绝未使用的依赖的利弊

huangapple go评论104阅读模式
英文:

Pros and cons of Go rejecting unused dependencies

问题

谷歌的新语言Go试图通过明确要求模块中列出的所有依赖项实际上都被使用来简化依赖管理。编译器将拒绝声明依赖于一个模块但未使用该模块任何内容的模块。

> 一个包导入自身或导入一个包而不引用其任何导出标识符是非法的。

我可以想到一些明显的优点(例如更清晰的模块),但也许还有一些不明显的优点。我能想到的唯一的缺点是过于拘谨的编译器,在重构过程中投诉过多,但也许还有其他的缺点?

你有没有使用其他语言来强制执行这一点的经验?这种方法的利弊是什么?

英文:

Google's new language Go tries to make dependencies management easier by explicitly requiring that all dependencies listed in a module actually be used. The compiler will reject a module that declares a dependency to a module without using anything from that module.

> It is illegal for a package to import itself or to import a package without referring to any of its exported identifiers.

I can think of some obvious advantages (e.g. cleaner modules) but maybe there are some non-obvious ones. The only disadvantage I can think of is having an overly pedantic compiler, complaining too much during refactoring, but maybe there are more?

Do you have any experience with other languages enforcing this? What are the pros and cons of this approach?

答案1

得分: 5

不仅需要明确使用所有依赖项,还必须使用所有变量。当您有未使用的变量时,编译器会给出错误提示。

它们很烦人。但这会让其他人感到高兴,因为他们可以得到整洁的代码。

我认为Go的设计者可能打算将Go作为一种在很大程度上依赖于集成开发环境的语言。

英文:

Not only you need to explicitly use all dependencies, but also all variables must be used. The compiler will give you errors when you have unused variables.

They are annoying. But it will make others happy because they get clean code.

I'm thinking that probably Go designers intended Go to be a language that is largely IDE dependent.

答案2

得分: 1

我猜最大的动力和最大的结果都是编译时间的改善。技术预览视频中特别强调了他们在短时间内编译大量代码的能力(他们的示例是在MacBook上8秒内编译了20万行代码 - 没有给出机器规格)。

在技术视频中,他们特别提到他们实现这一点的最大方法之一是改变了模块的编译和链接方式。

这里是一个当前C/C++系统中的示例:

类A在头文件C_H中定义,在C_CPP中实现。
类B从C派生,在B_H和B_CPP文件中实现。
类A从B派生,在A_H和A_CPP文件中实现。

由于派生链的存在,A_CPP包含了A_H、B_H和C_H。B_CPP包含了B_H和C_H。C_CPP包含了C_H。由于C预处理器的性质,它实际上将#include转换为剪切和粘贴操作,因此C_H的内容会通过编译器运行3次,B_H会运行2次。

此外,A_CPP、B_CPP和C_CPP的内容都存在于各自的目标文件中。因此,当链接器解析A.o时,它被迫加载和处理B.o和C.o。同样,当它解析B.o时,它必须再次处理C.o。

预编译头文件可以在解决这个问题的第一部分上有所帮助,但是它们也可能很难维护,我知道很多开发人员因此而不使用它们。它也没有从根本上改变问题 - 头文件仍然在多个级别上多次处理,只是现在处理的是预编译的二进制文件。虽然省略了几个步骤,但并没有完全省略整个过程。

Go的处理方式不同。用他们技术讲座中的话来说:

> “Go编译器从目标文件中提取传递依赖的类型信息 - 但只提取所需的信息。如果A.go依赖于B.go依赖于C.go:
> - 先编译C.go,然后是B.go,最后是A.go。
> - 要编译A.go,编译器读取的是B.o而不是C.o。在大规模情况下,这可以大大加快速度。”

好了,稍微偏离一下。为什么这个相关呢?答案也在Go技术讲座的PDF中:

> “包模型:显式依赖以实现更快的构建。”

我猜Go开发人员的立场是,当编译时间以秒计算时,即使对于非常大的项目,让开发人员保持这么短的编译时间也更加高效。假设我花了8秒钟来编译20万行代码,并发现我有一个多余的包导入,再花5-10秒钟(使用良好的IDE或熟悉的开发环境)找到并修复它,然后再花8秒钟重新编译。总共30秒 - 现在我的未来编译都在10秒的范围内。或者我们可以让我们的模块通过包含不必要的依赖项来增长,然后看着编译时间从8秒增加到10秒、12秒或15秒。这看起来不像什么,因为我们都习惯了几分钟的编译时间 - 但是当你开始意识到这是25%的性能下降时,你会停下来思考一分钟。

Go的编译时间已经非常快了。还要考虑到处理器速度仍在增加(尽管不如过去那么多),可用核心数也在增加(而编译大量代码非常适合多线程)。今天8秒内编译20万行代码意味着在10年内,想象20万行代码几乎可以瞬间编译完成也不是不合理的。我认为Go团队在这里做出了一个有意识的决定,让编译时间成为过去的事情,虽然你提出的问题只是其中的一小部分,但它仍然是其中的一部分。

另外,Go团队似乎还形成了一种强制一些良好编程实践的语言设计哲学。值得赞扬的是,他们努力实现了这一点,而且在很大程度上取得了成功。[顺便说一句:我能想到的唯一两个实际影响性能的事情是垃圾回收和强制初始化变量 - 而后者在今天已经相当微不足道了。]这将会让一些程序员非常恼火,同时也会让其他人感到高兴。这是编程世界中一个古老的争论,很明显Go站在了哪一边,不管你喜欢与否。

我认为这两个因素共同影响了他们的决定,我认为最终这是一种好的方式,尽管我支持其他评论者的建议,在项目生命周期的早期阶段允许使用“--strict”标志或类似的方式使这种特定行为可选。我可以很容易地想象自己在开始编写代码时定义变量或包含包,即使我还没有编写需要它们的代码。

英文:

I'm guessing that the biggest motivation for this, and the biggest result, is an improvement in compile times. The tech preview video made a major point of their ability to compile large amounts of code in short time periods (their example was 200,000 lines of code in 8 seconds on a MacBook - no machine specs given).

Also in the tech video, they specifically mention that one of the biggest ways they achieved that was in changing the way modules were compiled and linked.

Here's an example of how something would work in a current C/C++ system:

Class A is defined in header file C_H and implemented in C_CPP.
Class B derives from C and is implemented in files B_H and B_CPP.
Class A derives from B and is implemented in files A_H and A_CPP.

Because of the derivation chains, A_CPP includes A_H, B_H, and C_H. B_CPP includes B_H and C_H. C_CPP includes C_H. Due to the nature of the C preprocessor, which essentially turns a #include into a cut and paste operation, the contents of C_H are run through the compiler 3 times and B_H is run through twice.

Further, the contents of A_CPP, B_CPP, and C_CPP all live in their own object files. Hence when the linker goes to resolve A.o, it is forced to load and process both B.o and C.o. Also, when it resolves B.o, it has to process C.o again.

Precompiled headers can help quite a bit with the first part of this problem, but they also can be a royal pain to maintain and I know a lot of developers who simply don't use them for that reason. It also doesn't fundamentally change the problem - the headers are still processed at several levels multiple times, only now a precompiled binary is processed instead of the source. Several steps are cut out, but not the entire process.

Go approaches things differently. In the words straight out of the PDF from their tech talk:

> "The Go compiler pulls transitive
> dependency type info from the object
> file - but only what it needs. If A.go
> depends on B.go depends on C.go:
> - compile C.go, B.go, then A.go.
> - to compile A.go, compiler reads B.o not C.o. At scale, this can be a huge
> speedup."

OK, slight tangent is done. Why is that relevant? The answer is also in the Go Tech Talk PDF:

> "Package model: Explicit dependencies
> to enable faster builds."

I'm guessing that the Go developers took the stance that when compile times are measured in seconds, even for very large projects, that it's more productive for developers to keep compile times that short. Say it takes me 8 seconds to compile 200,000 lines of code and discover that I've got an extraneous package import, 5-10 seconds (with a good IDE, or good familiarity with your dev environment) to find it and fix it, and another 8 seconds to recompile. Call it 30 seconds total - and now all of my future compiles stay in the 10 second range. Or we can let our module grow by including unnecessary dependencies, and watch that compile time creep up from 8 to 10, 12, or 15 seconds. It doesn't seem like much because we're all used to compile times on the order of minutes - but when you start to realize that that's a 25% performance degradation, you stop and think about it for a minute.

Go compile times are already lightning fast. Consider also that processor speeds are still increasing (if not as much as in the past) and that the number of cores available is also increasing (and compiling large amounts of code is well suited to multi-threading). 200,000 lines of code in 8 seconds today means that it's not unreasonable to imagine 200,000 lines of code compiling essentially instantaneously in 10 years. I think the Go team has made a conscious decision here to make compile times a thing of the past, and while the issue you bring up is only a very small part of that, it is still a part of that.

On another note entirely, the Go team also seems to have developed a philosophy of a language design that forces some good programming practices. To their credit, they've made the effort to achieve that without serious performance penalties, and largely succeeded. [Aside: The only two things I can think of offhand that actually impact performance are garbage collection and forcibly initialized variables - and the latter is pretty trivial in this day and age.] This is going to royally irritate some programmers, while making others happy. It's an old, old argument in the programming world, and it's pretty clear which side Go has come down on, like it or not.

I think the two forces together influenced their decision, and I think that at the end of the day it's a good way to go, although I support other commenters here who have suggested allowing a "--strict" flag or some such to make this particular behavior optional, particularly during the early stages of a project's lifecycle. I could easily see myself defining variables or including packages when I first start writing code that I know I will need later, even though I haven't yet written the code that needs them.

答案3

得分: 0

正如yuku所提到的,如果你有一个与Netbeans和Eclipse在Java方面相媲美的IDE,你就不需要关心这种事情。

在边缘的小灯泡上右键单击,然后选择“删除所有未使用的依赖项”。

对于未使用的变量,它们通常会有一个波浪线下划线,并且很容易被发现。

唯一的区别是,与其他语言不同,你实际上有编译器和IDE一起抱怨,但如果你无论如何都使用IDE,这就成为一个非问题。

在工作中,我们有编码政策,基本上规定我们必须为其他语言做同样的事情(自己做),当然还有其他事情。所以我认为这种事情实际上确实有真实的应用。尽管在我看来,编译器应该给开发人员提供切换此行为的选项。严格模式,有人要吗?

英文:

As mentioned by yuku, if you have got an IDE that is on par with what Netbeans and Eclipse can do for java, you don't really have to care about this sort of thing.

Right click on the little light bulb in the margin and select "Remove all unused dependencies".

For variables that aren't used, they usually get a squiggly underline, and are pretty easy to spot.

The only difference here is that unlike other languages, you actually have the compiler complaining in addition to the IDE but if you use an IDE anyway, this becomes a non-issue.

At work we have coding policies that pretty much state that we have to do the same thing (ourselves), among other things, for other languages of course. So I would say that this sort of this does actually have real applications. Though IMHO, the compiler should give the developer the option to toggle this behaviour on and off. Strict mode anyone?

huangapple
  • 本文由 发表于 2009年12月1日 19:36:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/1825611.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定