1. 程式人生 > >[譯]Go語言最佳實戰[一]

[譯]Go語言最佳實戰[一]

Go 語言實戰: 編寫可維護 Go 語言程式碼建議

目錄

  • 1. 指導原則

    • 1.1 簡單性

    • 1.2 可讀性

    • 1.3 生產力

  • 2. 識別符號

    • 2.1 選擇標識是為了清晰, 而不是簡潔

    • 2.2 識別符號長度

    • 2.3 不要用變數型別命名變數

    • 2.4 使用一致的命名風格

    • 2.5 使用一致的宣告樣式

    • 2.6 成為團隊的合作者

  • 3. 註釋

    • 3.1 關於變數和常量的註釋應描述其內容而非其目的

    • 3.2 公共符號始終要註釋

  • 4. 包的設計

    • 4.1 一個好的包從它的名字開始

    • 4.2 避免使用類似 base、 common或 util的包名稱

    • 4.3 儘早 return而不是深度巢狀

    • 4.4 讓零值更有用

    • 4.5 避免包級別狀態

  • 5. 專案結構

    • 5.1 考慮更少,更大的包

    • 5.2 保持 main包內容儘可能的少

  • 6. API 設計

    • 6.1 設計難以被誤用的 API

    • 6.2 為其預設用例設計 API

    • 6.3 讓函式定義它們所需的行為

  • 7. 錯誤處理

    • 7.1 通過消除錯誤來消除錯誤處理

    • 7.2 錯誤只處理一次

  • 8. 併發

    • 8.1 保持自己忙碌或做自己的工作

    • 8.2 將併發性留給呼叫者

    • 8.3 永遠不要啟動一個停止不了的 goroutine

介紹

大家好,
我在接下來的兩個會議中的目標是向大家提供有關編寫 Go 程式碼最佳實踐的建議。

這是一個研討會形式的演講,不會有幻燈片, 而是直接從文件開始。

貼士: 在這裡有最新的文章連結
https://dave.cheney.net/practical-go/presentations/qcon-china.html

編者的話

  • 終於翻譯完了 Dave 大神的這一篇《Go 語言最佳實踐

  • 耗時兩週的空閒時間

  • 翻譯的同時也對 Go 語言的開發與實踐有了更深層次的瞭解

  • 有興趣的同學可以翻閱 Dave 的另一篇博文《SOLID Go 語言設計》(第六章節也會提到)

正文

1. 指導原則

如果我要談論任何程式語言的最佳實踐,我需要一些方法來定義 “什麼是最佳”。 如果你昨天來到我的主題演講,你會看到 Go 團隊負責人 Russ Cox 的這句話:

Software engineering is what happens to programming when you add time and other programmers. (軟體工程就是你和其他程式設計師花費時間在程式設計上所發生的事情。)
— Russ Cox

Russ 作出了軟體程式設計與軟體工程的區分。 前者是你自己寫的一個程式。 後者是很多人會隨著時間的推移而開發的產品。 工程師們來來去去,團隊會隨著時間增長與縮小,需求會發生變化,功能會被新增,錯誤也會得到修復。 這是軟體工程的本質。

我可能是這個房間裡 Go 最早的使用者之一,~ 但要爭辯說我的資歷給我的看法更多是假的~。 相反,今天我要提的建議是基於我認為的 Go 語言本身的指導原則:

  1. 簡單性

  2. 可讀性

  3. 生產力

注意:
你會注意到我沒有說效能或併發。 有些語言比 Go 語言快一點,但它們肯定不像 Go 語言那麼簡單。 有些語言使併發成為他們的最高目標,但它們並不具有可讀性及生產力。
效能和併發是重要的屬性,但不如簡單性,可讀性和生產力那麼重要。

1.1. 簡單性

我們為什麼要追求簡單? 為什麼 Go 語言程式的簡單性很重要?

我們都曾遇到過這樣的情況: “我不懂這段程式碼”,不是嗎? 我們都做過這樣的專案: 你害怕做出改變,因為你擔心它會破壞程式的另一部分; 你不理解的部分,不知道如何修復。

這就是複雜性。 複雜性把可靠的軟體中變成不可靠。 複雜性是殺死軟體專案的罪魁禍首。

簡單性是 Go 語言的最高目標。 無論我們編寫什麼程式,我們都應該同意這一點: 它們很簡單。

1.2. 可讀性

Readability is essential for maintainability.
(可讀性對於可維護性是至關重要的。)
— Mark Reinhold (2018 JVM 語言高層會議)

為什麼 Go 語言的程式碼可讀性是很重要的?我們為什麼要爭取可讀性?

Programs must be written for people to read, and only incidentally for machines to execute. (程式應該被寫來讓人們閱讀,只是順便為了機器執行。)
— Hal Abelson 與 Gerald Sussman (計算機程式的結構與解釋)

可讀性很重要,因為所有軟體不僅僅是 Go 語言程式,都是由人類編寫的,供他人閱讀。執行軟體的計算機則是次要的。

