官术网_书友最值得收藏!

2.1 Go基礎知識

Go是一個開源項目,它由Google一個名為“Go團隊”的分布式團隊維護。該項目由編程語言規范、編譯器、工具、文檔和標準庫組成。

讓我們通過一些事實和最佳實踐來了解Go的基礎知識及其在快進模式下的特征。雖然一些建議可能會讓人覺得武斷,但都是基于我自2014年以來使用Go的經驗。這些經驗充滿了小插曲、錯誤和我們吸取的慘痛教訓。筆者在這里分享它們,以為前車之鑒。

2.1.1 Go語言形態

Go項目的核心部分是同名的通用語言,主要用于系統編程。正如你在示例代碼2-1中注意到的,Go是一種命令式語言,我們可以對事情的執行方式進行(某種)控制。此外,它是靜態類型和可編譯的,這意味著編譯器可以在程序運行之前進行許多優化和檢查。僅憑這些特性就足以使Go適用于可靠和高效的程序。

代碼示例2-1:輸出“Hello World”并退出程序的例子

項目和語言都稱為“Go”,但有時你也可以將它們稱為“Golang”。

Go與Golang

根據經驗,我們可以在任何地方使用“Go”這個名稱,除非它與英文單詞go或一種名為“Go”的古老游戲沖突。“Golang”來自域選擇(https://golang.org),因為“go”對其作者不可用。因此,在網絡上搜索有關該編程語言的資源時,請使用“Golang”。

Go也有它的吉祥物,稱為“Go gopher”(https://oreil.ly/SbxVX)。我們經常在各種形式、情況和組合中看到這只可愛的地鼠,例如,會議演講、博客文章或項目徽標里。有時Go開發人員也被稱為“gophers”!

2.1.2 Go簡化代碼庫

這一切都始于2007年左右,來自谷歌的3位經驗豐富的程序員勾勒出Go語言的構想:

Rob Pike

UTF-8和Plan 9操作系統的聯合創始人。在Go之前是許多編程語言的合著者,例如用于編寫分布式系統的Limbo和用于在圖形用戶界面中編寫并發應用程序的Newsqueak。兩者都受到Hoare的通信順序過程(Communicating Sequential Processes,C S P)的啟發[4]。

Robert Griesemer

在其他工作中,Griesemer開發了Sawzall語言(https://oreil.ly/gYKMj)并與Niklaus Wirth一起獲得了博士學位。同樣,Niklaus撰寫的“A Plea for Lean Software”在1.2.3節中被引用。

Ken Thompson

首代Unix系統的原作者之一。grep命令行實用程序的唯一創建者。Ken與Rob Pike共同創建了UTF-8和Plan 9。他也寫過幾種語言,例如Bon和B語言。

他們三位旨在創建一種新的編程語言,旨在改進當時以C++、Java和Python為首的主流編程。一年后,隨著Ian Taylor和Russ Cox于2008年的加入,它變成了一個專項項目,該團隊后來被稱為Go團隊(https://oreil.ly/Nnj6N)。Go團隊于2009年宣布公開Go項目,并于2012年3月發布了1.0版本。

在Go的設計中提到的與C++相關的主要劣勢[5]有如下幾點:

? 復雜性,做同一件事的方法很多,特性太多。

? 編譯時間超長,尤其是對于較大的代碼庫。

? 大型項目中的更新和重構成本。

? 不易使用且內存模型容易出錯。

這些因素則是Go誕生的原因,它源于對現有解決方案的不滿,以及通過少做多得來實現更多目標的雄心。這些指導原則是創造一種語言,它不會因減少重復而犧牲安全性,且能接受更簡單的代碼。它不會為了更快的編譯或解釋而犧牲執行效率,同時確保構建時間足夠快。Go會盡量加快編譯速度,例如通過顯式導入(https://oreil.ly/qxuUS)。特別是在默認啟用緩存的情況下,它僅編譯更改的代碼,因此構建時間很少超過一分鐘。

你可以將Go代碼視為腳本!

雖然從技術上講Go是一種編譯語言,但你可以像運行JavaScript、Shell或Python一樣運行它。它就像調用go run <executable package> <flags>一樣簡單。由于它編譯速度超快,因此可以出色地工作。你可以將其視為一種腳本語言,且同時保持編譯的優勢。

在語法方面,Go應該是簡單、關鍵字高亮和令人熟知的。語法基于C語言,具有類型推導(自動類型檢測,如C++中的auto),并且沒有前向聲明和頭文件。概念保持正交,這使得其更容易組合和推理。元素的正交性意味著我們可以向任何類型或數據定義添加方法(添加方法與創建類型是分開的)。接口與類型同樣也是正交的。

2.1.3 Go是谷歌的開源項目

自從宣布Go以來,所有的開發都是在開源環境中完成的(https://oreil.ly/ZeKm6),并有公共郵件列表和bug跟蹤器。更改涉及公共、權威源代碼,采用BSD樣式許可證(https://oreil.ly/XBDEK),并由Go團隊審查所有貢獻。無論更改或想法是否來自谷歌,這個過程都是一樣的。項目路線圖和提案也是公開制定的。

不幸的是,雖然有很多開源項目,但有些項目的開放程度不如其他項目。谷歌仍然是唯一一家管理Go的公司,并對它擁有最終的決定性控制權。即使任何人都可以修改、使用和貢獻,由單個供應商協調的項目也存在做出自私和破壞性決定的風險,如重新許可或阻止某些功能。雖然在一些有爭議的情況下,Go團隊的決定讓社區感到驚訝[6],但總體而言,該項目的管理是相當合理的。很多變化來自谷歌之外,Go2.0草案提案過程得到了很好的尊重和社區的推動。最后,我相信Go團隊的一致決策和管理也會帶來很多好處。不同的觀點和沖突是不可避免的,只要大家有一致的基本共識,即使不完美,也比沒有決定或用多種方法做同一件事更好。

到目前為止,這個項目設置已被證明在采用和語言穩定性方面運行良好。對于軟件的效率目標而言,這樣的結合再好不過了。我們建立并投資了一家大公司,以確保每次發行新版本都不會帶來任何性能下降。一些谷歌內部軟件依賴于Go,例如谷歌云平臺(https://oreil.ly/vjyOc)。許多人依賴谷歌云平臺的可靠性。另一方面,我們有一個龐大的Go社區,可以提供反饋、發現錯誤并提供想法和優化。如果這還不夠,我們還有開源代碼,以允許我們深入研究實際的Go庫、運行環境(見2.2.3節)等,以了解特定的性能特征代碼。

2.1.4 核心設計原則

Robert Griesemer在GopherCon 2015(https://oreil.ly/s3ZZ5)中提到,當他們第一次開始構建Go時,他們知道哪些事情不能做。主要指導原則是簡單、安全和易讀。換句話說,Go遵循“少即是多”的模式。這是一個跨越許多領域的至理名言。在Go中,只有一種慣用的編碼風格[7],而一個名為gofmt的工具可以確保其中的大部分風格一致。特別是代碼格式(僅次于命名)是程序員很少解決的一個問題。我們花時間爭論它,并根據我們的特定需求和信仰對其進行調整。由于通過工具實現了單一風格,我們節省了大量時間。正如一條Go諺語所說(https://oreil.ly/ua2G8),“gofmt的風格無人喜歡,但gofmt是每個人的最愛。”Go的作者們計劃將語言設計最小化,以便基本上只用一種方法來編寫特定的結構。當你編寫程序時,這會減少很多決策。比如用一種方法處理錯誤,一種方法編寫對象,一種方法并發運行,等等。

Go可能“缺少”大量功能,但可以說它比C或C++更具表現力(https://oreil.ly/CPkvV)。這種極簡主義風格允許Go保持代碼的簡單性和可讀性,從而提高軟件的可靠性、安全性,并且提升應用程序目標落地的整體速度。

我的代碼地道嗎?

“地道”一詞在Go社區中被過度使用了。通常,它意味著“經?!笔褂玫腉o模式。由于Go的采用率已經大幅提高,人們將最初的“地道”風格做了許多創造性的改進。如今,并不能說清楚什么才是“地道”的風格。

這就像曼達洛系列中的“This is the way”。當我們說“這段代碼是地道的”時,它能讓我們更加自信。所以要謹慎使用這個詞,或者說避免使用它,除非你能詳細說明為什么你的方法更好(https://oreil.ly/dAAKz)。

有趣的是,“少即是多”這個成語可以幫助我們提高效率。正如我們在第1章中所了解到的,如果在運行環境中做少量的工作,這通常意味著程序擁有更快的執行速度、更精簡和更低復雜度的代碼。在本書中,我們將嘗試在提高代碼性能的同時保持這些方面。

2.1.5 包和模塊

Go源代碼以包或模塊的形式被安置在目錄中。在同一目錄中,包是源文件的集合(源文件帶有.go后綴)。包名在每個源文件的頂部用package的語法指定,如示例2-1所示。同一目錄中的所有文件必須具有相同的包名[8](包名可以與目錄名不同)。多個包可以是單個Go模塊的一部分。模塊是一個包含go.mod文件的目錄,該文件聲明所有依賴模塊及其構建Go應用程序所需的版本。這個文件后續會被依賴管理工具Go Modules(https://oreil.ly/z5GqG)使用。模塊中的每個源文件都可以從其所在的模塊或外部模塊導入包。一些包也可以是“可執行的”。例如,如果一個包名為main并且在某個文件中有func main(),我們就可以執行它。為了方便找到這種包,有時他們會放在cmd目錄中。請注意,你不能導入可執行包。你只能構建或運行它。

在包內,你可以決定將哪些函數、類型、接口和方法暴露給包的用戶,哪些只能在包范圍內訪問。這很重要,因為暴露出盡可能少的API以獲得更好的可讀性、可重用性和可靠性。Go對此沒有任何private或public關鍵字。相反,它采用了一種比較新穎的方法。如代碼示例2-2所示,如果函數、類型、接口、變量的名稱以大寫字母開頭,則包外的任何代碼都可以使用。如果名稱以小寫字母開頭,則是私有的。

代碼示例2-2:使用命名大小寫實現構造可訪問性控制

? 細心的讀者可能會注意到代碼中的奇怪之處,即在私有類型或接口中對外暴露了類型或方法。如果結構或接口是私有的,包外的人可以使用它們嗎?答案是肯定的,你可以在公共函數中返回私有接口或類型,但是這種用法極少,例如,func New()privateStruct { return privateStruct{}}。盡管privateStruct是私有的,但包的用戶可以訪問其所有公共字段和方法。

內部包

你可以根據需要命名和構建代碼目錄以形成包,但目錄名稱是為特殊含義保留的。如果你確保只有指定的包才能導入其他包,可以創建一個名為internal的包子目錄。internal目錄下的任何包都不能被祖先以外的任何包導入(包括internal中的其他包)。

2.1.6 Go如何管理依賴

根據筆者的經驗,導入預編譯庫(例如C++、C#或Java)并使用某些頭文件中定義的導出函數和類是很常見的。不管怎樣,導入編譯后的代碼是有一些好處:

? 它使工程師不必費力編譯特定代碼,即可查找和下載依賴項的對應版本、特殊編譯工具或額外的資源。

? 在暴露開源代碼和擔心客戶復制能提供商業價值的代碼的情況下,銷售這樣一個預構建的庫可能會更容易[9]。

原則上,這意味著它可以很好地工作。庫的開發人員維護特定的編程協議(API),并且此類庫的用戶不需要擔心其實現復雜性。

不幸的是,在現實中這很難完美地實現。落地過程中可能會失敗或效率低,接口可能會產生誤導,文檔也可能會丟失。在這種情況下,訪問源代碼是非常重要的,它使我們能夠更深入地了解實現。我們可以根據具體的源代碼來發現問題,而不是靠猜。我們甚至可以修復工具庫或分包并立即使用它還可以提取所需的部分并使用它們來構建其他工具庫。

Go通過使用名為“導入路徑”的包URI來顯式導入所需要庫的各個部分(在Go中稱為“模塊的包”)來承擔這種可能存在的瑕疵。此類導入也會受到嚴格控制,即未使用的導入或循環依賴會導致編譯錯誤。讓我們看看在代碼示例2-3中聲明的不同的導入方法。

代碼示例2-3:main.go文件中來自github.com/prometheus/prometheus模塊的部分導入語句

? 如果導入聲明中沒有帶路徑結構的域,則表示導入了“standard”[10]庫中的包。這個特殊的導入方式允許我們使用$(go env GOROOT)/src/context/目錄中的代碼,這些代碼通過context引用,例如context.Background()。

? 可以顯式導入包,而不需要任何標識符。我們不想引用這個包中的任何構造,但我們希望初始化一些全局變量。在這種情況下,pprof包將調試端點添加到全局HTTP服務路由器中。雖然這種方式可行,但在實踐中我們應該避免重復使用全局的、可修改的變量。

? 非標準包可以使用互聯網域名形式的導入路徑和特定模塊中包的可選路徑導入。例如,Go工具與https://github.com能很好地集成。因此如果你將Go代碼托管在Git存儲庫中,它會找到指定的包。在本例中,github.com/oklog/run模塊中有run包,它則是https://github.com/oklog/run Git存儲庫。

? 如果包取自當前模塊(在本例中,我們的模塊是github.com/prometheus/prometheus),包將從你的本地目錄解析。在我們的示例中,是導入<module root>/config模塊。

這種設計側重于開放和明確定義的依賴關系。它與開源分發模型配合得非常好,社區可以在公共Git存儲庫中協作開發強大的包。當然,也可以使用標準版本控制身份驗證協議來隱藏模塊或包。此外,官方工具不支持以二進制形式分發包(https://oreil.ly/EnkBT),因此強烈建議在編譯時提供依賴源。

軟件的依賴性的問題并不容易解決。Go從C++和其他語言的錯誤中吸取了教訓,并采取了謹慎的方法來避免漫長的編譯時間,以及通常被稱為“依賴地獄”的影響。

通過標準庫的設計,我們在控制依賴關系方面做出了巨大努力。為了使用一個函數,復制一點代碼比引入一個大庫更好。(系統構建中的測試會抱怨新核心依賴項的出現)依賴的清爽勝過代碼重用。這在實踐中的一個例子是(低級)網絡包有它自己的整數到十進制的例行轉換程序,以避免依賴更大和依賴性強的格式化I/O包。另一個是字符串轉換包strconv有一個“可打印”字符定義的私有實現,而不是引入大型Unicode字符類表;strconv遵循的Unicode標準已通過包的測試,從而得到了驗證。

——Rob Pike,“Go at Google:Language Design in the Service of Software Engineering”(https://oreil.ly/wqKGT

再次強調,在追求效率的大方向下,依賴性和透明度的潛在極簡主義帶來了巨大的價值。更少的未知因素意味著我們可以快速發現主要瓶頸,并首先關注最重要的價值優化。如果我們注意到依賴項中存在潛在的優化空間,我們就不需要圍繞它進行工作。相反,我們歡迎直接向上游貢獻優化改進,這對雙方都有幫助!

2.1.7 一致的工具

從一開始,Go就擁有一套強大而一致的工具,其作為其命令行界面工具的一部分,稱為go。列舉一些實用程序:

? go bug打開一個新的瀏覽器標簽,其中包含可以提交正式bug報告的正確位置(GitHub上的Go存儲庫)。

? go build -o <output path> <packages>命令,構建給定的Go包。

? go env顯示當前終端會話中設置的與Go相關的所有環境變量。

? go fmt <file,packages or directories>會將給定的代碼格式化為所需的樣式,比如清除空白、修復錯誤的縮進等。源代碼甚至不需要是有效的和可編譯的Go代碼。你還可以安裝一個擴展的官方格式化工具goimports(https://oreil.ly/6fDcy),它可以額外清理和格式化你的導入語句。

為了獲得最佳的體驗,請將你的IDE設置為在每個文件上運行goimports-w $FILE,這樣就不用再擔心手動縮進了!

? go get <package@version>允許你安裝所要求依賴項的預期版本。使用@latest后綴獲取@none的最新版本以卸載依賴項。

? go help <command/topic>打印有關命令或給定主題的文檔。例如,go help environment告訴你所有關于Go可能使用的環境變量。

? go install <package>類似于go get,如果給定的包是“可執行的”,則安裝相應的二進制文件。

? go list列出Go的包和模塊。它允許使用Go模板(稍后解釋)進行靈活的輸出格式化,例如,go list-mod=readonly-m-f'{{ if and(not.Indirect)(not.Main)}}{{.Path}}{{end}}' all列出所有直接非可執行依賴模塊。

? go mod允許管理依賴模塊。

? go test允許運行單元測試、模糊測試和基準測試。我們會在第8章詳細討論后者。

? go tool擁有十幾個更高級的CLI工具。我們將在9.2.1節中特別關注go tool pprof,以進行性能優化。

? go vet運行基本的靜態分析檢查。

在大多數情況下,Go CLI就是你進行高效Go編程時所需的全部了[11]。

2.1.8 錯誤處理

錯誤是每個運行的軟件中不可避免的一部分。尤其是在分布式系統,它們一般具有處理不同類型故障的高級研究和算法[12]。盡管錯誤提示是必要的,但大多數編程語言并不推薦或強制使用特定的故障處理方式。例如,在C++中,你會看到程序員使用多種方法從函數返回錯誤:

? 異常(Exceptions)。

? 整型返回碼(如果返回值非零,則表示錯誤)。

? 隱式狀態碼[13]。

? 其他標記值(如果返回值為空,則為錯誤)。

? 通過參數返回潛在錯誤。

? 自定義錯誤類。

? 單子(Monads)[14]。

每種選擇都有其利弊,但事實上,用如此多的方法處理錯誤可能會導致嚴重的問題。它可能會隱藏一些語句并導致返回錯誤,從而引起意外,引入復雜性,使我們的軟件不可靠。

當然,提供多選項的用意是好的。它為開發人員提供了更多選擇。也許你創建的軟件不是很關鍵,或者是第一次迭代,所以你想讓“快樂路徑”十分清楚。在這種情況下,掩蓋一些“壞路徑”似乎是一個不錯的短期想法,對吧?不幸的是,與許多捷徑一樣,它會帶來許多風險。軟件的復雜性和對功能的需求導致代碼永遠不會超出“第一次迭代”,而非關鍵代碼很快就會成為關鍵代碼的依賴項。這是導致軟件不可靠或難以調試的罪魁禍首之一。

Go采取了一種獨特的方法,即將錯誤視為一種首要的語言特性。它假設我們想要編寫可靠的軟件,使錯誤處理顯式、簡單和統一地在庫與接口之間進行。讓我們看一下代碼示例2-4中的一些例子。

代碼示例2-4:具有不同返回參數的多個函數簽名

? 這里的關鍵點是函數和方法將錯誤流定義為它們簽名的一部分。在這種情況下,noErrCanHappen函數聲明在其調用期間不會發生任何錯誤。

? 通過查看doOrErr函數簽名,我們知道可能會發生一些錯誤。我們還不知道是什么類型的錯誤,只知道它正在實現一個內置的錯誤接口。我們也知道如果錯誤為nil,則表示沒有錯誤。

? 在“快樂路徑”中計算某些結果時,可以利用Go函數能返回多個參數的能力。如果可能發生錯誤,它應該總是最后一個返回參數。從調用方來看,如果錯誤為零,我們應該只接觸結果。

值得注意的是,Go有一個名為panic的異常機制,可以使用內置函數recover()進行恢復。它雖然在某些情況下有用或者是必要的(例如初始化),但在生產代碼實踐中,你不應該使用panic機制來處理常規錯誤。它們效率較低,隱藏了錯誤,總的來說會帶來許多意外。將錯誤作為調用的一部分,使得編譯器和程序員能在正常執行路徑中為應對錯誤情況做好準備。代碼示例2-5展示了如果錯誤發生在我們的函數執行路徑中,我們可以如何處理錯誤。

代碼示例2-5:檢查和處理錯誤

? 請注意,我們沒有導入內置的errors包,而是使用了開源的直接替代品github.com/efficientgo/core/errors的core模塊。這是我推薦的替代品,可用來替換errors包和當前正流行的github.com/pkg/errors包。

? 要判斷是否發生了錯誤,我們需要檢查err變量是否為nil。如果發生錯誤,我們可以進行錯誤處理。通常,這意味著記錄它、退出程序、增加指標,甚至明確忽略它。

? 有時,將錯誤處理委托給調用者是合適的。例如,如果函數因許多錯誤而失敗,請考慮使用errors.Wrap函數對其進行包裝,以添加錯誤的短上下文。例如,使用github.com/efficientgo/core/errors,我們將擁有上下文和堆棧跟蹤信息,如果后面使用%+v,這些內容將被呈現出來。

如何包裝錯誤

請注意,我建議使用errors.Wrap(或errors.Wrapf),而不是內置的包裝錯誤的方法。Go為了允許傳遞錯誤的fmt.Errors類型函數定義了%w標識符。目前,我不建議使用%w,因為它不是類型安全的,也不像Wrap那樣明確,過去曾引起過一些不明顯的bug。

只通過這種方式定義錯誤和處理錯誤是Go最好的特性之一。有趣的是,由于過于冗長和某些復雜難懂的樣板文件,這也成為它在語言上的一個缺點。它有時可能會讓人覺得重復,但一些工具可以幫你減少樣板文件。

一些Go IDE定義了代碼模板。例如,在JetBrain的GoLand產品中,鍵入err并按Tab鍵,將生成if err!=nil語句。你還可以通過折疊或展開錯誤處理塊來提高可讀性。

另一個常見的抱怨是,編寫Go讓人感到非?!氨^”,因為它把那些可能永遠不會發生的錯誤也顯現出來了。程序員必須在每一步都決定如何處理它們,這需要耗費腦力和時間。然而,根據筆者的經驗,這樣做是值得的,并且可以使程序變得更加可預見且更容易調試。

永遠不要忽略錯誤!

由于錯誤處理的冗長,很容易發生跳過err!=nil檢查的情況。除非你知道一個函數永遠不會返回錯誤(未來的版本也一樣?。?,否則不要考慮這樣做。如果你不知道如何處理錯誤,請考慮默認將其傳遞給調用者。如果你必須忽略錯誤,就考慮明確地執行“_=”語法。此外,始終使用linters,它會警告你某些未檢查的錯誤。

錯誤處理對一般Go代碼的運行時效率是否有影響?是的!不幸的是,這種影響比開發人員通常預期的還要顯著得多。根據筆者的經驗,錯誤路徑通常比正常路徑慢一個數量級,并且執行成本更高。其中一個原因是,在監控或基準測試步驟中,我們往往不會忽略錯誤流(見3.3節)。

另一個常見的原因是,構建錯誤通常涉及大量的字符串操作,以創建人類可讀的消息。因此,它可能代價高昂,特別是對于冗長的調試標簽,這將在本書的后面部分提到。理解這些含義并確保一致性和有效的錯誤處理,對于任何軟件來說都是必不可少的,我們將在接下來的章節中詳細介紹。

2.1.9 語言生態

對于Go這樣一個“年輕”的語言來說,它的一個普遍的優點是其生態系統非常成熟。雖然本節中列出的項目對于扎實的編程語言來說不是強制性的,但它們改善了整個開發體驗。這也是Go社區如此龐大且仍在增長的原因。

首先,Go允許程序員專注于業務邏輯,而不必為YAML解碼或加密哈希算法等基本功能重新實現或導入第三方庫。Go標準庫質量高、健壯、超向后兼容且功能豐富。它們具有良好的基準測試、可靠的API和良好的文檔。因此,你可以在不導入外部包的情況下實現大部分目標。例如,運行HTTP服務非常簡單,如代碼示例2-6所示。

代碼示例2-6:服務HTTP請求的最少代碼[15]

在大多數情況下,標準庫的效率足夠好,甚至優于第三方替代方案。例如,尤其對于包的低級元素,比如HTTP客戶端和服務器代碼的net/http,或者是crypto、math和sort等元素(以及更多元素),有大量的優化來服務于大多數用例。這使得開發人員可以在不擔心排序性能等基本問題的情況下構建更復雜的代碼。然而情況并非總是如此。有些庫是為特定用途設計的,濫用它們可能會導致嚴重的資源浪費。我們將在第11章中介紹你需要注意的所有事項。

成熟生態系統的另一個亮點是,在瀏覽器中內置一個基礎的官方的Go編輯器,名為Go Playground(https://oreil.ly/9Os3y)。如果你想快速測試某些東西或分享交互式代碼示例,這是一個很棒的工具。它也很容易擴展,因此社區經常發布Go Playground的變體,以嘗試和分享以前的實驗性的語言特性,如泛型(https://oreil.ly/f0qpm)(現在是主要語言的一部分,我們會在2.2.5節中進行講解)。

最后但同樣重要的是,Go項目定義了它的模板語言,稱為Go模板(https://oreil.ly/FdEZ8)。在某種程度上,它類似于Python的Jinja2語言(https://oreil.ly/U6Em1)。雖然這聽起來像是Go的一個附帶功能,但它對任何動態文本或HTML的生成都是有利的。它也經常用于像Helm(https://helm.sh)或Hugo(https://gohugo.io)這類流行的工具中。

2.1.10 移除未使用的import和變量

如果你在Go中定義了一個變量,但從未讀取任何值或不將其傳遞給另一個函數,編譯將會失敗。同樣,如果你在import語句中添加了一個包,但在文件中沒有使用該包,它也會失敗。

筆者看到Go開發人員已經習慣并且喜歡上了這個特性,但對于新手來說,這很令人詫異。如果你想快速地使用這門語言,那么在未被使用的構造上受阻可能會帶來些許沮喪,例如,創建一些變量,卻從不以調試目的使用他們。

但是,有一些方法可以顯式處理這些情況!你可以在代碼示例2-7中看到一些處理這些用法檢查的示例。

代碼示例2-7:未使用和已使用變量的示例

? 變量a、b、d沒有被使用,所以會導致編譯錯誤。

? 變量e被使用。

? 變量f在技術上用于明確無標識符(_)。如果你明確想要告訴讀者(和編譯器)你想要忽略該值,那么這種方法很有用。

同樣,未被使用的導入將使編譯過程失敗,因此像goimports這樣的工具(在2.1.7節中提到)會自動刪除未使用的導入。對未使用的變量和導入進行編譯失敗提醒,可以有效地確保代碼保持清晰度和相關性。請注意,它只檢查內部函數變量。未使用的結構字段、方法或類型等元素不會被檢查。

2.1.11 單元測試和表測試

測試是每個應用程序強制實施的部分,無論大小。在Go中,測試是開發過程中自然而然的一部分,它易于編寫,注重簡潔性和可讀性。如果我們想要高效的代碼,則需要進行可靠的測試,以便讓我們不必在迭代程序時擔心回歸。添加一個帶有_test.go后綴的文件,從而在包中為你的代碼引入單元測試。你可以在該文件中編寫任何代碼,生產代碼無法訪問到它們。你可以添加四種類型的函數,這些函數將針對不同的測試部分進行調用。某些簽名可以區分這些類型,尤其是以前綴為Test、fuzz、Example或Benchmark命名的函數,以及特定的參數。

讓我們來看看代碼示例2-8中的單元測試類型。這是一個有趣的表測試。Example和Benchmark在2.2.1節和8.1.1節中進行了解釋。

代碼示例2-8:示例單元表測試

? 如果_test.go文件中的函數以關鍵字Test命名,并且只接受t*testing.T,則認為它是“單元測試”。你可以通過go test命令運行它們。

? 通常,我們希望使用多個測試用例(通常是邊緣情況)來測試一個特定的函數,這些測試用例定義了不同的輸入和預期輸出。在這里,我建議使用表測試。定義你的輸入和輸出,然后在易于閱讀的循環中運行相同的函數。

? 你可以選擇調用t.Run,它允許你指定子測試。在動態測試用例(如表測試)上定義這些子測試是一種很好的做法,它使你能夠快速導航到失敗的用例。

? Go testing.T類型提供了有用的方法,如Fail或Fatal,來中止單元測試,或使用Error接口來繼續運行并檢查其他潛在錯誤。在我們的例子中,筆者建議使用一個簡單的輔助工具,即來自我們的開源核心庫(https://oreil.ly/yAit9)的testutil.Equals[16]

筆者建議經常編寫測試。提前為關鍵部分編寫單元測試將幫助你更快地實現所需的功能。這也是筆者建議遵循合理的測試驅動開發形式的原因,詳見3.6節。

在討論更高級的功能之前,上述信息應該能使你形成對語言目標、優勢和功能的大致認識。

主站蜘蛛池模板: 大兴区| 湾仔区| 加查县| 通州市| 靖远县| 南涧| 宝应县| 安溪县| 蒲城县| 盐亭县| 仪征市| 株洲市| 珠海市| 文成县| 红桥区| 昌宁县| 邳州市| 儋州市| 庆元县| 安顺市| 天峻县| 杨浦区| 同德县| 利津县| 迁西县| 民勤县| 定襄县| 锡林郭勒盟| 中超| 永仁县| 元江| 静乐县| 咸宁市| 义乌市| 克拉玛依市| 柳州市| 蒙城县| 高邮市| 留坝县| 专栏| 祁阳县|