为什么 Go 对于不聪明的程序员来说是不好的

这篇文章是对之前发表的一篇文章的回应 澳大利亚文章.

为什么 Go 对于不聪明的程序员来说是不好的

在过去两年多的时间里,我一直在使用 Go 来实现一个带有开发的计费系统的专用 RADIUS 服务器。 一路上,我正在学习语言本身的复杂性。 程序本身非常简单,并不是本文的目的,但使用 Go 的体验本身值得用几句话来辩护。 Go 正在成为严肃的、可扩展代码的日益主流的语言。 该语言由 Google 创建,并被广泛使用。 最重要的是,我真的认为 Go 语言的设计对于不聪明的程序员来说是不好的。

专为弱程序员设计的?

弱者谈论问题。 强者谈论想法和梦想......

Go 非常容易学习,简单到你几乎不需要任何培训就可以阅读代码。 当与非核心专家(经理、客户等)一起阅读代码时,许多跨国公司都会使用该语言的这一功能。 这对于设计驱动开发等方法非常方便。
即使是新手程序员在一两周后也开始编写相当不错的代码。 我学习的书是《Go 编程》(Mark Summerfield 着)。 这本书非常好,涉及到语言的许多细微差别。 在使用了 Java、PHP 等不必要的复杂语言之后,缺乏魔力令人耳目一新。 但迟早,很多能力有限的程序员都会产生在新领域使用旧方法的想法。 这真的有必要吗?

Rob Pike(该语言的主要思想家)创建了 Go 语言作为一种易于理解且有效使用的工业语言。 毫无疑问,该语言是为了最大程度地提高大型团队的生产力而设计的。 许多新手程序员抱怨他们缺少许多功能。 这种对简单性的渴望是语言设计者有意识的决定,为了充分理解为什么需要它,我们必须了解开发人员的动机以及他们试图在 Go 中实现的目标。

那么为什么它变得如此简单呢? 以下是罗布·派克 (Rob Pike) 的几句话:

这里的关键点是我们的程序员不是研究人员。 一般来说,他们都很年轻,是学完才来找我们的,也许他们学的是Java,或者C/C++,或者Python。 他们无法理解优秀的语言,但同时我们希望他们能够创建优秀的软件。 这就是为什么该语言应该易于理解和学习。

他应该很熟悉,大致上和C类似。 在 Google 工作的程序员很早就开始了他们的职业生涯,并且大多熟悉过程语言,特别是 C 系列。 新编程语言对快速生产力的要求意味着该语言不应该太激进。

很明智的话,不是吗?

简单的文物

简单是美的必要条件。 列夫·托尔斯泰.

保持简单是任何设计中最重要的目标之一。 如您所知,一个完美的项目不是一个没有任何东西可以添加的项目,而是一个没有任何东西可以删除的项目。 许多人认为,为了解决(甚至表达)复杂的问题,需要复杂的工具。 然而,事实并非如此。 我们以 PERL 语言为例。 语言思想家认为,程序员应该至少有三种不同的方法来解决一个问题。 Go 语言的思想家走了一条不同的道路;他们认为,只有一种方法,而且是一种非常好的方法,就足以实现目标。 这种方法有一个坚实的基础:唯一的方法是更容易学习且更难忘记。

许多移民抱怨这种语言不包含优雅的抽象概念。 是的,这是事实,但这是该语言的主要优点之一。 该语言包含最少的魔力 - 因此无需深厚的知识即可阅读该程序。 至于代码的冗长,这根本不是问题。 编写良好的 Golang 程序是垂直读取的,很少或没有结构。 此外,读取程序的速度至少比写入程序的速度大一个数量级。 如果您认为所有代码都具有统一的格式(使用内置的 gofmt 命令完成),那么读取一些额外的行根本不是问题。

不太善于表达

艺术不会容忍其自由受到限制。 准确性不是他的责任。

由于追求简单性,Go 缺乏在其他语言中被习惯使用它们的人认为是自然的结构。 起初,这可能有些不方便,但随后您会发现该程序更容易阅读,也更明确。

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

package main

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

func main() {

    flag.Parse()

    scanner := newScanner(flag.Args())

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

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

    fmt.Println(text)
}

func newScanner(flags []string) *bufio.Scanner {
    if len(flags) == 0 {
        return bufio.NewScanner(os.Stdin)
    }

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

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

    return bufio.NewScanner(file)
}

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);
    }
}

抄袭地狱

人的内心携带着地狱。 马丁路德。

初学者经常抱怨 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 main() {

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

    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

该语言有足够的手段来实现此类结构。 例如,通用编程就可以了。

package main

import "fmt"

func Eval32(list []int32, fn func(a, b int32)int32) int32 {
    var res int32
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int32Add(a, b int32) int32 {
    return a + b
}

func int32Sub(a, b int32) int32 {
    return a + b
}

func Eval64(list []int64, fn func(a, b int64)int64) int64 {
    var res int64
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int64Add(a, b int64) int64 {
    return a + b
}

func int64Sub(a, b int64) int64 {
    return a - b
}

func main() {

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

    fmt.Println(Eval32(list32, int32Add))
    fmt.Println(Eval64(list64, int64Add))
    fmt.Println(Eval64(list64, int64Sub))
}

而且,尽管我们的代码比前一个例子要长一些,但它已经变得通用化了。 因此,我们实现所有的算术运算并不困难。

许多人会说 D 语言的程序看起来短得多,他们是对的。

import std.stdio;
import std.algorithm;

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

然而,它只是更短,而不是更正确,因为 D 实现完全忽略了错误处理问题。

在现实生活中,随着逻辑复杂性的增加,差距迅速缩小。 当您需要执行无法使用标准语言运算符执行的操作时,这种差距会更快地缩小。

在可维护性、可扩展性和可读性方面,我认为 Go 语言胜出,尽管它输在了冗长上。

在某些情况下,广义编程给我们带来了不可否认的好处。 sort 包清楚地说明了这一点。 因此,要对任何列表进行排序,我们只需要实现 sort.Interface 接口。

import "sort"

type Names []string

func (ns Names) Len() int {
    return len(ns)
}

func (ns Names) Less(i, j int) bool {
    return ns[i] < ns[j]
}

func (ns Names) Swap(i, j int) {
    ns[i], ns[j] = ns[j], ns[i]
}

func main() {
    names := Names{"London", "Berlin", "Rim"}
    sort.Sort(names)
}

如果您使用任何开源项目并运行 grep “interface{}” -R 命令,您将看到使用令人困惑的接口的频率。 心胸狭隘的同志马上就会说,这一切都是因为没有仿制药。 然而,这并非总是如此。 我们以DELPHI为例。 尽管存在这些相同的泛型,但它包含一个特殊的 VARIANT 类型,用于任意数据类型的操作。 Go 语言也做同样的事情。

从麻雀上的枪

而且紧身衣必须适合疯狂的尺寸。 斯坦尼斯拉夫·莱克

许多极端粉丝可能会声称 Go 还有另一种创建泛型的机制 - 反射。 他们是对的……但只是在极少数情况下。

罗布·派克警告我们:

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

维基百科告诉我们以下内容:

反射是指程序在执行过程中监视和修改自身结构和行为的过程。 反射底层的编程范式称为反射编程。 这是一种元编程。

然而,如你所知,你必须付出一切代价。 在这种情况下是:

  • 编写程序的困难
  • 程序执行速度

因此,反射必须像大口径武器一样谨慎使用。 不经过深思熟虑地使用反射会导致程序不可读、错误不断且速度低下。 对于一个势利的程序员来说,这正是能够在其他更务实、更谦虚的同事面前炫耀他的代码的事情。

习近平的文化包袱? 不,来自多种语言!

除了财富之外,债务也留给了继承人。

尽管许多人认为该语言完全基于 C 遗产,但事实并非如此。 该语言融合了最佳编程语言的许多方面。

句法

首先,语法结构的语法是基于C语言的语法。 然而,DELPHI 语言也产生了重大影响。 这样,我们看到大大降低程序可读性的多余括号已经被完全删除了。 该语言还包含 DELPHI 语言固有的“:=”运算符。 包的概念是借用自 ADA 等语言。 未使用实体的声明是从 PROLOG 语言借用的。

语义学

这些包基于 DELPHI 语言的语义。 每个包都封装了数据和代码,并包含私有和公共实体。 这允许您将包接口减少到最少。

委托方法的实现操作借鉴了DELPHI语言。

汇编

有一个笑话并非没有道理:Go 是在编译 C 程序时开发的。 该语言的优势之一是其超快的编译速度。 这个想法是从 DELPHI 语言借来的。 每个Go包对应一个DELPHI模块。 仅在确实需要时才重新编译这些包。 因此,在下次编辑后,您不需要编译整个程序,而只需重新编译更改的包和依赖于这些更改的包的包(即使如此,仅当包接口已更改时)。

高级构造

该语言包含许多不同的高级结构,这些结构与 C 等低级语言没有任何关系。

  • 字符串
  • 哈希表
  • 切片
  • 鸭子类型是从 RUBY 等语言借用的(不幸的是,许多人没有理解或充分利用 RUBY 的潜力)。

内存管理

内存管理通常值得单独撰写一篇文章。 如果说在C++这样的语言中,控制权完全留给了开发者,那么在后来的DELPHI这样的语言中,就使用了引用计数模型。 使用这种方法,不允许循环引用,因为形成了孤儿簇,然后 Go 内置了此类簇的检测(如 C#)。 此外,垃圾收集器比大多数当前已知的实现更高效,并且已经可以用于许多实时任务。 语言本身可以识别可以在堆栈上分配存储变量的值的情况。 这减少了内存管理器的负载并提高了程序的速度。

并发与并发

该语言的并行性和竞争力令人赞叹不已。 没有一种低级语言可以与 Go 进行远程竞争。 公平地说,值得注意的是,该模型并不是该语言的作者发明的,而只是从古老的 ADA 语言借用的。 该语言能够使用所有 CPU 处理数百万个并行连接,同时具有多线程代码典型的死锁和竞争条件等复杂问题的数量级。

额外的好处

如果有利可图,每个人都会变得无私。

语言还为我们提供了许多无疑的好处:

  • 构建项目后的单个可执行文件大大简化了应用程序的部署。
  • 即使不编写测试,静态类型和类型推断也可以显着减少代码中的错误数量。 我认识一些程序员,他们根本不编写测试,而且他们的代码质量并没有受到太大影响。
  • 非常简单的交叉编译和标准库优秀的可移植性,极大地简化了跨平台应用程序的开发。
  • RE2 正则表达式是线程安全的并且具有可预测的执行时间。
  • 一个强大的标准库,允许大多数项目无需第三方框架。
  • 该语言足够强大,可以专注于问题而不是如何解决问题,但又足够低级,可以有效地解决问题。
  • Go 生态系统已经包含适用于所有场合的开箱即用的开发工具:测试、文档、包管理、强大的 linter、代码生成、竞争条件检测器等。
  • Go 1.11 版本引入了内置语义依赖管理,构建在流行的 VCS 托管之上。 构成 Go 生态系统的所有工具都使用这些服务来一次性下载、构建和安装代码。 那太好了。 随着1.11版本的到来,包版本控制的问题也得到了彻底的解决。
  • 因为该语言的核心思想是减少魔法,所以该语言激励开发人员显式地进行错误处理。 这是正确的,因为否则,它会完全忘记错误处理。 另一件事是,大多数开发人员故意忽略错误处理,宁愿简单地将错误向上转发而不是处理它们。
  • 该语言没有实现经典的 OOP 方法,因为其纯粹的形式在 Go 中不存在虚拟性。 然而,使用接口时这不是问题。 面向对象编程的缺失大大降低了初学者的入门门槛。

简单有利于社区利益

复杂化容易,简单化难。

Go 的设计很简单,并且它成功地实现了这一目标。 它是为那些了解团队合作的好处并且厌倦了企业级语言的无尽变化的聪明程序员编写的。 它的武器库中拥有相对较小的句法结构,实际上不会随着时间的推移而发生变化,因此开发人员可以腾出大量时间进行开发,而不是无休止地研究语言创新。

公司还获得了许多优势:较低的进入门槛使他们能够快速找到专家,并且语言的不变性使他们即使在 10 年后也可以使用相同的代码。

结论

大脑尺寸大从来没有使任何大象成为诺贝尔奖获得者。

对于那些个人自我优先于团队精神的程序员,以及热爱学术挑战和无休止“自我完善”的理论家来说,这种语言确实很糟糕,因为它是一种通用的手工语言,不允许你获得从工作成果中获得审美愉悦,并在同事面前表现出自己的专业性(前提是我们通过这些标准而不是智商来衡量智力)。 就像生活中的一切一样,这是个人优先事项的问题。 与所有有价值的创新一样,这种语言从普遍否认到广泛接受已经走过了漫长的道路。 这种语言的巧妙之处就在于它的简单,而正如你所知,一切巧妙的东西都是简单的!

来源: habr.com

添加评论