程式碼的讀取次數比寫入次數多。一段程式碼在其生命週期內會被讀取數百次,甚至數千次。

The most important skill for a programmer is the ability to effectively communicate ideas. (程式設計師最重要的技能是有效溝通想法的能力。)
— Gastón Jorquera [1]

可讀性是能夠理解程式正在做什麼的關鍵。如果你無法理解程式正在做什麼,那你希望如何維護它?如果軟體無法維護,那麼它將被重寫; 最後這可能是你的公司最後一次投資 Go 語言。

~ 如果你正在為自己編寫一個程式,也許它只需要執行一次,或者你是唯一一個曾經看過它的人,然後做任何對你有用的事。~ 但是,如果是一個不止一個人會貢獻編寫的軟體,或者在很長一段時間內需求、功能或者環境會改變,那麼你的目標必須是你的程式可被維護。

編寫可維護程式碼的第一步是確保程式碼可讀。

1.3. 生產力

Design is the art of arranging code to work today, and be changeable forever. (設計是安排程式碼到工作的藝術,並且永遠可變。)
— Sandi Metz

我要強調的最後一個基本原則是生產力。開發人員的工作效率是一個龐大的主題,但歸結為此; 你花多少時間做有用的工作,而不是等待你的工具或迷失在一個外國的程式碼庫裡。Go 程式設計師應該覺得他們可以通過 Go 語言完成很多工作。

有人開玩笑說,Go 語言是在等待 C ++ 語言程式編譯時設計的。快速編譯是 Go 語言的一個關鍵特性,也是吸引新開發人員的關鍵工具。雖然編譯速度仍然是一個持久的戰場,但可以說,在其他語言中需要幾分鐘的編譯,在 Go 語言中只需幾秒鐘。這有助於 Go 語言開發人員感受到與使用動態語言的同行一樣的高效,而且沒有那些語言固有的可靠性問題。

對於開發人員生產力問題更為基礎的是,Go 程式設計師意識到編寫程式碼是為了閱讀,因此將讀程式碼的行為置於編寫程式碼的行為之上。 Go 語言甚至通過工具和自定義強制執行所有程式碼以特定樣式格式化。這就消除了專案中學習特定格式的摩擦,並幫助發現錯誤,因為它們看起來不正確。

Go 程式設計師不會花費整天的時間來除錯不可思議的編譯錯誤。他們也不會將浪費時間在複雜的構建指令碼或在生產中部署程式碼。最重要的是,他們不用花費時間來試圖瞭解他們的同事所寫的內容。

當他們說語言必須擴充套件時,Go 團隊會談論生產力。

2. 識別符號

我們要討論的第一個主題是識別符號。 識別符號是一個用來表示名稱的花哨單詞; 變數的名稱,函式的名稱,方法的名稱,型別的名稱,包的名稱等。

Poor naming is symptomatic of poor design. (命名不佳是設計不佳的症狀。)
— Dave Cheney

鑑於 Go 語言的語法有限,我們為程式選擇的名稱對我們程式的可讀性產生了非常大的影響。 可讀性是良好程式碼的定義質量,因此選擇好名稱對於 Go 程式碼的可讀性至關重要。

2.1. 選擇識別符號是為了清晰,而不是簡潔

Obvious code is important. What you can do in one line you should do in three.
(清晰的程式碼很重要。在一行可以做的你應當分三行做。(if/else 嗎?))
— Ukiah Smith

Go 語言不是為了單行而優化的語言。 Go 語言不是為了最少行程式而優化的語言。我們沒有優化原始碼的大小,也沒有優化輸入所需的時間。

Good naming is like a good joke. If you have to explain it, it’s not funny.
(好的命名就像一個好笑話。如果你必須解釋它,那就不好笑了。)
— Dave Cheney

清晰的關鍵是在 Go 語言程式中我們選擇的標識名稱。讓我們談一談所謂好的名字:

  • 好的名字很簡潔。 好的名字不一定是最短的名字,但好的名字不會浪費在無關的東西上。好名字具有高的信噪比。

  • 好的名字是描述性的。 好的名字會描述變數或常量的應用,而不是它們的內容。好的名字應該描述函式的結果或方法的行為,而不是它們的操作。好的名字應該描述包的目的而非它的內容。描述東西越準確的名字就越好。

  • 好的名字應該是可預測的。 你能夠從名字中推斷出使用方式。~ 這是選擇描述性名稱的功能,但它也遵循傳統。~ 這是 Go 程式設計師在談到習慣用語時所談論的內容。

讓我們深入討論以下這些屬性。

2.2. 識別符號長度

有時候人們批評 Go 語言推薦短變數名的風格。正如 Rob Pike 所說,“Go 程式設計師想要正確的長度的識別符號”。 [1]

Andrew Gerrand 建議通過對某些事物使用更長的標識,向讀者表明它們具有更高的重要性。

The greater the distance between a name’s declaration and its uses, the longer the name should be. (名字的宣告與其使用之間的距離越大,名字應該越長。)
— Andrew Gerrand [2]

由此我們可以得出一些指導方針:

  • 短變數名稱在宣告和上次使用之間的距離很短時效果很好。

  • 長變數名稱需要證明自己的合理性; 名稱越長,需要提供的價值越高。冗長的名稱與頁面上的重量相比,訊號量較小。

  • 請勿在變數名稱中包含型別名稱。

  • 常量應該描述它們持有的值,而不是該如何使用。

  • 對於迴圈和分支使用單字母變數,引數和返回值使用單個字,函式和包級別宣告使用多個單詞

  • 方法、介面和包使用單個詞。

  • 請記住,包的名稱是呼叫者用來引用名稱的一部分,因此要好好利用這一點。

我們來舉個栗子:

  1. type Personstruct{

  2. Namestring

  3. Ageint

  4. }

  5. // AverageAge returns the average age of people.

  6. func AverageAge(people []Person)int{

  7. if len(people)==0{

  8. return0

  9. }

  10. var count, sum int

  11. for _, p := range people {

  12.        sum += p.Age

  13.        count +=1

  14. }

  15. return sum / count

  16. }

在此示例中,變數 p的在第 10行被宣告並且也只在接下來的一行中被引用。 p在執行函式期間存在時間很短。如果要了解 p的作用只需閱讀兩行程式碼。

相比之下, people在函式第 7行引數中被宣告。 sum和 count也是如此,他們用了更長的名字。讀者必須檢視更多的行數來定位它們,因此他們名字更為獨特。

我可以選擇 s替代 sum以及 c(或可能是 n)替代 count,但是這樣做會將程式中的所有變數份量降低到同樣的級別。我可以選擇 p來代替 people,但是用什麼來呼叫 for... range迭代變數。如果用 person的話看起來很奇怪,因為迴圈迭代變數的生命時間很短,其名字的長度超出了它的值。

貼士: 
與使用段落分解文件的方式一樣用空行來分解函式。 在 AverageAge中,按順序共有三個操作。 第一個是前提條件,檢查 people是否為空,第二個是 sum和 count的累積,最後是平均值的計算。

2.2.1. 上下文是關鍵

重要的是要意識到關於命名的大多數建議都是需要考慮上下文的。 我想說這是一個原則,而不是一個規則。

兩個識別符號 i和 index之間有什麼區別。 我們不能斷定一個就比另一個好,例如

  1. for index :=0; index < len(s); index++{

  2. //

  3. }

從根本上說,上面的程式碼更具有可讀性

  1. for i :=0; i < len(s); i++{

  2. //

  3. }

我認為它不是,因為就此事而論, i和 index的範圍很大可能上僅限於 for 迴圈的主體,後者的額外冗長性 (指 index) 幾乎沒有增加對於程式的理解。

但是,哪些功能更具可讀性?

  1. func (s *SNMP)Fetch(oid []int, index int)(int, error)

  1. func (s *SNMP)Fetch(o []int, i int)(int, error)

在此示例中, oid是 SNMP物件 ID的縮寫,因此將其縮短為 o意味著程式設計師必須要將文件中常用符號轉換為程式碼中較短的符號。 類似地將 index替換成 i, 模糊了 i所代表的含義,因為在 SNMP訊息中,每個 OID的子值稱為索引。

貼士: 在同一宣告中長和短形式的引數不能混搭。

2.3. 不要用變數型別命名你的變數

你不應該用變數的型別來命名你的變數, 就像您不會將寵物命名為 “狗” 和“貓”。 出於同樣的原因,您也不應在變數名字中包含型別的名字。

變數的名稱應描述其內容,而不是內容的型別。 例如:

  1. var usersMap map[string]*User

這個宣告有什麼好處? 我們可以看到它是一個 map,它與 *User型別有關。 但是 usersMap是一個 map,而 Go 語言是一種靜態型別的語言,如果沒有定義變數, 不會讓我們意外地使用到它,因此 Map字尾是多餘的。

接下來, 如果我們像這樣來宣告其他變數:

  1. var(

  2.    companiesMap map[string]*Company

  3.    productsMap map[string]*Products

  4. )

usersMap, companiesMap和 productsMap 三個 map型別變數,所有對映字串都是不同的型別。 我們知道它們是 map,我們也知道我們不能使用其中一個來代替另一個 - 如果我們在需要 map[string]*User的地方嘗試使用 companiesMap, 編譯器將丟擲錯誤異常。 在這種情況下,很明顯變數中 Map字尾並沒有提高程式碼的清晰度,它只是增加了要輸入的額外樣板程式碼。

我的建議是避免使用任何類似變數型別的字尾。

貼士: 
如果 users的描述性都不夠用,那麼 usersMap也不會。

此建議也適用於函式引數。 例如:

  1. type Configstruct{

  2. //

  3. }

  4. func WriteConfig(w io.Writer, config *Config)

命名 *Config引數 config是多餘的。 我們知道它是 *Config型別,就是這樣。

在這種情況下,如果變數的生命週期足夠短,請考慮使用 conf或 c

如果有更多的 *Config,那麼將它們稱為 original和