为什么 Go 设计不适合聪明的程序员

在过去的几个月里,我一直在使用 Go 进行实现。 概念验证 (约。:用代码来测试某个想法的功能)在空闲时间,部分时间是为了研究编程语言本身。 程序本身非常简单,不是本文的目的,但使用 Go 的体验本身值得多说几句。 Go 承诺是 (约。:2015 年撰写的文章)是一种用于严肃可扩展代码的流行语言。 该语言由 Google 创建,并被广泛使用。 最重要的是,我真的认为 Go 语言的设计对于聪明的程序员来说是不好的。

专为弱程序员设计的?

Go 非常容易学习,简单到我花了一个晚上才完成介绍,之后我已经可以高效地编写代码了。 我以前学Go的书叫 Go 编程简介 (翻译),可以在线获取。 这本书和 Go 源代码本身一样,很容易阅读,有很好的代码示例,大约有 150 页,可以一口气读完。 这种简单性起初令人耳目一新,尤其是在充满过于复杂技术的编程世界中。 但最终,迟早会出现这样的想法:“真的是这样吗?”

谷歌声称 Go 的简单性是它的卖点,并且该语言是为了最大程度地提高大型团队的生产力而设计的,但我对此表示怀疑。 有些功能要么缺失,要么过于详细。 这一切都是因为对开发人员缺乏信任,认为他们无法做正确的事情。 这种对简单性的渴望是语言设计者有意识的决定,为了充分理解为什么需要它,我们必须了解开发人员的动机以及他们试图在 Go 中实现的目标。

那么为什么它变得如此简单呢? 这里有几句话 罗布·派克 (约。:Go 语言的共同创造者之一):

这里的关键点是我们的程序员(约。: 谷歌人)不是研究人员。 一般来说,他们都很年轻,是学完才来找我们的,也许他们学的是Java,或者C/C++,或者Python。 他们无法理解优秀的语言,但同时我们希望他们能够创建优秀的软件。 这就是为什么他们的语言应该易于理解和学习。
 
他应该很熟悉,大致上和C类似。 在 Google 工作的程序员很早就开始了他们的职业生涯,并且大多熟悉过程语言,特别是 C 系列。 新编程语言对快速生产力的要求意味着该语言不应该太激进。

什么? 所以 Rob Pike 基本上是说 Google 的开发人员没那么优秀,这就是为什么他们为白痴创建了一种语言(约。:愚蠢)这样他们就能够做某事。 用什么傲慢的眼光看待自己的同事? 我一直相信 Google 的开发人员都是从地球上最聪明、最优秀的人中精心挑选出来的。 他们肯定能处理更困难的事情吗?

过于简单的文物

在任何设计中,简单都是一个值得追求的目标,但试图让事情变得简单却很困难。 然而,当尝试解决(甚至表达)复杂问题时,有时需要复杂的工具。 复杂性和复杂性并不是编程语言的最佳特性,但有一个中间立场,该语言可以创建易于理解和使用的优雅抽象。

不太善于表达

由于其对简单性的承诺,Go 缺乏在其他语言中被认为是自然的结构。 乍一看这似乎是个好主意,但实际上它会导致冗长的代码。 这样做的原因应该很明显——开发人员需要轻松阅读其他人的代码,但事实上这些简化只会损害可读性。 Go 中没有缩写:要么很多,要么什么都没有。

例如,从命令行参数读取 stdin 或文件的控制台实用程序如下所示:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()
    flags := flag.Args()

    var text string
    var scanner *bufio.Scanner
    var err error

    if len(flags) > 0 {

        file, err := os.Open(flags[0])

        if err != nil {
            log.Fatal(err)
        }

        scanner = bufio.NewScanner(file)

    } else {
        scanner = bufio.NewScanner(os.Stdin)
    }

    for scanner.Scan() {
        text += scanner.Text()
    }

    err = scanner.Err()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

尽管此代码也试图尽可能通用,但 Go 的强制冗长会妨碍,因此,解决一个简单的问题会导致大量代码。

例如,这里是同一问题的解决方案 D:

import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

现在谁更具可读性? 我会把票投给 D。他的代码更具可读性,因为他更清楚地描述了操作。 D 使用更复杂的概念(约。: 替代函数调用 и 模式)比 Go 示例中的要好,但理解它们实际上并不复杂。

抄袭地狱

改进 Go 的一个流行建议是通用性。 这至少有助于避免不必要的代码复制以支持所有数据类型。 例如,对整数列表求和的函数只能通过为每个整数类型复制粘贴其基本函数来实现;没有其他方法:

package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int16Sum(list []int16) (uint64) {
    var result int16 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int8Sum(list []int8) (uint64) {
    var result int8 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list8  := []int8 {1, 2, 3, 4, 5}
    list16 := []int16{1, 2, 3, 4, 5}
    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int8Sum(list8))
    fmt.Println(int16Sum(list16))
    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

这个例子甚至不适用于有符号类型。 这种做法完全违背了不重复自己的原则(干性),最著名和最明显的原则之一,忽视它是许多错误的根源。 Go 为什么要这样做? 这是语言的一个可怕的方面。

D 上的相同示例:

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

简单、优雅、开门见山。 这里使用的函数是 reduce 对于模板类型和谓词。 是的,这又比 Go 版本更复杂,但对于聪明的程序员来说并不难理解。 哪个示例更易于维护且易于阅读?

简单型系统旁路

我想 Go 程序员读到这篇文章一定会口吐白沫,尖叫道:“你做错了!” 好吧,还有另一种方法可以创建泛型函数和类型,但它完全破坏了类型系统!

看一下这个愚蠢的语言修复示例来解决该问题:

package main

import "fmt"
import "reflect"

func Reduce(in interface{}, memo interface{}, fn func(interface{}, interface{}) interface{}) interface{} {
    val := reflect.ValueOf(in)

    for i := 0; i < val.Len(); i++ {
        memo = fn(val.Index(i).Interface(), memo)
    }

    return memo
}

func main() {

    list := []int{1, 2, 3, 4, 5}

    result := Reduce(list, 0, func(val interface{}, memo interface{}) interface{} {
        return memo.(int) + val.(int)
    })

    fmt.Println(result)
}

本次实施 Reduce 是从文章借来的 Go 中惯用的泛型 (约。:我找不到翻译,如果你能帮忙的话我会很高兴)。 好吧,如果它是惯用的,我不想看到一个非惯用的例子。 用法 interface{} - 一场闹剧,在语言中只需要绕过打字。 这是一个空接口,所有类型都实现它,为每个人提供完全的自由。 这种编程风格非常丑陋,但这还不是全部。 像这样的杂技技巧需要使用运行时反射。 正如罗伯·派克 (Rob Pike) 在一份报告中提到的那样,他也不喜欢滥用此功能的人。

这是一个强大的工具,应谨慎使用。 除非绝对必要,否则应避免这样做。

我会采用 D 模板而不是这些废话。 怎么会有人这么说 interface{} 更具可读性甚至类型安全?

依赖管理的困境

Go 有一个内置的依赖系统,构建在流行的托管提供商之上 VCS。 Go 附带的工具了解这些服务,并且可以一次性下载、构建和安装其中的代码。 虽然这很棒,但版本控制存在一个重大缺陷! 是的,确实可以使用 Go 工具从 github 或 bitbucket 等服务获取源代码,但无法指定版本。 再次,简单性是以牺牲实用性为代价的。 我无法理解这样的决定的逻辑。

在提出有关此问题的解决方案的问题后,Go 开发团队创建了 论坛主题,其中概述了他们将如何解决这个问题。 他们的建议是有一天将整个存储库复制到您的项目中,然后“按原样”保留。 他们到底在想什么? 我们拥有令人惊叹的版本控制系统,具有出色的标记和版本支持,Go 创建者会忽略这些系统并仅复制源代码。

习近平的文化包袱

在我看来,Go 是由那些一生都使用 C 的人和那些不想尝试新事物的人开发的。 该语言可以描述为带有额外轮子的 C(原来。: 训练轮)。 除了支持并行性(顺便说一句,这很棒)之外,它没有新的想法,这是一种耻辱。 你在一种几乎不可用、蹩脚的语言中拥有出色的并行性。

另一个棘手的问题是 Go 是一种过程语言(就像 C 语言的无声恐怖一样)。 您最终会以一种感觉陈旧且过时的过程式风格编写代码。 我知道面向对象编程不是灵丹妙药,但如果能够将细节抽象为类型并提供封装,那就太好了。

为了您自己的利益而简单

Go 的设计很简单,并且它成功地实现了这一目标。 它是为弱程序员编写的,使用旧语言作为模板。 它配备了简单的工具来完成简单的事情。 它易于阅读且易于使用。

它非常冗长、不起眼,而且对聪明的程序员来说很糟糕。

谢谢 梅尔辛瓦尔德 用于编辑

来源: habr.com

添加评论