1. 程式人生 > >Golang 編碼技巧分享(上)

Golang 編碼技巧分享(上)

0. 引子

閱讀了Dave Cheney 關於go編碼的部落格:Practical Go: Real world advice for writing maintainable Go programs

實際應用下來,對我這個go入門者,提升效果顯著。

我對作者的文章進行整理翻譯,提取精煉,加上自己的理解,分享出來。希望也能給大家帶來幫助。

希望大家支援原作者,原汁原味的內容可以點選 連結 閱讀。文中部分例子為個人新增,如有不足敬請包容指出^ _ ^

(PS:如涉及侵權,請與我聯絡,我會及時刪除文章,知識傳播無界,望大家支援)

1. 指導原則

個人認為,編碼的最佳實踐本質是為了提高程式碼的迭代產能,減少bug的機率。(成本、效率、穩定)

作者Dave Cheney提到,go語言的最佳實踐的指導原則,需要考慮3點

  1. 簡潔

  2. 可讀性

  3. 開發效率

1.1 簡潔

簡潔是對於人而言的,如果程式碼很複雜,甚至違法人的慣性理解,那麼修改和維護是牽一髮而動全身的。

1.2 可讀性

因為程式碼被閱讀的次數遠遠多於被修改的次數。在作者看來,程式碼被人的閱讀和修改的需求,比被機器執行的需求更強烈。go編碼最佳實踐第一步就應該確定程式碼的可讀性。

在我個人看來,類似於一致性演算法中, raft為什麼比paxos傳播和應用更廣,一個很重要的原因就是raft更加易於理解,raft作者在論文中也提到,raft設計的最重要的初衷就是,paxos太難懂了。可讀性的重要性應該排在首位的。

1.3 開發效率

良好的編碼習慣,可以提高程式碼的交流效率。使得同事們看到程式碼就知道實現了什麼,而不必去逐行閱讀,大大節約了時間,提高開發效率。

此外,對於go語言本身而言,無論在編譯速度還是debug時間花費上,go相對C++也是開發效率大大提高的。

2. 命名

命名對編寫可讀性好的go程式至關重要!

曾經聽到這樣的一個言論:對變數的命名要像給自己孩子起名一樣慎重。

其實,不光是變數命名,還包括function、method、type、package等,命名都很重要。

2.1  選擇辨識度高的名字,而不是選擇簡短的名字

就像編碼不是為了在儘量短的行數內,寫完程式。而是為了寫出可讀性高的程式。

同樣的,我們的命名標識也不是越短越好,而是容易被他人理解。

一個好名字應該具備的特點:

  1. 簡短:一個好名字應該在具備高辨識度的情況下,儘量簡短。

    1. 比如一個判斷使用者登入許可權的方法:壞名字是judgeAuth(容易歧義),judgeUserLoginAuthority(冗長)

    2. 好的例子judgeLoginAuth

  2. 描述性的:一個好的名字應該是描述變數和常量的用途,而非他們的內容;描述function的結果,或者method的行為,而不是他們的操作;描述package的目的,而非包含的內容。描述的準確性衡量了名字的好壞。

    1. 比如設計一個用來主從選舉的包。壞的package名字leader_operation,好的名字election

    2. 壞的function或者method名字ReturnElection,好的名字NewElection

    3. 壞的變數或者常量名字ElectionState,好的名字Role

  3. 可預測的:一個好的名字,僅通過名字,大家就可以推斷他們的用途。應該遵循大家的慣用理解。下面會詳細闡述。比如

    1. i,j,k常用來在迭代中描述引用計數值

    2. n通常用來表示計數累加值

    3. v通常表示一個編碼函式的值

    4. k通常用在map中的key

    5. s通常用來表示字串

2.2 命名的長度

關於名字的長度,我們有這些建議:

  1. 如果變數的宣告和它被最後一次使用的距離很短,可以使用短的變數名

  2. 如果一個變數很重要,那麼可以避免歧義,允許變數名稱長一些,消除歧義

  3. 變數的名字中請不要包含變數的型別名

  4. 常量的名字應該描述他們儲存的值,而不是如何使用該值

  5. 單個字母的名字可以用作迭代、邏輯分支判斷、引數和返回值。包和函式的名字請使用多個字母的組合。

  6. method、interface、package 請使用單個單詞

  7. pakcage名字也是呼叫方引用時需要註明的,所以請利用package的名字

