Go coding in go way(用Go的思維去coding)

一、序

今天我要分享的題目是“Go coding in go way”,中文含義就是用“Go語言編程思維去寫Go代碼”。看到這個題目大家不禁要問:究竟什麼是Go語言編程思維呢?關於什麼是Go語言變成思維其實並沒有官方說法。這裡要和大家交流的內容都是基於Go誕生七年多以來我個人對Go的設計者、Go team以及Go主流社區的觀點和代碼行為的整理、分析和總結。希望通過我的這次“拋磚引玉”,讓在座的Gopher們對“Go語言編程思維”有一個初步的認知,並在日常開發工作中遵循Go語言的編程思維,寫出idiomatic的Go代碼。

二、編程語言與編程思維

1、大師的觀點

在人類自然語言學界有一個很著名的假說:”薩丕爾-沃夫假說“,這個假說的內容是這樣的:語言影響或決定人類的思維方式

<code>("Language inuences/determines thought" - Sapir-Whorf hypothesis )/<code>

說到這個假說,我們不能不提及在2017年初國內上映了一部口碑不錯的美國科幻大片《降臨》,這部片子改編自雨果獎獲得者華裔科幻小說家Ted姜的《你一生的故事》,片中主線劇情的理論基礎就是就是“薩丕爾-沃夫假說”。更誇張的是片中直接將該假說應用到外星人語言上,將其擴展到宇宙範疇,片中的女主作為人類代表與外星人溝通,並學會了外星語言,從此思維大變,擁有了預知未來的“超能力”。由此我們可以看出“選擇對一門語言是多麼的重要”。奇妙的是,在編程語言界,有位大師級人物也有著與”薩丕爾-沃夫假說”異曲同工的觀點和認知。他就是首屆圖靈獎得主、著名計算機科學家Alan J. Perlis(艾倫·佩利)。他從另外一個角度提出了:“不能影響到你的編程思維方式的編程語言不值得去學習和使用”

<code>A language that doesn't aect the way you think about programming is not worth knowing./<code> 

2、現實中的“投影”

從上述大師們的理論和觀點,我們似乎看到了語言與思維之間存在著某種聯繫。那麼兩者間的這種聯繫在真實編程世界中的投影又是什麼樣子的呢?我們來看一個簡單的編程問題

[問題: 素數篩]問題描述:素數是一個自然數,它具有兩個截然不同的自然數除數:1和它本身。 要找到小於或等於給定整數n的素數。針對這個問題,我們可以採用埃拉託斯特尼素數篩算法。 算法描述:先用最小的素數2去篩,把2的倍數剔除掉;下一個未篩除的數就是素數(這裡是3)。再用這個素數3去篩,篩除掉3的倍數... 這樣不斷重複下去,直到篩完為止。

<code>func generate(ch chan   for i := 2; i < 100; i++ {
ch }
close(ch)
}
func filter(src prime, ok := if !ok {
close(dst)
return
}
fmt.Println(prime)
out := make(chan int)
go filter(out, dst)
for num := range src {
if num%prime != 0 {
out }
}
close(out)
}
//篩選素數
func Sieve() {

origin, wait := make(chan int), make(chan int) // Create a new channel.
go generate(origin)
go filter(origin, wait)
}/<code>

我想大家用Go語言實現這個編程問題一定想不到這種方式,如果C實現大家可能是命令式編程,用Java實現可能多數人是面向對象思維,Go版本的素數篩實現採用的是goroutine的併發組合。程序從2開始,依次為每個素數建立一個goroutine,用於作為篩除該素數的倍數。ch指向當前最新輸出素數所位於的篩子goroutine的源channel,這段代碼來自於Rob Pike的一次關於concurrency的分享slide

3、思考

通過上述這個現實中的問題我們可以看到:面對同一個問題,來自不同編程語言的程序員給出了思維方式截然不同的解決方法:C的命令式思維、Java的面向對象思維,和Go的併發思維。這一定程度上印證了前面的假說:編程語言影響編程思維。Go語言誕生較晚(2007年設計、2009年發佈Go1),絕大多數Gopher(包括我在內)第一語言都不是Go,都是“半路出家”從其他語言轉過來的,諸如:C、C++、Java、Python等,甚至是Javascript、Haskell、Lisp等。由於Go語言上手容易,在轉Go的初期大家很快就掌握了Go的語法。但寫著寫著,就是發現自己寫的代碼總是感覺很彆扭,並且總是嘗試在Go語言中尋找自己上一門語言中熟悉的語法元素;自己的代碼風格似乎和Go stdlib、主流Go開源項目的代碼在思考角度和使用方式上存在較大差異。而每每看到Go core team member(比如:rob pike)的一些代碼卻總有一種醍醐灌頂的趕腳。這就是我們經常說的go coding in c way、in java way、in python way等。出現這種情況的主要原因就是大腦中的原有語言的思維方式在“作祟”。我們學習和使用一門編程語言,目標就是要用這門語言思維方式去Coding。學習Go,就要用Go的編程思維去寫Go代碼,而不是用其他語言的思維方式。

