為什麼 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 年後也可以使用相同的程式碼。

結論

大腦尺寸大從來沒有讓任何大象成為諾貝爾獎得主。

對於那些個人自我優先於團隊精神的程式設計師,以及熱愛學術挑戰和無休止「自我完善」的理論家來說,這種語言確實很糟糕,因為它是一種通用的手工語言,不允許你獲得從工作成果中獲得美感愉悅,並在同事面前展現自己的專業性(前提是我們透過這些標準而不是智商來衡量智力)。 就像生活中的一切一樣,這是個人優先事項的問題。 與所有有價值的創新一樣,這種語言從普遍否認到廣泛接受已經走過了漫長的道路。 這種語言的巧妙之處就在於它的簡單,而如你所知,一切巧妙的東西都是簡單的!

來源: www.habr.com

添加評論