舉一個作者文中的例子說明:

640?wx_fmt=png

在這個例子中,people 距離最後一次使用間隔7行,而變數p是用來迭代perple的,p距離最後一次使用間隔1行。所以p可以使用1個字母命名,而people則使用單詞來命名。

其實這裡是防止人們閱讀程式碼時,閱讀過多行數後,突然發現一個上下文不理解的詞,再去找定義,導致可讀性差。

同時,注意例子中的空行的使用。一個是函式之間的空行,另一個是函式內的空行:在函式裡幹了3件事:異常判斷;累加age;返回。在這3者之間新增空行,可以增加可讀性。

2.2.1 上下文是關鍵

以上強調的原則需要在上下文中去實際判斷才行,萬事無絕對。

640?wx_fmt=png

相比,顯然使用oid命名更具備可讀性,而使用短變數o則不容易理解。

2.3 變數的命名不要攜帶變數的型別

因為golang 是一個強型別的語言,在變數的命名中包含型別是資訊冗餘的,而且容易導致誤解錯誤。舉個作者的例子:

var usersMap map[string]*User

我們將一個從string 到 User 的map結構,命名為UsersMap,看起來合情合理,但是變數的型別中已經包含了map,沒有必要再在變數中註明了。

作者的話來講:如果Users 描述不清楚,nameUsersMap也不見得多清楚。

對於函式的名稱同樣適用,比如:

640?wx_fmt=png

config 的名稱有冗餘了,型別中已經說明它是一個*Config了,如果變數在函式中最後一次引用的距離足夠短,那麼適用簡稱c或者conf 會更簡潔。

提示:不要讓包名搶佔了好的變數名。比如context這個包,如果使用func WriteLog(context context.Context, message string),那麼編譯的時候會報錯,因為包名和變數名衝突了。所以一般使用的時候,會使用func WriteLog(ctx context.Context, message string)

2.4 使用一致的命名

儘量不要將常見的變數名,換成其他的意思,這樣會造成讀者的歧義。

而且對於程式碼中一個型別的變數,不要多次改換它的名字,儘量使用一個名字。比如對於資料庫處理的變數,不要每次出現不同的名字,比如d *sql.DB,dbase *sql.DB,DB *sql.DB,最好使用慣用的,一致的名字db *sql.DB。這樣你在其他的程式碼中,看到變數db時,也能推測到它是*sql.DB

還有一些慣用的短變數名字,這裡提一下:

  • i, j, k用作迴圈中的索引

  • n 用在計數和累加

  • v 表示值

  • k表示一個map或者slice 的key

  • s 表示字串

2.5 使用一致的宣告型別

對於一個變數的宣告有多重宣告型別:

  • var x int = 1

  • var x = 1

  • var x int;x=1

  • var x = int(1)

  • x:=1

在作者看來,這是go的設計者犯的錯誤,但是來不及改正了,新的版本要保持向前相容。有這麼多種宣告的方式,我們怎麼選擇自己的型別呢。

作者給出了這些建議:

  • 當宣告一個變數,但是不去初始化時,使用var

640?wx_fmt=png

var 往往表示這是這個型別的空值。

  • 當宣告並且初始化值的時候,使用:=

640?wx_fmt=png

對於go來說,= 右側的型別,就是=左側的型別,上面三個例子中,最後一個使用:=的例子,既能充分標識型別,又足夠簡潔。

2.6 作為團隊的一員

程式設計生涯大部分時間都是和作為團隊的一員,參與其中。作者建議大家最好保持團隊原來的編碼風格,即使那不是你偏愛的風格。要不人會導致整個工程風格不一致,這會更糟糕。

3. 註釋

註釋很重要,註釋應該做到以下3點之一:

  1. 解釋做了什麼

  2. 解釋怎麼做

  3. 解釋為什麼這麼做

舉個例子

這是適合對外方法的註釋,解釋了做了什麼,怎麼做的

640?wx_fmt=png

這是適合方法內的註釋,解釋了做了什麼

640?wx_fmt=png

解釋為什麼的註釋比較少見,但是也是必要的,比如以下:

640?wx_fmt=png

將value 設定成0的作用並不好理解,增加註釋大大增加可理解性。

3.1 變數和常量的註釋應該描述他們的內容,而不是他們的作用