4、編程語言思維的形成

人類語言如何影響人類思維這個課題自然要留給人類語言學家去破解。但編程語言如何影響編程思維,每個程序員都有著自己的理解。作為一個有著十幾年編程經驗的程序員,我認為可以用下面這幅示意圖來解釋一門編程語言思維的形成機制:

Go coding in go way(用Go的思維去coding)

決定編程語言思維的根本在於這門編程語言的價值觀!什麼是價值觀?個人認為:一門編程語言的價值觀就是這門語言的最初設計者對程序世界的認知。不同編程語言的價值觀不盡相同,導致不同編程語言採用不同的語法結構,不同語言的使用者擁有著不同的思維方式,表現出針對特定問題的不同的行為(具現為:代碼設計上的差異和代碼風格上的不同),就像上面素數篩那樣。比如:

C的價值觀摘錄:

  • 相信程序員:提供指針和指針運算,讓C程序員天馬行空的發揮
  • 自己動手,豐衣足食:提供一個很小的標準庫,其餘的讓程序員自造
  • 保持語言的短小和簡單
  • 性能優先

C++價值觀摘錄:

  • 支持多範式,不強迫程序員使用某個特定的範式
  • 不求完美,但求實用(並且立即可用)

此外,從上述模型圖,我們可以看出思維和結構影響語言應用行為,這是語言應用的核心;同時存在一個反饋機制:即語言應用行為會反過來持續影響/優化語言結構。我們常用冰山表示一個事物的表象與內涵。如果說某種語言的慣用法idiomatic tips或者best practice這些具體行為是露出水面上的冰山頭部,那麼價值觀和思維方式就是深藏在水面以下的冰山的基座

三 、Go語言價值觀的形成

從上述模型來看,編程語言思維形成的主導因素是這門編程語言的價值觀,因此我們首先來看一下Go語言價值觀的形成以及Go語言價值觀的具體內容。Go語言的價值觀的形成我覺得至少有三點因素

1、語言設計者&Unix文化

Go語言價值觀形成是與Go的初期設計者不無關係的,可以說Go最初設計者主導了Go語言價值觀的形成!這就好比一個企業的最初創始人締造企業價值觀和文化一樣。

Go coding in go way(用Go的思維去coding)

圖中是Go的三位最初設計者,從左到右分別是羅伯特·格瑞史莫、羅伯·派克和肯·湯普遜。Go初期的所有features adoption是需要三巨頭達成一致才行。三位設計者有一個共同特徵,那就是深受Unix文化薰陶。羅伯特·格瑞史莫參與設計了Java的HotSpot虛擬機和Chrome瀏覽器的JavaScript V8引擎,羅博·派克在大名鼎鼎的bell lab侵淫多年,參與了Plan9操作系統、C編譯器以及多種語言編譯器的設計和實現,肯·湯普遜更是圖靈獎得主、Unix之父。關於Unix設計哲學闡述最好的一本書莫過於埃瑞克.理曼德(Eric S. Raymond)的《UNIX編程藝術》了,該書中列舉了很多unix的哲學條目,比如:簡單、模塊化、正交、組合、pipe、功能短小且聚焦等。三位設計者將Unix設計哲學應用到了Go語言的設計當中,因此你或多或少都能在Go的設計和應用中找到這些哲學的影子

2、遺傳基因

Go併發模型CSP理論的最初提出者Tony Hoare曾提出過這樣一個觀點:The task of the programming language designer " is consolidation not innovation ". (Tony Hoare, 1973).編程語言設計者的任務不是創新,而是鞏固。

Go coding in go way(用Go的思維去coding)

和其他語言一樣,Go也是站在巨人肩膀上的。這種基因繼承,不僅僅是語法結構的繼承,還有部分價值觀的繼承和進一步認知改進。當然不可否認的是Go也有自己的微創新,比如: implicit interface implementation、首字母大小寫決定visibility等。雖然不受學院派待見,但把Go的這些微創新組合在一起,你會看到Go迸發出了強大的表現力。

