13426109659
webmaster@21cto.com

Go 语言的设计仍然不太好

图片

导读:作者通过一些实例,来说明Go语言存在的一些设计缺点,甚至是弱智功能。

关于 Go 的一些事儿让我越来越烦恼了。

主要是因为一些东西完全没有必要。世界本来能自己更懂事,但 Go 就是这么被重复创造出来了。

错误的变量作用域和强制错误


下面是一个语言强迫你做错事的例子。最小化变量的作用域对编码的读者非常有帮助(其实代码的阅读频率比编写频率要高)。如果你能通过语法告诉读者变量只在这两行代码中使用,那就太好了。

下面是代码例子:

if err:=foo(); err!=nil{  return err}

(关于这个冗长重复的样板代码已经说得不少了,我不需要再说了。我也不特别地在意)

那看起来就没问题了。读者知道err在这里,而且只在这儿用。

但随后你会遇到这种情况:

bar,err:=foo()if err!=nil{    return err}if err=foo2();err!=nil{    return err}
[…a lot of code below…]
等等,为什么?为什么 err被foo2()函数重复使用了?是不是有什么微妙之处我没注意到?即使我们把它改成:=,我还是会疑惑,为什么err会在函数的其余部分(可能)被覆盖。

为什么?它稍后会被读取吗?

err这个变量,尤其是在查找 bug 的时候,经验丰富的程序员会发现这些问题,然后放慢速度,因为这里可能有条“大鱼”。好了,现在我又在重复使用for的这个“红鲱鱼”上浪费了几秒钟,这就是foo2()

该函数以此结尾是否是一个错误?

// Return foo99() error. (oops, that's not what we're doing)foo99()return err// This is `err` from way up there in the foo() call.
为什么范围会err超出其相关范围?

很明显,如果err 的作用域更小,代码会更容易阅读,但这在 Go 的语法上是不可能的。

这似乎根本没经过深思熟虑,直接做决定不是思考。


两种类型的 nil


跟我再来看看这些“废话代码”:

package mainimport "fmt"type linterface{}type S struct{}func main(){     var il     vars *S     fmt.PrintIn(s,i)// nil nil     fmt.Println(s==nil,i==nil,s==i)// t,t,f: They're equal, but they're not.     i=S     fmt.PrintIn(s,i)// nil nil     fmt.PrintIn(s==nil,i==nil,s==i)// t,f,t: They are not equal, but they are.}

造成这种差异的原因再次归结为:不思考,只打字。

它并不方便移植


我始终认为,在文件顶部附近添加注释来实现条件编译肯定是最愚蠢的做法。任何真正尝试过维护可移植程序的人都会告诉你,这只会带来麻烦。

这是亚里士多德设计语言的科学方法;把自己锁在房间里,永远不要用现实来检验你的假设。

问题是现在不是公元前350年。我们实际上有经验证明,除了空气阻力之外,重的物体和轻的物体下落的速度实际上是相同的。而且我们有使用便携式程序的经验,不会做这么愚蠢的事情。

如果这是公元前350年,那还可以原谅。那时我们所知的科学技术尚未发明。但这是在几十年来便携性经验广泛应用之后。

append没有明确的所有权


我们来看一下以下面的程序打印了什么?

package mainimport "fmt"
func foo(a[]string){    a=append(a,"NIGHTMARE")}
func main(){    a:=[]string{        "hello","world","!"    }
    foo(a[:1])    fmt.Println(a)}

它可能会输出[hello NIGHTMARE !]。可是谁想要这样的结果?估计没人想要。

如果结果有点牵强,来看一下这个代码怎么样?

package mainimport "fmt"func foo(a[]string){    a=append(a,"BACON","THIS","SHOULD","WORK")}func main(){    a:=[]string{"hello","world","!"}    foo(a[:1])    fmt.Println(a)}

如果你猜对了是[hello world !],那么也会对这种愚蠢编程语言的怪癖了解就比任何人都要多。图片

defer甚是愚蠢


即使在 GC 语言中,有时你也迫不及待地想要销毁某个资源。它确实需要在离开本地代码时运行,无论是通过正常返回,还是通过异常(又称 panic)。

我们想要的是 RAII,或者类似的东西。

Java 语言有这样的操作:

try(MyResourcer=new MyResource()){    /*work with resource r, which will be cleaned up when the scopeends via    .close(), not merely when the GC feels like it.    */}

Python 也有这个功能。虽然 Python几乎完全基于引用计数,可以依赖__del__终结器被调用。它很重要,使用了with语法

with MyResource() asres:some code. At end of the block __exit__ will be called on res.

Go是怎么干的?Go 会让你去阅读手册,看看这个特定的资源是否需要调用 defer 函数,以及调用哪一个。

foo,err:=myResource()if err!=nil{    return err}defer foo.Close()

这实在太笨了。有些资源需要延迟销毁,有些则不需要。是哪些资源?它好像在说祝你好运。

而且你还会经常遇到类似这种“可怕”的事情

f,err:=openFile()if err!=nil{    return nil,err}defer f.Close()if err:=f.Write(something());err!=nil{    return nil,err}if err:=f.Close();err!=nil{    return nil,err}

是的,这就是你在 Go 中安全地向文件写入内容所需要做的事情。

这是什么?第二次调用 Close()?哦,是的,当然需要。双重关闭会更安全吗?还是我的 defer 需要检查一下?它碰巧要更安全的处理os.File,但在其他情况下:谁知道呢?

标准库吞噬了异常,所以一切希望都破灭了


Go 声称它没有异常。这也让在Go 中使用异常变得极其尴尬,好像他们想惩罚使用异常的程序员。


好的,到目前为止还不错。

但所有 Go 程序员仍然必须编写异常安全的代码。因为虽然他们不使用异常,但其他代码会。

因此,你需要(而不是选择)编写如下代码:

func(f*Foo)foo(){    f.mutex.Lock()    defer f.mutex.Unlock()    f.bar()}

这愚蠢的中间函数/系统是什么鬼?这蠢透了,就像把日期放在中间一样蠢——MMDDYY,真的吗?后面再做详细吐槽。

但是他们说,恐慌会终止程序,所以为什么你要关心在程序退出前五毫秒是否解锁互斥锁呢?

因为如果某些事情吞噬了该异常并继续正常进行,而您现在却陷入了锁定的互斥锁中,该怎么办?

但肯定没人会这么做吧?合理而严格的编码标准肯定能阻止这种事发生,除非被解雇?

标准库fmt.Print在调用时执行此操作.String(),并且标准库 HTTP 服务器在 HTTP 处理程序中处理异常时执行此操作。

一切希望都破灭了。你必须编写异常安全的代码。但你不能使用异常。你只能承受异常带来的负面影响。

不要让他们欺骗了你。

有时不是 UTF-8


如果你将随机二进制数据塞入string,Go 就会顺利运行,正如本文所述。

几十年来,我因为某此工具跳过非 UTF-8 文件名而丢失过数据。我不应该因为拥有 UTF-8 出现之前命名的文件而受到指责。

嗯……我之前有。现在没了,备份/恢复的时候被悄悄跳过了。

Go 会让你继续丢失数据。或者至少,当你丢失数据时,它会这样跟你说:“嗯,数据你用的是什么编码?”

那么,当你设计一门语言时,你为什么不做些更周全的事情呢?为什么不做正确的事情,而不是做那些明显错误的简单事情呢?

内存使用


我为什么要关心内存使用情况?现在的内存比较便宜,比阅读这篇文章的时间还要便宜得多。我之所以关心,是因为我的服务运行在云实例上,而你实际上需要为内存付费。或者你运行容器,并且希望在同一台机器上运行一千个容器。你的数据可能可以装入内存中,但如果你必须为这千个容器分配 4TB 而不是 1TB 的 RAM,那么这让成本仍然很高。

您可以使用手动触发 GC 运行runtime.GC(),但“哦不,不要这样做”,他们说,“它会在需要时运行,只需相信它”。

是的,90% 的情况下,每次都有效。但有时却不行。

我用另一种语言重写了一些东西,因为随着时间的推移,Go 版本会使用越来越多的内存。

好多事情不必如此


所以,通过上面的内容我们更清楚Go语言的某些弱点。显然,这并不是关于使用符号还是英文单词的 COBOL 争论。

并且我们当时也不知道Java 的思想实现的也一般,当时我知道 Go 的想法并不太好。

到这里我们已经更了解 Go 了,但现在我们还要被糟糕的 Go 代码库所困扰。

各位感受如何?

作者:手扶托拉斯基

参考:

https://blog.habets.se/2022/08/Java-a-fractal-of-bad-experiments.html

评论