在上文中提到,變數和常量的名字又應該描述他們的目的。然而他們的註釋最好描述他們的內容。

const randomNumber = 6 // determined from an unbiased die

在這個例子中,註釋描述了為什麼randomNumber 被賦值為6,註釋沒有描述在哪裡randomNumer會被使用。再看一些例子:

640?wx_fmt=png

這裡區分一下,內容表示100代表什麼,代表RFC 7231,但是100的目的是表示StatusContinue。

提示,對於沒有初始值的變數,註釋應該描述誰來初始化這些變數

// sizeCalculationDisabled indicates whether it is safe
// to calculate Types' widths and alignments. See dowidth.
var sizeCalculationDisabled bool

3.2 要對公共的名稱新增文件

因為dodoc 是你的專案package的文件,所以你應該在每個公共的名稱上添加註釋,包括變數,常量,函式,方法。

這裡給出兩個谷歌風格指南的準則:

  • 任何不是簡練清晰的公共的函式,都應該添加註釋

  • 庫中的任何函式,不管名稱多長或者多麼負責,都必須增加註釋

舉個例子:

640?wx_fmt=png

這個規則有一個例外,無需對實現介面的方法新增文件註釋,比如不要這麼做:

640?wx_fmt=png

這裡給出一個io包的完整例子:

640?wx_fmt=png

提示:在寫函式的內容前,最好先把函式的註釋寫出

3.2.1 不要在不完善的程式碼上寫註釋,而是重新它

如果遇到了不完善的程式碼,應該記錄一個issue,以便後續去修復。

傳統的方法是在程式碼上記錄一個todo,以便提醒。比如

640?wx_fmt=png

3.2.2 如果要在一段程式碼上添加註釋,要想想能否重構它

好的程式碼本身就是註釋。如果要在一段程式碼上添加註釋,要問問自己,能否優化這段程式碼,而不用添加註釋。

函式應該只做一件事,如果你發現要在這個函式的註釋裡,提到其他函式,那麼該想想拆解這個冗餘的函式。

此外,函式越精簡,越便於測試。而且函式名本身就是最好的註釋。

4. package設計

每個go 的package 實際上都是自己的小型go程式。就好比一個function或者method的實現對呼叫者無關一樣,包內的對外暴露的function,method和型別的實現,和呼叫者無關。

一個好的go長鬚應該努力降低耦合度,這樣隨著專案的演化,一個package的變化不會影響到整個程式的其他package。

接下來會討論如何設計一個package,包括名字,型別,和編寫method和funciton的一些技巧。

4.1 一個好的packag首先有一個好名字

package 的名字應該儘量簡短,最好用一個單詞表示。考慮package名字的時候,不要想著我要在package內寫哪些型別,而是想著這個package要提供哪些服務。要以package提供哪些服務命名。

4.1.1 一個好的package名字應該是唯一的

一個專案那的package名字應該都是不同的。如果你發現可能要取相同的pcakge名字,那麼可能是以下原因:

  1. package的名字太通用了

  2. 這個package提供的服務與另一個package重合了。如果是這種情況,要考慮你的package設計了

4.2 package名字避免使用base,common,util

如果package內包含了一些列不相關的function,那麼很難說明這個package提供了哪些服務。這常常會導致package名字取一些通用的名字,類似utilities

大的專案中,經常會出現像utils或者helpers這樣的package名字。它們往往在依賴的最底層,以避免迴圈匯入問題。但是這樣也導致出現一些通用的包名稱,並且體現不出包的用意。

作者的建議是將utilshelpers這樣的package名字取取消掉:分析函式被呼叫的場景,如果可能的話,將函式轉移到呼叫者的package內,即使這涉及一些程式碼的拷貝。

提示:程式碼重複,比錯誤的抽象,代價更低

提示:使用單詞的複數命名通用的包。比如strings包含了string處理的通用函式。

我們應該儘可能的減少package的數量,比如現在有三個包commonclientserver,我們可以將其組合為一個包het/http,用client.go和server.go來區分client和server,避免引入過多的冗餘包。

提示,識別符號的名字包含了包名,比如net/httpGETfunction,呼叫的使用寫作http.Get,在識別符號起名和package起名時要考慮這一點

未完待續,下週一繼續給大家分享!

活動: Gopher Meetup 巡迴第五站 - 廣州報名火熱進行中

詳情點選閱讀原文