為什麼 Go 設計不適合聰明的程式設計師

在過去的幾個月裡,我一直在使用 Go 進行實作。 概念證明 (約。:用程式碼來測試某個想法的功能)在空閒時間,部分時間是為了研究程式語言本身。 程式本身非常簡單,不是本文的目的,但使用 Go 的體驗本身就值得多說幾句。 Go 承諾是 (約。:2015 年撰寫的文章)是一種用於嚴肅可擴展程式碼的流行語言。 該語言由 Google 創建,並被廣泛使用。 最重要的是,我真的認為 Go 語言的設計對於聰明的程式設計師來說是不好的。

專為弱程式設計師設計的?

Go 非常容易學習,簡單到我花了一個晚上才完成介紹,之後我已經可以有效率地編寫程式碼了。 我以前學Go的書叫 Go 程式設計簡介 (翻譯),可以在線獲取。 這本書和 Go 原始碼本身一樣,很容易閱讀,有很好的程式碼範例,包含大約​​ 150 頁,可以一口氣讀完。 這種簡單性起初令人耳目一新,尤其是在充滿過於複雜技術的程式設計世界中。 但最終,遲早會出現這樣的想法:“真的是這樣嗎?”

Google聲稱 Go 的簡單性是它的賣點,而語言是為了最大程度地提高大型團隊的生產力而設計的,但我對此表示懷疑。 有些功能要么缺失,要么過於詳細。 這一切都是因為對開發人員缺乏信任,認為自己無法做正確的事。 這種對簡單性的渴望是語言設計者有意識的決定,為了充分理解為什麼需要它,我們必須了解開發人員的動機以及他們試圖在 Go 中實現的目標。

那麼為什麼它變得如此簡單呢? 這裡有幾句話 羅布派克 (約。:Go 語言的共同創造者之一):

這裡的關鍵點是我們的程式設計師(約。: Google人)不是研究人員。 一般來說,他們都很年輕,學完才來找我們的,也許他們學的是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))
}

這個例子甚至不適用於有符號類型。 這種做法完全違背了不重複自己的原則(DRY),最著名和最明顯的原則之一,忽視它是許多錯誤的根源。 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 的設計很簡單,而且它成功地實現了這一目標。 它是為弱程式設計師編寫的,使用舊語言作為模板。 它配備了簡單的工具來完成簡單的事情。 它易於閱讀且易於使用。

它非常冗長、不起眼,而且對聰明的程式設計師來說很糟糕。

謝謝 梅爾辛瓦爾德 用於編輯

來源: www.habr.com

添加評論