前言
本規(guī)范是針對(duì) Go 語言的編碼規(guī)范,目的是為了統(tǒng)一項(xiàng)目的編碼風(fēng)格,提高源程序的可讀性、可靠性和可重用性,從而提高軟件的質(zhì)量。
本規(guī)范適用于所有產(chǎn)品的軟件源程序,同時(shí)考慮到不同產(chǎn)品和項(xiàng)目的實(shí)際開發(fā)特性,本規(guī)范分成規(guī)則性和建議性兩種:對(duì)于規(guī)則性規(guī)范,要求所有軟件開發(fā)人員嚴(yán)格執(zhí)行;對(duì)于建議性規(guī)范,各項(xiàng)目編程人員可以根據(jù)實(shí)際情況選擇執(zhí)行。本規(guī)范的示例都以Go 語言描述。
本規(guī)范的內(nèi)容包括:開發(fā)環(huán)境、包設(shè)計(jì)、布局、注釋、命名、基本元素設(shè)計(jì)、函數(shù)設(shè)計(jì)、錯(cuò)誤和異常設(shè)計(jì)、整潔測試等。
對(duì)本規(guī)范中所用的術(shù)語解釋如下:
原則:編程時(shí)應(yīng)該堅(jiān)持的指導(dǎo)思想。
規(guī)則:編程時(shí)必須遵守的約定。
建議:編程時(shí)必須加以考慮的約定。
說明:對(duì)此規(guī)則或建議的必要的解釋。
正例:對(duì)此規(guī)則或建議給出的正確例子。
反例:對(duì)此規(guī)則或建議給出的反面例子。
開發(fā)環(huán)境
【規(guī)則1-1】為了防止代碼出現(xiàn)可移植性問題和兼容性問題,團(tuán)隊(duì)使用的操作系統(tǒng)、編譯器類型、版本保持一致性。
【規(guī)則1-2】團(tuán)隊(duì)統(tǒng)一使用相同的IDE,并使用統(tǒng)一的代碼模板,保持代碼風(fēng)格的一致性。
說明:系統(tǒng)中所有的代碼看起來就好像是由單獨(dú)一個(gè)值得勝任的人編寫的。
【規(guī)則1-3】團(tuán)隊(duì)統(tǒng)一配置 IDE 的 TAB 為4個(gè)空格。
包設(shè)計(jì)
【原則2-1】包設(shè)計(jì)要滿足單一職責(zé)原則。
說明: 這是SRP(Single Reponsibility Priciple) 在包(package)設(shè)計(jì)時(shí)的一個(gè)具體運(yùn)用,我們要將包設(shè)計(jì)的非常內(nèi)聚。
【原則2-2】包內(nèi)標(biāo)識(shí)符遵守最小可見性原則。
說明: 如果一個(gè)標(biāo)識(shí)符(interface名、類型名、變量名或函數(shù)名)在語義上僅在包內(nèi)可見,則它的命名不要用大寫開頭。
【規(guī)則2-1】測試文件放在實(shí)現(xiàn)文件的同級(jí)目錄下,便于Go語言工具的使用。
說明: 雖然測試文件和實(shí)現(xiàn)文件的代碼在同一個(gè)包內(nèi),但是測試用例的設(shè)計(jì)盡量站在包用戶的角度去考慮,一般只測試包外可見的函數(shù)。
【規(guī)則2-2】包間禁止共享全局變量。
說明:常量除外
【規(guī)則2-3】 不允許一個(gè)目錄下有多個(gè)包。
布局
【規(guī)則3-1】import 導(dǎo)入包時(shí)統(tǒng)一使用小括號(hào),包名要另起一行
說明:import "C" 除外
正例:
import (
"fmt"
"reflect"
)
反例:
import "fmt"
import "reflect"
【規(guī)則3-2】import 包時(shí),路徑分隔符一律使用Unix 風(fēng)格,拒絕使用Windows 風(fēng)格;即采用/ 而不是使用\ 分割路徑。
正例:
import (
"cs/domain/model/object"
)
反例:
import (
"cs\domain\model\object"
)
【規(guī)則3-3】包含空格在內(nèi),代碼的行寬不應(yīng)超過120列。
說明: 長行要在低優(yōu)先級(jí)操作符處拆分成新行,拆分出的新行要進(jìn)行適當(dāng)?shù)目s進(jìn),使排版整齊。
【規(guī)則3-4】程序?qū)嶓w之間有且僅有一行空行區(qū)分。
說明: 函數(shù)之間的空行,能夠幫組我們快速定位函數(shù)的始末的準(zhǔn)確位置;甚至在函數(shù)內(nèi)部,將邏輯相關(guān)的代碼放在一起也同樣具有意義,它能夠幫組我們更好地理解代碼塊的語義。超過一行的空行完全沒有必要,部分粗心的程序員在處理這些細(xì)節(jié)時(shí)總存在著或多或少的問題,團(tuán)隊(duì)?wèi)?yīng)該杜絕這樣的情況發(fā)生。
【規(guī)則3-5】每個(gè)文件末尾都應(yīng)該有且僅有一行空行。
【規(guī)則3-6】一元操作符如“!”、“~”、“++”、“--”、 “*”、 “&”(地址運(yùn)算符)等前后不加空格; “[]”、“.”、“->”這類操作符前后不加空格。
【規(guī)則3-7】函數(shù)名之后不要留空格
說明: 函數(shù)名后緊跟左括號(hào)‘(’,以與關(guān)鍵字區(qū)別。
正例:
func getStatus() string {
return status
}
反例:
func getStatus () string {
return status
}
【規(guī)則3-8】在進(jìn)行“==”或“!="比較時(shí),將常量或常數(shù)放在“==”或“!="號(hào)的右邊。
正例:
if type == "book" {
return true
}
反例:
if "book" == type {
return link
}
【規(guī)則3-9】 數(shù)組的初始化按照矩陣結(jié)構(gòu)分行書寫。
正例:
numbers := [4][3]int {
1, 1, 1,
2, 4, 8,
3, 9, 27,
4, 16, 64,
}
【建議3-1】 每次提交代碼前,使用gofmt工具格式化一下。
注釋
注釋有助于理解代碼,有效的注釋是指在代碼的功能、意圖層次上進(jìn)行注釋,提供有用、額外的信息,而不是代碼表面意義的簡單重復(fù)。
注釋的恰當(dāng)用法是彌補(bǔ)我們?cè)谟么a表達(dá)意圖時(shí)遭遇的失敗。每次寫注釋,你都應(yīng)該做個(gè)鬼臉,感受自己在表達(dá)能力上的失敗。
寫注釋時(shí),首先想到的應(yīng)該是通過重構(gòu)來提高表達(dá)力,不要太早放棄。
【規(guī)則4-1】注釋與所描述內(nèi)容進(jìn)行同樣的縮進(jìn)。
說明: 可使程序排版整齊,并方便注釋的閱讀與理解。
【規(guī)則4-2】避免垃圾注釋。
說明: 對(duì)于代碼本身能夠表達(dá)的意思,不必增加額外的注釋。
【規(guī)則4-3】注釋符 “//” 或 "/*” (“*/”) 與注釋內(nèi)容之間用一個(gè)空格分隔。
【建議4-1】并非所有的函數(shù)都要配有函數(shù)頭,短函數(shù)需要一個(gè)好名字而非太多描述。
【建議4-2】提倡代碼自注釋。
說明: 能用函數(shù)或變量時(shí)就不要用注釋,如果可以的話,應(yīng)該創(chuàng)建一個(gè)描述與注釋所言同一事物的函數(shù)或變量用于消除注釋。
【建議4-3】行注釋和塊注釋都可行時(shí),優(yōu)先使用行注釋。
【建議4-4】保證代碼和注釋的一致性。
說明: 修改代碼同時(shí)修改相應(yīng)的注釋,不再有用的注釋要?jiǎng)h除。
【建議4-5】注釋應(yīng)與其描述的代碼相近,對(duì)代碼的注釋應(yīng)放在其上方或右方(對(duì)單條語句的注釋)相鄰位置,不可放在下面,如放于上方則需與其上面的代碼用空行隔開,而且注釋內(nèi)容與與被注釋的代碼相同縮進(jìn)。
命名
【規(guī)則5-1】命名要名副其實(shí)——要像給自己的baby 起名字一樣謹(jǐn)慎來對(duì)待程序命名。
說明: 變量、函數(shù)的命名告訴我們,它為什么會(huì)存在,它做什么事,應(yīng)該怎么用。如果名稱需要注釋來補(bǔ)充,那就不算是名副其實(shí)。要像給自己的baby 起名字一樣謹(jǐn)慎來對(duì)待程序命名。
【規(guī)則5-2】目錄名一律使用小寫和中劃線風(fēng)格的名稱。
正例:
task-agent
反例:
taskagent
task_agent
TaskAgent
taskAgent
【規(guī)則5-3】 包名一律使用小寫風(fēng)格,通常為過濾掉中劃線的目錄名。
正例1:
目錄名:service
對(duì)應(yīng)的包名:service
正例2:
目錄名:project-obj
對(duì)應(yīng)的包名:projectobj
【規(guī)則5-4】 開發(fā)文件命名一律使用小寫和下劃線風(fēng)格的名稱。
正例:
openstack_virtual_machine.go
反例:
openstack-virtual-machine.go
OpenstackVirtualMachine.go
openstackvirtualmachine.go
【規(guī)則5-5】標(biāo)識(shí)符要采用英文單詞或其組合,便于記憶和閱讀,切忌使用漢語拼音來命名。
說明: 標(biāo)識(shí)符應(yīng)當(dāng)直觀且可以拼讀,可望文知義,避免使人產(chǎn)生誤解。程序中的英文單詞一般不要太復(fù)雜,用詞應(yīng)當(dāng)準(zhǔn)確。
【規(guī)則5-6】如果函數(shù)返回值的類型為bool,則名字前面加上is, has, may, can, should, need等詞修飾會(huì)增強(qiáng)語意。
正例:
func isDigit() bool
反例:
func digit() bool
【規(guī)則5-7】接口名、類型名、變量名和函數(shù)名統(tǒng)一使用駝峰命名法,首字母是否大寫由包外可見性決定。
說明: 應(yīng)遵循最小可見性原則
【規(guī)則5-8】避免在名稱中攜帶類型信息。
正例:
var num int
var ports []Port
反例:
var iNum int
var portSlice []Port
【規(guī)則5-9】 變量名的主體應(yīng)當(dāng)使用“名詞”或者“形容詞+名詞”。
【規(guī)則5-10】 函數(shù)名應(yīng)當(dāng)使用“動(dòng)詞”或者“動(dòng)詞+名詞”(動(dòng)賓詞組)。
【規(guī)則5-11】 系統(tǒng)中每個(gè)實(shí)體概念對(duì)應(yīng)一個(gè)詞。
說明: 給每個(gè)抽象概念選一個(gè)詞,并且在同一個(gè)系統(tǒng)中統(tǒng)一,以便符合SRP 原則。如在同一系統(tǒng)的代碼中既有controller,還有manager 和driver,會(huì)令使用者困惑,應(yīng)統(tǒng)一。
【規(guī)則5-12】 不使用雙關(guān)語命名變量。
說明: 變量命名時(shí)應(yīng)避免將同一單詞用于不同目的,同一術(shù)語用于不同概念,應(yīng)遵從“一詞一義”規(guī)則。比如add在表達(dá)計(jì)算兩個(gè)值的和的語義時(shí),就不能再表達(dá)往一個(gè)數(shù)組切片插入一個(gè)元素的語義。
【規(guī)則5-13】 常量名使用駝峰命名法,首字母是否大寫由包外可見性決定。
說明: 應(yīng)遵循最小可見性原則
【規(guī)則5-14】 團(tuán)隊(duì)使用統(tǒng)一的縮略語,并和業(yè)界常用的縮略語保持一致。
說明: 較短的單詞可通過去掉“元音”形成縮寫,較長的單詞可取單詞的頭幾個(gè)字母形成縮寫,一些單詞有大家公認(rèn)的縮寫,常用單詞的縮寫必須統(tǒng)一。協(xié)議中的單詞的縮寫與協(xié)議保持一致。對(duì)于某個(gè)系統(tǒng)使用的專用縮寫應(yīng)該在某處注釋中做統(tǒng)一說明。
正例: 如下單詞的縮寫能夠被大家認(rèn)可
temp 可縮寫為:tmp
flag 可縮寫為:flg
statistic 可縮寫為:stat
increment 可縮寫為:inc
message可縮寫為:msg
規(guī)范的常用縮寫如下:
常用詞 | 縮寫 | 常用詞 | 縮寫 |
---|---|---|---|
Argument | Arg | Buffer | Buf |
Clear | Clr | Clock | Clk |
Compare | Cmp | Configuration | Cfg |
Context | Ctx | Delay | Dly |
Device | Dev | Disable | Dis |
Display | Disp | Enable | En |
Error | Err | Function | Fnct |
Hexadecimal | Hex | High Priority Task | HPT |
I/O System | IOS | Initialize | Init |
Mailbox | Mbox | Manager | Mgr |
Maximum | Max | Message | Msg |
Minimum | Min | Multiplex | Mux |
Operating System | OS | Overflow | Ovf |
Parameter | Param | Pointer | Ptr |
Previous | Prev | Priority | Prio |
Read | Rd | Ready | Rdy |
Register | Reg | Request | Req |
Response | Rsp | Schedule | Sched |
Semaphore | Sem | Stack | Stk |
Synchronize | Sync | Timer | Tmr |
Trigger | Trig | Write | Wr |
【規(guī)則5-16】 用正確的反義詞組命名具有互斥意義的變量或相反動(dòng)作的函數(shù)等。
正例:
詞組 | 詞組 |
---|---|
add / remove | insert / delete |
create / destroy | begin / end |
first / last | lock / unlock |
increment / decrement | push / pull |
open / close | min / max |
old / new | start / stop |
next / previous | source / destination |
show / hide | send / receive |
attach / detach | left / right |
up / down | north / south |
基本元素設(shè)計(jì)
變量與常量
【規(guī)則6-1-1】 一個(gè)變量有且只有一個(gè)功能,并與其名稱相一致,不能把一個(gè)變量用作多種用途。
說明: 一個(gè)變量只用來表示一個(gè)特定功能,不能把一個(gè)變量用作多種用途,即同一變量取值不同時(shí),其代表的意義也不同。除循環(huán)變量和收集計(jì)算結(jié)果的變量,在一個(gè)函數(shù)中,一個(gè)變量被賦值不應(yīng)該超過一次。
【規(guī)則6-1-2】 代碼中不允許出現(xiàn)魔法數(shù)。
說明: 魔法數(shù),即擁有特殊意義,卻又不能明確表現(xiàn)出這種意義的數(shù)字。用const來定義常數(shù),并根據(jù)其意義為它命名,既提高了代碼的可讀性,又便于使用IDE 等工具進(jìn)行查找修改。
【規(guī)則6-1-3】 如果 struct 中的數(shù)據(jù)變量需要進(jìn)行 json 序列化,則需要以大寫字母開頭,同時(shí)需要 json 重命名。
說明: 結(jié)構(gòu)體中的變量以大寫字母開頭,可以保證 json.Marshal 的時(shí)候數(shù)據(jù)持久化正確。如果結(jié)構(gòu)體中的變量以小寫字母開頭,則使得 json.Marshal 的時(shí)候忽略該字段,使得該字段的值丟失,從而 json.Unmarshal 的時(shí)候?qū)⒃撟兞康闹抵脼槟J(rèn)值。由于結(jié)構(gòu)體中的變量以大寫字母開頭, json 串中的字段 key 的字符串形式變成了以大寫字母開始,這對(duì)于追求以 json 串全小寫為美的我們來說,需要進(jìn)行 json 重命名。
正例:
type Position struct {
X int `json:"x"`
Y int `json:"y"`
Z int `json:"z"`
}
type Student struct {
Name string `json:"name"`
Sex string `json:"sex"`
Age int `json:"age"`
Posi Position `json:"position"`
}
反例:
type Position struct {
X int
Y int
Z int
}
type Student struct {
Name string
Sex string
Age int
Posi Position
}
【建議6-1-1】 變量應(yīng)盡可能的滿足短跨度和短存活時(shí)間。
說明: 那些介于同一個(gè)變量多個(gè)引用點(diǎn)之間的代碼可稱為攻擊窗口,我們用跨度來衡量一個(gè)變量的不同引用點(diǎn)之間的靠近程度,而變量的存活時(shí)間是一個(gè)變量存在期間所跨越的語句總數(shù)。跨度越短,則表明一個(gè)變量的不同引用點(diǎn)越靠近;存活時(shí)間越短,則表明一個(gè)變量經(jīng)歷的語句數(shù)越少。
我們追求的目標(biāo)是短跨度和短存活時(shí)間,因?yàn)?br>
(1)可以提高程序的可讀性;
(2)可以減小變量的攻擊窗口;
(3)可以減少變量的初始化錯(cuò)誤;
(4)可以減少全局變量的使用;
(5)可以方便修改Bug;
(6)可以方便重構(gòu)代碼。
表達(dá)式和語句
【規(guī)則6-2-1】 對(duì)于布爾類型的變量,應(yīng)直接進(jìn)行真假判斷
正例:
/* 設(shè)flag 是布爾類型的變量 */
if flag /* 表示flag為真 */
if !flag /* 表示flag為假 */
反例:
/* 設(shè)flag 是布爾類型的變量 */
if flag == true
if flag == 1
if flag == false
if flag == 0
【規(guī)則6-2-2】 在條件判斷語句中,當(dāng)整型變量與0 比較時(shí),不可模仿布爾變量的風(fēng)格,應(yīng)當(dāng)將整型變量用“==”或“!=”直接與0比較。
正例:
/* 設(shè)value是整型的變量 */
if value == 0
if value != 0
反例:
/* 設(shè)value是整型的變量 */
if value /* 會(huì)讓人誤解 value是布爾類型的變量 */
if !value
【規(guī)則6-2-3】 邏輯表達(dá)式已經(jīng)具有 true 或 false 語義,無需畫蛇添足。
正例:
return i == 3
反例:
if i == 3 {
return true
} else {
return false
}
【建議6-2-1】 循環(huán)嵌套次數(shù)不大于3。
【建議6-2-2】 if 語句的嵌套層數(shù)不要大于3。
說明: 適當(dāng)調(diào)整和優(yōu)化判斷邏輯,能夠有效地控制if語句的嵌套層次,這對(duì)于代碼的走查、測試、變更維護(hù)都有很大的幫助。如果能減少大語句塊的嵌套深度,對(duì)于減輕代碼閱讀時(shí)的理解負(fù)擔(dān)很有好處。
條件式通常有兩種呈現(xiàn)形式:第一種形式是所有分支都屬于正常行為;第二種形式則是條件式提供的答案只有一種是正常行為,其他都是不常見的情況。
這兩類條件式有不同的用途,這一點(diǎn)應(yīng)該通過代碼表現(xiàn)出來。如果兩條分支都是正常行為,就應(yīng)該使用形如if-else的條件式;如果某個(gè)條件極其罕見,就應(yīng)該單獨(dú)檢查該條件,并在該條件為真時(shí)立刻從函數(shù)中返回,這樣的單獨(dú)檢查常常被稱為衛(wèi)語句。
使用衛(wèi)語句,能夠有效的減少if語句嵌套層數(shù)。
【建議6-2-3】 使用for循環(huán)時(shí),優(yōu)先使用range 關(guān)鍵字而不是顯式下標(biāo)遞增控制。
正例:
for i, v := range array {
fmt.Printf("element %v of array is %v\n", i, v)
}
反例:
for i := 0; i < len(array); i++ {
fmt.Printf("element %v of array is %v\n", i, array[i])
}
【建議6-2-4】 對(duì)于 range 的返回值,如果只需要第二項(xiàng),則把第一項(xiàng)置為下劃線。
正例:
sum := 0
for _, value := range array {
sum += value
}
函數(shù)設(shè)計(jì)
函數(shù)實(shí)現(xiàn)
【規(guī)則7-1-1】 函數(shù)命名要短小精悍和名副其實(shí),避免誤導(dǎo)。一般以它" 做什么" 來命名,而不是以它" 怎么做" 來命名。
說明: 函數(shù)命名名副其實(shí)就是指通過只讀函數(shù)的名稱就可以知道函數(shù)的功能,而不需要注釋來補(bǔ)充。
給函數(shù)命名的方法:通過對(duì)要完成的功能進(jìn)行分解和抽象,將功能分解成一個(gè)個(gè)單一的短小的功能實(shí)現(xiàn)體,對(duì)實(shí)現(xiàn)體的功能采用一個(gè)恰當(dāng)?shù)拿枋鲂悦Q命名,形成函數(shù)名稱。
【規(guī)則7-1-2】 函數(shù)要短小,還要更短小。盡量控制在20行代碼之內(nèi),包括空行和{}。
說明: 有幾個(gè)原因造成我喜歡短而命名良好的函數(shù)。首先,如果每個(gè)函數(shù)的粒度都很小,那么函數(shù)被復(fù)用的機(jī)會(huì)就更大;其次,如果函數(shù)都是細(xì)粒度,那么函數(shù)在修改時(shí)也會(huì)更容易些;再次,高層函數(shù)調(diào)用命名良好的短小函數(shù),使高層函數(shù)讀起來就像一系列解釋。
一個(gè)函數(shù)多長才算合適?長度不是問題,關(guān)鍵在于函數(shù)名稱和函數(shù)本體之間的語義距離。建議函數(shù)體的規(guī)模不能太大,20 行封頂最佳。
【規(guī)則7-1-3】 函數(shù)應(yīng)該做一件事,做好這件事,只做這一件事。
說明: 判斷一個(gè)函數(shù)是否只做了一件事,可以通過兩種方法:
(1)函數(shù)只是做了該函數(shù)名下同一抽象層上的步驟,則函數(shù)只做了一件事;
(2)如果一個(gè)函數(shù)內(nèi)部的實(shí)現(xiàn)還可以拆分出一個(gè)函數(shù),則該函數(shù)違反只做一件事原則。
【規(guī)則7-1-4】 函數(shù)的縮進(jìn)層次不應(yīng)該超過3層。
【規(guī)則7-1-5】 分隔指令與詢問,不要設(shè)置多功能函數(shù)。
說明: 函數(shù)要么做什么事,要么回答什么事,兩者不可兼得。如某個(gè)函數(shù)既返回對(duì)象狀態(tài)值,又修改對(duì)象狀態(tài)值,則需要建立兩個(gè)不同的函數(shù),其中一個(gè)負(fù)責(zé)查詢對(duì)象狀態(tài),另一個(gè)負(fù)責(zé)修改對(duì)象狀態(tài)。
【建議7-1-1】 為簡單功能編寫函數(shù)。
說明: 雖然為僅用一兩行就可完成的功能去編函數(shù)好象沒有必要,但使用函數(shù)可使功能明確化,增加程序可讀性,亦可方便維護(hù)、測試。
參數(shù)
【規(guī)則7-2-1】 禁止定義多于3個(gè)參數(shù)的函數(shù)。
說明: 函數(shù)參數(shù)設(shè)置最理想的參數(shù)個(gè)數(shù)是零,其次是一,再次是二,最后是三。參數(shù)不易對(duì)付,它們有太多的概念性。另外從測試的角度看,參數(shù)更叫人為難。
【規(guī)則7-2-2】 函數(shù)參數(shù)不能含有標(biāo)識(shí)參數(shù)。
說明: 標(biāo)識(shí)參數(shù)丑陋不堪,函數(shù)往往根據(jù)它的多個(gè)取值而做多件事情,這與函數(shù)只做一件事原則違背。如果參數(shù)只是用于賦值,那么就不是標(biāo)識(shí)參數(shù),所以是否標(biāo)識(shí)參數(shù)不是今通過形參來界定,而是看函數(shù)的實(shí)現(xiàn)是否因?yàn)楹瘮?shù)的入?yún)⒍隽硕嗉虑椤?/p>
【規(guī)則7-2-3】當(dāng)struct變量作為參數(shù)時(shí),應(yīng)傳送struct的指針而不傳送struct,并且不得修改struct中的元素,用作輸出時(shí)除外。
說明: 一個(gè)函數(shù)被調(diào)用的時(shí)候,形參會(huì)被一個(gè)個(gè)壓入被調(diào)函數(shù)的堆棧中,在函數(shù)調(diào)用結(jié)束以后再彈出。一個(gè)結(jié)構(gòu)所包含的變量往往比較多,直接以一個(gè)結(jié)構(gòu)為參數(shù),壓棧出棧的內(nèi)容就會(huì)太多,不但占用堆棧空間,而且影響代碼執(zhí)行效率。
如果使用結(jié)構(gòu)的指針作為參數(shù),因?yàn)橹羔樀拈L度是固定不變的,結(jié)構(gòu)的大小就不會(huì)影響代碼執(zhí)行的效率,也不會(huì)過多地占用堆棧空間。
如果傳遞的參數(shù)類型是 map、slice 和 channel 等引用類型,則不用傳遞指針,修改引用類型變量的初始地址除外(比如 json.Unmarshal)。
【規(guī)則7-2-4】在API函數(shù)中對(duì)輸入?yún)?shù)的正確性和有效性進(jìn)行檢查,在內(nèi)部能保證的條件下其他函數(shù)不用再進(jìn)行重復(fù)檢查。
說明: 很多程序錯(cuò)誤是由非法參數(shù)引起的,我們應(yīng)該充分理解并正確處理來防止此類錯(cuò)誤,特別是指針參數(shù)地址非法判斷和數(shù)組下標(biāo)參數(shù)的邊界判斷,但是我們沒有必要在多個(gè)函數(shù)中重復(fù)檢查。
【規(guī)則7-2-5】防止將函數(shù)的參數(shù)作為工作變量。
說明: 將函數(shù)的參數(shù)作為工作變量,有可能錯(cuò)誤地改變?nèi)雲(yún)⒌膬?nèi)容,所以很危險(xiǎn)。對(duì)于必須要改變的出參,最好也先使用局部變量,最后再將該局部變量賦值給該出參。
【規(guī)則7-2-6】如果參數(shù)列表中若干個(gè)相鄰的參數(shù)類型相同,則可以在參數(shù)列表中省略前面變量的類型聲明。
正例:
func Add(a, b int)(int, error) {
// ...
}
【規(guī)則7-2-7】當(dāng) channel 作為函數(shù)參數(shù)時(shí),根據(jù)最小權(quán)限原則,使用單向 channel。
說明: 從設(shè)計(jì)的角度考慮,所有的代碼應(yīng)該都遵循“最小權(quán)限原則”。
正例:在函數(shù)Parse中ch不會(huì)被改寫
func Parse(ch <-chan int) {
for value := range ch {
fmt.Println("Parsing value", value)
}
}
返回值
【規(guī)則7-3-1】 返回值的個(gè)數(shù)不要大于3。
錯(cuò)誤和異常設(shè)計(jì)
錯(cuò)誤設(shè)計(jì)
【規(guī)則8-1-1】 錯(cuò)誤值統(tǒng)一分組定義,而不是跟著感覺走。
說明: 很多人寫代碼時(shí),到處return errors.New(value),而錯(cuò)誤value在表達(dá)同一個(gè)含義時(shí)也可能形式不同,比如“記錄不存在”的錯(cuò)誤value可能為:
- "record is not existed."
- "record is not exist!"
- "###record is not existed!!!"
這使得相同的錯(cuò)誤value撒在一大片代碼里,當(dāng)上層函數(shù)要對(duì)特定錯(cuò)誤value進(jìn)行統(tǒng)一處理時(shí),需要漫游所有下層代碼,以保證錯(cuò)誤value統(tǒng)一,不幸的是有時(shí)會(huì)有漏網(wǎng)之魚,而且這種方式嚴(yán)重阻礙了錯(cuò)誤value的重構(gòu)。
于是,我們可以參考C/C++的錯(cuò)誤碼定義文件,在Go語言的每個(gè)包中增加一個(gè)錯(cuò)誤對(duì)象定義文件,對(duì)于共性的錯(cuò)誤對(duì)象定義,則放在公共的目錄中。
正例:
// file error object
var (
ErrEof = errors.New("EOF")
ErrClosedPipe = errors.New("io: read/write on closed pipe")
ErrShortBuffer = errors.New("short buffer")
ErrShortWrite = errors.New("short write")
)
【規(guī)則8-1-2】 失敗的原因只有一個(gè)時(shí),不使用error。
正例:
func (self *AgentContext) IsValidHostType(hostType string) bool {
return hostType == "virtual_machine" || hostType == "bare_metal"
}
反例:
func (self *AgentContext) CheckHostType(hostType string) error {
switch hostType {
case "virtual_machine":
return nil
case "bare_metal":
return nil
}
return ErrInvalidHostType
}
【規(guī)則8-1-3】 沒有失敗原因時(shí),不使用error。
說明: error在Go語言中是如此的流行,以至于很多人設(shè)計(jì)函數(shù)時(shí)不管三七二十一都使用error,即使沒有一個(gè)失敗原因,而該函數(shù)的調(diào)用者無疑是無奈的。
正例:
函數(shù)設(shè)計(jì):
func SetProjectId(projectId string)
反例:
函數(shù)設(shè)計(jì):
func SetProjectId(projectId string) error
【規(guī)則8-1-4】 error/bool應(yīng)放在返回值類型列表的最后。
【規(guī)則8-1-5】 錯(cuò)誤處理巧用defer。
正例:
func deferDemo() error {
err := createResource1()
if err != nil {
return ErrCreateResource1Failed
}
defer func() {
if err != nil {
destroyResource1()
}
}()
err = createResource2()
if err != nil {
return ErrCreateResource2Failed
}
defer func() {
if err != nil {
destroyResource2()
}
}()
err = createResource3()
if err != nil {
return ErrCreateResource3Failed
}
defer func() {
if err != nil {
destroyResource3()
}
}()
err = createResource4()
if err != nil {
return ErrCreateResourc4Failed
}
return nil
}
反例:
func deferDemo() error {
err := createResource1()
if err != nil {
return ErrCreateResource1Failed
}
err = createResource2()
if err != nil {
destroyResource1()
return ErrCreateResource2Failed
}
err = createResource3()
if err != nil {
destroyResource1()
destroyResource2()
return ErrCreateResource3Failed
}
err = createResource4()
if err != nil {
destroyResource1()
destroyResource2()
destroyResource3()
return ErrCreateResource4Failed
}
return nil
}
【規(guī)則8-1-6】 當(dāng)嘗試幾次可以避免失敗時(shí),不要立即返回錯(cuò)誤。
說明: 如果錯(cuò)誤的發(fā)生是偶然性的,或由不可預(yù)知的問題導(dǎo)致。一個(gè)明智的選擇是重新嘗試失敗的操作,有時(shí)第二次或第三次嘗試時(shí)會(huì)成功。在重試時(shí),我們需要限制重試的時(shí)間間隔或重試的次數(shù),防止無限制的重試。比如我們平時(shí)上網(wǎng)時(shí),嘗試請(qǐng)求某個(gè)URL,有時(shí)第一次沒有響應(yīng),當(dāng)我們?cè)俅嗡⑿聲r(shí),就有了驚喜。
【規(guī)則8-1-7】 當(dāng)上層函數(shù)不關(guān)心錯(cuò)誤時(shí),不返回error。
說明: 對(duì)于一些資源清理相關(guān)的函數(shù)(destroy/delete/clear),如果子函數(shù)出錯(cuò),打印日志即可,而無需將錯(cuò)誤進(jìn)一步反饋到上層函數(shù),因?yàn)橐话闱闆r下,上層函數(shù)是不關(guān)心執(zhí)行結(jié)果的,或者即使關(guān)心也無能為力,于是我們建議將相關(guān)函數(shù)設(shè)計(jì)為不返回error。
異常設(shè)計(jì)
【規(guī)則8-2-1】 在程序開發(fā)階段,堅(jiān)持速錯(cuò),讓程序異常崩潰。
說明: 所謂速錯(cuò)簡單來講就是“讓它掛”,只有掛了你才會(huì)第一時(shí)間知道錯(cuò)誤。在早期開發(fā)以及任何發(fā)布階段之前,最簡單的同時(shí)也可能是最好的方法是調(diào)用panic函數(shù)來中斷程序的執(zhí)行以強(qiáng)制發(fā)生錯(cuò)誤,使得該錯(cuò)誤不會(huì)被忽略,因而能夠被盡快修復(fù)。
【規(guī)則8-2-2】 在程序部署后,應(yīng)恢復(fù)異常避免程序終止。
說明: 在Golang中,雖然有類似Erlang進(jìn)程的Goroutine,但需要強(qiáng)調(diào)的是Erlang的掛,只是Erlang進(jìn)程的異常退出,不會(huì)導(dǎo)致整個(gè)Erlang節(jié)點(diǎn)退出,所以它掛的影響層面比較低,而Goroutine如果panic了,并且沒有recover,那么整個(gè)Golang進(jìn)程(類似Erlang節(jié)點(diǎn))就會(huì)異常退出。所以,一旦Golang程序部署后,在任何情況下發(fā)生的異常都不應(yīng)該導(dǎo)致程序異常退出,我們?cè)谏蠈雍瘮?shù)FuncA中加一個(gè)延遲執(zhí)行的recover調(diào)用來達(dá)到這個(gè)目的,并且是否進(jìn)行recover需要根據(jù)環(huán)境變量或配置文件來定(便于在開發(fā)階段進(jìn)行速錯(cuò)),默認(rèn)需要recover。
正例:
func FuncA() (err error) {
defer func() {
if permittedRecover {
if p := recover(); p != nil {
fmt.Println("panic recover! p:", p)
str, ok := p.(string)
if ok {
err = errors.New(str)
} else {
err = errors.New("panic")
}
debug.PrintStack()
}
}
}()
...
}
【規(guī)則8-2-3】 對(duì)于不應(yīng)該出現(xiàn)的分支,使用異常處理。
說明: 當(dāng)某些不應(yīng)該發(fā)生的場景發(fā)生時(shí),我們就應(yīng)該調(diào)用panic函數(shù)來觸發(fā)異常。
正例:
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}
【規(guī)則8-2-4】 針對(duì)入?yún)⒉粦?yīng)該有問題的函數(shù),使用異常設(shè)計(jì)。
說明: 入?yún)⒉粦?yīng)該有問題一般指的是入?yún)橛簿幋a,而不是API的外部輸入。當(dāng)調(diào)用者明確知道輸入不會(huì)引起函數(shù)錯(cuò)誤時(shí),要求調(diào)用者檢查這個(gè)錯(cuò)誤是不必要和累贅的。我們應(yīng)該假設(shè)函數(shù)的輸入一直合法,當(dāng)調(diào)用者輸入了不應(yīng)該出現(xiàn)的輸入時(shí),就觸發(fā)panic異常。
正例: 庫函數(shù)MustCompile的實(shí)現(xiàn)
func MustCompile(str string) *Regexp {
regexp, error := Compile(str)
if error != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
}
return regexp
}
整潔測試
【規(guī)則9-1】 不要為了測試而對(duì)產(chǎn)品代碼進(jìn)行侵入性修改。
說明: 禁止僅為了測試而在產(chǎn)品代碼中增加條件分支或函數(shù)變量。
【建議9-1】 測試用例中不應(yīng)該存在復(fù)雜的循環(huán)和條件控制語句。
說明: 測試用例對(duì)可讀性的要求非常高,如果出現(xiàn)大量的循環(huán)、條件控制語句,將大大地?fù)p害了用例的可讀性。一般地,測試用例應(yīng)該是由若干條陳述句所組成,越簡單越好。
【建議9-2】 測試代碼和產(chǎn)品代碼一樣重要。
說明: 測試代碼不是二等公民,它需要被思考、被設(shè)計(jì)和被照料,它該像產(chǎn)品代碼一般保持整潔。
【建議9-3】 整潔的測試有三個(gè)要素:可讀性,可讀性和可讀性。
說明: 對(duì)于測試代碼,可讀性比產(chǎn)品代碼還重要。產(chǎn)品代碼的正確性由測試代碼來保證,而測試代碼的正確性只能由自己來保證。如果測試代碼一直保持簡單清晰,那么錯(cuò)誤便無處藏身。
【建議9-4】 測試應(yīng)該是黑盒的。
說明: 避免根據(jù)代碼編寫測試。