3、面向新的基礎設施環境和大規模軟件開發的諸多問題

有一種開玩笑的說法:Go誕生於C++程序的漫長compile過程中。如果c++編譯很快,那麼上面的Go語言三巨頭也沒有閒暇時間一起喝著咖啡並討論如何設計一門新語言。面對時代變遷、面對新的基礎設施環境、多核多處理器平臺的出現,很多傳統語言表現出了“不適應”,這直接導致在開發大規模軟件過程中出現了各種各樣的問題,比如:構建緩慢、依賴失控、代碼風格各異、難用且複雜無法自動化的工具、跨語言構建難等。Go的設計初衷就是為了面向新環境、面向解決問題的。雖然這些問題不都是能在語言層面解決的,但這些環境和問題影響了設計者對程序世界的認知,也就影響了Go的價值觀

四、Go語言價值觀

在明確了Go價值觀的形成因素後,我認為Go語言的價值觀至少包含以下幾點:

  • Overall Simplicity 全面的簡單
  • Orthogonal Composition 正交組合
  • Preference in Concurrency 偏好併發

用一句話概括Go的價值觀(也便於記憶):Go是在偏好併發的環境下的簡單概念/事物的正交組合

<code>Go is about orthogonal composition of simple concepts with preference in concurrency./<code>

無論是Go語言設計還是Go語言使用,都受到上述價值觀的影響。接下來我們逐個來看一下Go語言價值觀主導下的Go語言編程思維,並看看每種編程思維在語言設計、標準庫實現以及主流Go開源項目中的應用體現。

我將按“價值觀”,“(價值觀下的)語言設計”,“編程思維”,“編程思維體現”的順序展開

Overall Simplicity(整體簡潔性)

Go的第一個價值觀就是”全面簡單”。圖靈獎獲得者迪傑斯特拉說過:”簡單和優雅不受歡迎,那是因為人們要想實現它們需要更努力地工作,更多自律以及領會更多的教育。” 而Go的設計者們恰恰在語言設計初期就將複雜性留給了語言自身的設計和實現階段,留給了Go core team,而將簡單留給了gopher們。因此,Simplicity價值觀更多地體現在了Go語言設計層面。 這裡簡單列舉一些:- 簡潔、正規的語法:大大降低Go入門門檻,讓來自其他語言的初學者可以輕鬆使用Go語言;

  • 僅僅25個keyword:主流編程語言中最簡單的,沒有之一;
  • “一種”代碼寫法、儘可能減少程序員付出;
  • 垃圾回收GC: 降低程序員在內存管理方面的心智負擔;
  • goroutine:提供最簡潔的併發支持;
  • constants:對傳統語言中常量定義和使用方法做進一步簡化;
  • interface:純方法集合,純行為定義,隱式實現;
  • package:Go程序結構層面的唯一組織形式,它將設計、語法、命名、構建、鏈接和測試都聚於一包中,導入和使用簡單。

如今,Go語言的簡單已經從自身設計擴展到Go應用的方方面面,但也正是由於在語言層面已經足夠簡單了,因此在應用層面,“簡單”體現的反倒不是很明顯,更加順其自然。接下來,我總結整理幾個在“全面簡單”價值觀下形成的Go編程思維,我們一起看一下。

1、short naming thought(短命名思維)

在gofmt的幫助下,Go語言一統coding style。在這樣的一個情形下,naming成了gopher們為數不多可以“自由發揮”的空間了。但對於naming,Go有著自己的思維:短命名。即在並不影響readablity的前提下,儘可能的用長度短小的標識符,於是我們經常看到用單個字母命名的變量,這與其他一些語言在命名上的建議有不同,比如Java建議遵循“見名知義”的命名原則。Go一般在上下文環境中用最短的名字攜帶足夠的信息,我們可以對比一下Java和Go:

<code>java vs. go "
index" vs. "i"
value" vs. "v"/<code>

除了短小,Go還要求儘量在一定上下文中保持命名含義的一致性,比如:在一個上下文中,我們聲明瞭兩個變量b,如果第一個b表意為buf,那麼後續的b也最好是這個含義。在命名短小和一致性方面,stdlib和知名開源項目為我們做出表率

2、minimal thought(最小思維)

