
本文翻譯自John Arundel的《Ten commandments of Go》[1],全文如下:
作為一名全職的Go語言作家[2]和老師[3],我花了很多時間和學生們一起,幫助他們寫出更清晰、更好、更有用的Go程式,我發現,我給他們的建議可以歸納總結為一套通用原則,在這里我將這些原則分享給大家,
1. 你應該是無聊的
Go社區喜歡共識(consensus),比如:Go源代碼有一個由gofmt強制執行的統一的代碼格式規范,同樣,無論你要解決什么問題,通常都有一個標準的、類似于Go行事風格的方法來解決,有時它是標準的方式,因為它是最好的方式,但通常它只是最好的方式,因為它是標準的方式,
要抵制住創意、時尚或(最糟糕的是)聰明的傭訓,這些不是Go的行事風格,Go行事風格的代碼簡單、無聊,通常相當啰嗦,而且最重要的是顯式的風格(由于這個原因,有些人把Go稱為面向顯式(obviousness-oriented)風格的編程語言),
當有疑問時,請遵循最小驚喜原則[4],爭取做到一目了然[5],要直截了當,要簡單,要顯式,要無聊,
這并不是說在軟體工程層面沒有展示令人嘆為觀止的優雅和風格的空間了;當然有,但那是在設計層面上,而不是單個代碼行,代碼并不重要,它應該以被隨時替換,重要的是程式,
2. 你應該以測驗為先
在Go中,一個常見的錯誤是先寫了一些函式(比如:GetDataFromAPI),然后在考慮如何測驗它時不知所措,函式通過網路進行了真正的API呼叫,它向終端列印東西,它寫磁盤檔案了,這是一個可怕的的不可測驗性的坑,
不要先寫那個函式,而是先寫一個測驗(比如:TestGetDataFromAPI),如何寫這樣一個測驗呢?它必須為函式的呼叫提供一個本地的TLS測驗服務器,所以你需要一種方法來注入這種依賴,它要寫資料到io.Writer,你同樣需要為此注入一個模擬外部世界的本地依賴,比如:bytes.Buffer,
現在,當你開始撰寫GetDataFromAPI函式時,一切都將變得很容易了,它的所有依賴關系都被注入,所以它的業務邏輯與它與外部世界的互動和監聽方式完全脫鉤,
HTTP handler也是如此,一個HTTP handler的唯一作業是決議請求中的資料,將其傳遞給某個業務邏輯函式來計算結果,并將結果格式化到ResponseWriter,這幾乎不需要測驗,所以你的大部分測驗將在業務邏輯函式本身,而不是handler,我們知道HTTP的作業原理,
3. 你應該測驗行為,而不是函式
如果你想知道如何在不實際呼叫API的情況下測驗這個函式,那么答案很簡單:"不要測驗這個函式",
你需要測驗的不是一些函式,而是一些行為,例如,一個可能是"給定一些用戶輸入,我可以正確地組合URL并以正確的引數呼叫API," 另一個可能是"給定API回傳的一些JSON資料,我可以正確地將其解包到某個Go結構體中,"
當你沿著這樣的思路考量問題的解決方法的時候,寫測驗就容易多了:你可以想象一些這類函式,它們每個函式都會接受一些輸入,并產生一些輸出,并且很容易給它們撰寫單元測驗,有些事情它們是不會做的,例如進行任何HTTP呼叫,
同樣,當你試圖實作"資料可以持久地存盤在資料庫中并從資料庫中檢索"這樣的行為時,你可以將其分解成更小的、更可測驗的行為,例如,"給定一個Go結構體,我可以正確地生成SQL查詢,并將其內容存盤到Postgres表中",或者 "給定一個物件,我可以正確地將結果決議到Go結構體切片中",不需要mock資料庫,不需要真正的資料庫!
4. 你不應制造文書作業
所有的程式都會在某一點上涉及到一些繁瑣的、不可避免的資料倒換重組活動;我們可以把所有這類活動歸入文書作業的范疇,對程式員來說,唯一的問題是,這些文書作業在API邊界的哪一邊?
如果是放在用戶側,那就意味著用戶必須撰寫大量的代碼來為你的庫準備文書作業,然后再撰寫大量的代碼來將結果解壓成有用的格式,
相反(將文書作業放在API實作側),寫零文書作業的庫,可以在一行中呼叫:
game.Run()
不要讓用戶呼叫一個建構式來獲取某個物件,然后再基于這個物件進行方法呼叫,那就是文書作業,只要讓一切在他們直接呼叫時發生就可以了,如果有可配置的設定,請設定合理的默認值,這樣用戶根本不用考慮,除非他們因為某些原因需要覆寫默認值,功能選項(functional option)[6]是一個很好的模式,
這是另一個先寫測驗的好理由,如果你寫的API中創造了文書作業,那么在測驗時你將不得不自己做所有的文書作業,以便使用你自己的庫,如果這被證明是笨拙、啰嗦和耗時的,可以考慮將這些文書作業移到API邊界內,
5. 你不應該殺死程式
你的庫沒有權利終止用戶的程式,不要在你的包中呼叫像os.Exit、log.Fatal、panic這樣的函式,這不是你能決定的,相反,如果你遇到了不可恢復(recover)的錯誤,將它們回傳給呼叫者,
為什么不呢?因為它迫使任何想使用你的庫的人去寫代碼,不管panic是否真的被觸發,出于同樣的原因,你永遠不應該使用會引起panic的第三方庫,因為一旦你用了,你就需要recover它們,
所以你千萬不要顯式呼叫(這些可以殺死程式的函式),但是隱式呼叫呢?你所做的任何操作,在某些情況下可能會panic(比如:索引一個空的片斷,寫入一個空map,型別斷言失敗)都應該先檢查一下是否正常,如果不正常就回傳一個錯誤,
6. 你不要泄露資源
對于一個打算永遠運行而不崩潰或出錯的程式來說,對其的要求要比對單次命令列工具要嚴格一些,例如,想想太空探測器:在關鍵時刻意外重啟制導系統,可能會讓價值數十億美元的飛行器駛向星系間的虛空,對于負責的軟體工程師來說,這很可能會導致一場沒有咖啡的面談,讓人有些不舒服,
我們不是都在為太空器寫軟體,但我們應該像太空工程師一樣思考,自然,我們的程式應該永遠不會崩潰(最壞的情況下,它們應該優雅地退化,并提出退出程序的詳實資訊),但它們也需要是可持續的,這意味著不能泄露記憶體、goroutines、檔案句柄或任何其他稀缺資源,
每當你有一些可泄漏的資源時,當你知道你已經成功獲得它的那一刻,你應該想著釋放它,無論函式如何退出或何時退出,保證將其清理掉,我們可以用Go帶給我們的禮物:defer[7],
任何時候啟動一個goroutine,你都應該知道它是如何結束的,啟動它的同一個函式應該負責停止它,使用waitgroups或者errgroups,并且總是向一個可能被取消的函式傳遞一個context.Context,
7. 你不應該限制用戶的選擇
我們如何撰寫友好、靈活、強大、易用的庫呢?一種方法是避免不必要地限制用戶對庫的操作,一個常見的Gopherism(Go主義)是 "接受介面,回傳結構",但為什么這是個好建議呢?
假設你有一個函式,接受類似于一個*os.File的引數 ,并向其寫入資料,也許被寫入的東西是一個檔案并不重要,具體來說,它只需要是一個 "你可以寫入的東西"(這個想法由標準庫介面,如io.Writer表達),有很多這樣的東西:網路連接、HTTP response writer、bytes.Buffer等等,
通過強迫用戶傳遞給你一個檔案,你限制了他們對你的庫的使用,通過接受一個介面(如 io.Writer)來代替,你將打開新的可能性,包括尚未被創造的型別,后續它們仍然可以滿足(介面) ,可以與你的代碼io.Writer一起作業,
為什么要 "回傳結構體"?好吧,假設你回傳一些介面型別,這極大地限制了用戶對該值的操作(他們能做的就是呼叫其上的方法),即使他們事實上可以用底層的具體型別做他們需要做的事情,他們也必須先用型別斷言來解包它,換句話說,這就是額外的文書作業(應該避免),
另一種避免限制用戶選擇的方法是不要使用只有當前Go版本才有的功能,相反,考慮至少支持最近兩個主要的Go版本:有些人不能立即升級,
8. 你應該設定邊界
讓每一個軟體組件在自己的內部是完整的、有能力的;不要讓它的內部關注點暴露出來,越過它的邊界滲入到其他組件中,這一點對于與其他人的代碼的邊界來說,是雙倍的,
例如,假設你的庫呼叫了某個API,這個API會有自己的模式和自己的詞匯,反映自己的關注點和自己的領域語言,
邊界是那些與你的代碼接觸的點:例如,呼叫API并決議其回應的函式,我把它稱為 "airlock "函式,因為它的作業部分是確保你的內部型別和關注點不會泄露出去,并防止外來資料泄露進來,
一旦你讓一點外來資料在你的程式內部自由運行,它很快就會到處亂跑,你的其他包都需要匯入這些外來型別,這很煩人,而且代碼將會有一股糟糕的味道,
相反,你的airlock函式應該做兩件事:它應該將外來資料轉化為你自己的內部格式,而且應該確保資料是有效的,現在,你的所有其他代碼只需要處理你的內部型別,它不需要擔心資料是否會出錯、丟失或不完整,
另一種執行良好邊界的方法是始終檢查錯誤,如果你不這樣做,無效的資料可能會泄露進來,
9. 你不應該在內部使用介面
一個介面值說:"我不知道這個東西到底是什么,但也許我知道有些事情我可以用它來做," 這在Go程式中是一種超級不方便的值,因為我們不能做任何沒有被介面指定的事情,
對于空介面(interface{})來說,這也是雙倍的,因為我們對它一無所知,因此,根據定義,如果你有一個空的介面值,你需要把它型別化為具體的東西才能使用它,
在處理任意資料(也就是在運行時型別或模式未知的資料)時,不得不使用它們是很常見的,比如無處不在的map[string]interface{}[8],但是,我們應該盡快使用airlock將這一團無知轉化為某種具體型別的有用的Go值,
特別是,不要用interface{}型別值來模擬泛型(Go有泛型[9]),不要寫一個函式,接受一些可以是七種具體型別之一的值,然后對其進行型別轉換,為該型別找到合適的操作,相反,寫七個函式,每個具體型別一個,
不要僅因為你可以在測驗中注入mock,就創建一個公共的介面,這是一個錯誤,創建一個真正的用戶在呼叫你的函式之前必須實作的介面,這違反了“無文書作業原則”,不要在一般情況下寫mock;Go不適合這種風格的測驗,(當Go中的某些東西很困難時,這通常是你做錯事的標志,)
10. 你不要盲目地遵從誡命,而要自己思考
人們說:"告訴我們什么是最佳做法",仿佛有一本小秘籍,里面有任何技識訓組織問題的正確答案,(是有的,但不要說出去,我們不希望每個人都成為顧問),
小心任何看似清楚、明確、簡單地告訴你在某種情況下該怎么做的建議,它不會適用于每一種情況,在適用的地方,它都需要告誡,需要細微的差別,需要澄清,
每個人都希望得到的是不需要真正理解就能應用的建議,但這樣的建議比它能帶來的幫助更危險:它能讓你走到橋的一半,然后你會發現橋是紙做的,而且剛開始下雨,
非常感謝比爾-肯尼迪(Bill Kennedy)[10]和伊南克-古姆斯(Inanc Gumus)[11]對這篇文章的有益評論,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275399.html
標籤:AI
上一篇:實時 OLAP, 從 0 到 1