碼農們是苦逼的,每天坐在電腦前一坐就是10多個小時,自己的“業餘”時間已經很少了。Go語言的設計者在這方面做的很“貼心”,考慮到為了讓男Gopher能有時間撩妹,女Gopher能有時間傍帥哥,Go語言倡導minimal thought,即儘可能的降低gopher們的投入。這種思維體現在語言設計、語言使用、相關工具使用等多個方面。比如:

  • 一種代碼風格:程序員們再也無需為coding style的個人喜好而爭論不休了,節省下來的時間專心做自己喜歡的事情
  • 一種代碼寫法(或提供最少的寫法、更簡單的寫法):你會發現,面對一個問題,大多數gopher們給出的go實現方式都類似。這就是Go“一種代碼寫法”思維的直接體現。Go在語法結構層面沒有提供給Gopher很大的靈活性。Go is not a “TMTOWTDI — There’s More Than One Way To Do It”。這點與C++、Ruby有著很大不同。
  • 顯式代碼(obvious),而不是聰明(clever)代碼:Go提倡顯而易見、可讀性好的代碼,而非充滿各種技巧或稱為“炫技”的所謂“聰明”代碼。縱觀stdlib、kubernetes等go代碼庫,都是很obvious(直觀)的go code,clever code難覓蹤跡。這樣一來,別人寫的代碼,你可以輕鬆地看懂(為你節省大量時間和精力)。這也是Go代碼中clever code比例遠遠小於其他主流語言的原因,Go不是炫技的舞臺。

C++程序員看到這裡是不是在“抹眼淚”,這裡並非黑C++,C++的複雜和多範式是客觀的,C++98標準和C++17標準的差異甚至可以用“兩門語言”來形容,用泛型的代碼和不用泛型的代碼看起來也像是兩門完全不同的語言,這種心智負擔之沉重可想而知。

Orthogonal Composition(正交組合)

正交組合是我總結出來的第二個Go語言的價值觀。如果說上一個價值觀聚焦的是Go程序提供的各種小概念實體或者說”零件”的話,那麼Composition就是在考慮如何將這些”零件”聯繫到一起,搭建程序的靜態骨架。

在Go語言設計層面,Go設計者為gopher提供了正交的語法元素,供後續組合使用,包括:

  • Go語言無類型體系(type hierarchy),類型定義正交獨立;
  • 方法和類型的正交性: 每種類型都可以擁有自己的method set;
  • interface與其實現之間無”顯式關聯”;

正交性為”組合”策略的實施奠定了基礎,提供了前提。Rob Pike曾說過: “If C++ and Java are about type hierarchies and the taxonomy(分類)of types, Go is about composition.”。組合是搭建Go程序靜態結構的主要方式。“組合”的價值觀貫穿整個語言設計和語言應用:

  • 類型間的耦合方式直接影響到程序的結構;
  • Go語言通過“組合”構架程序靜態結構;
  • 垂直組合(類型組合):Go通過 type embedding機制提供;
  • 水平組合:Go通過interface語法進行“連接”。

interface是水平組合的關鍵,好比程序肌體上的“關節”,給予連接“關節”的兩個部分各自“自由活動”的能力,而整體上又實現了某種功能。

Preference in Concurrency(偏好併發)

Go語言的第三個價值觀是偏好併發。如果說interface和組合決定了程序的靜態結構組成的話,那麼concurrency則影響著程序在執行階段的動態運行結構。從某種意義上說,Go語言就是關於concurrency和interface的設計!併發不是並行(paralell),併發是關於程序結構的,而不是關於性能的。併發讓並行更easy,並且通常性能更好;對於程序結構來說,concurrency是一個比interface組合更大的概念。併發是一種在程序執行層面上的組合:goroutines各自執行特定的工作,通過channels+select將goroutines連接起來。原生對併發的支持,讓Go語言更適應現代計算環境。併發的存在鼓勵程序員在程序設計時進行獨立計算的分解。在語言層面,Go提供了三種併發原語

  • goroutines提供併發執行, 它是Go runtime調度的基本單元;goroutine實現了異步編程模型的高效,但卻允許你使用同步範式編寫代碼,降低心智負擔;
  • goroutines被動態地多路複用到系統核心線程上以保證所有goroutines的正常運行。channels用於goroutines之間的通信和同步;channel是goroutines間建立聯繫的主要途徑或者說goroutine通過channel耦合/組合在一起,這一點channel的功用有點像Unix pipe。
  • select可以讓goroutine同時協調處理多個channel操作。


分享到:


相關文章: