如何組織 Go 代碼?Go 作者的回答驚呆了
這是最常見(jiàn)的問(wèn)題之一。你可以通過(guò)互聯(lián)網(wǎng)尋找這個(gè)問(wèn)題的答案。不過(guò),我不確認(rèn)我的設(shè)計(jì)是否 100% 正確,但希望給你一些參考。
前段時(shí)間,我有幸見(jiàn)到了 Robert Griesemer[1](Go 的作者之一)。我們問(wèn)了他這個(gè)問(wèn)題:“如何組織 Go 代碼?”。他說(shuō):“我不知道。” - 這很顯然并不令人滿意,我知道。當(dāng)我們問(wèn)他如何設(shè)計(jì)他的代碼時(shí),Robert 說(shuō)他總是從扁平結(jié)構(gòu)開(kāi)始,并在必要時(shí)創(chuàng)建包。
我花了很多時(shí)間在生產(chǎn)應(yīng)用程序的兩個(gè)寵物項(xiàng)目中嘗試不同的方法。在本文中,我將向你展示所有選擇并告訴你它們的優(yōu)缺點(diǎn)。閱讀完這篇博文后,你將不會(huì)有一種“統(tǒng)治所有模式的模式”。
01 在我們開(kāi)始之前
無(wú)論你如何組織代碼,你都必須考慮閱讀它的人。最重要的是你不應(yīng)該讓你的貢獻(xiàn)者或同事思考。把所有東西都放在明顯的地方。不要重新發(fā)明輪子。你還記得 Rob Pike[2] 說(shuō)過(guò)的關(guān)于 go fmt 的話嗎?
Gofmt 的風(fēng)格沒(méi)有人喜歡,但 gofmt 是每個(gè)人的最愛(ài)。
你可能不喜歡眾所周知的模式。堅(jiān)持下去對(duì)我們和整個(gè)社區(qū)都更好。但是,如果你有充分的理由做出不同的決定,那也沒(méi)關(guān)系。如果你的包設(shè)計(jì)良好,源代碼樹(shù)會(huì)很好地反應(yīng)出來(lái)。
讓我們從文檔開(kāi)始。每個(gè)開(kāi)源 Go 項(xiàng)目在 pkg.go.dev[3] 上都有它的文檔。對(duì)于每個(gè)包,你首先看到的是包的概述。以net/http包為例,在描述每個(gè)公共函數(shù)、常量或類型之前,你需要對(duì)包提供的內(nèi)容進(jìn)行描述。你可以從中學(xué)習(xí)如何使用 API 和更深入的細(xì)節(jié)。從哪個(gè)來(lái)源生成概覽?該net/http包有一個(gè) doc.go[4] 文件,作者在其中放置了該包的一般描述。你可以將此概述放在文件夾中的任何其他文件中,但 doc.go 大家公認(rèn)的標(biāo)準(zhǔn)。
那么Readme文件中應(yīng)該有什么?首先,對(duì)這個(gè)項(xiàng)目的總體概述——它的目標(biāo)。然后,你應(yīng)該有一個(gè)快速入門(mén)部分,你可以在其中描述開(kāi)始處理項(xiàng)目時(shí)應(yīng)該做的事情,包括任何依賴項(xiàng),如 Docker 或我們正在使用的任何其他工具。你可以在此處找到基本示例或指向更詳細(xì)描述項(xiàng)目的外部網(wǎng)站的鏈接。你必須記住,此處應(yīng)保留的內(nèi)容取決于項(xiàng)目。你必須從讀者的角度思考。什么信息對(duì)他們最重要?
當(dāng)你有更多文檔要提供時(shí),將它們放入docs文件夾中。不要將它們隱藏在像/common/docs 中。這種方法有好處:很容易找到,并且在一個(gè)拉取請(qǐng)求中,你可以更改公共 API 及其文檔。你不必克隆另一個(gè)存儲(chǔ)庫(kù)并在它們之間同步更改。
我的下一個(gè)建議可能讓你吃驚。我建議使用眾所周知的工具,例如make,我知道有一些替代品,例如 sake[5], mage[6]、zim[7]或 opctl[8]。問(wèn)題是要開(kāi)始使用它們,你必須學(xué)習(xí)它們。如果任何項(xiàng)目都使用不同的自動(dòng)化工具,新維護(hù)人員將更難開(kāi)始。我的觀點(diǎn)是你應(yīng)該明智地選擇你的工具。你使用的工具越多,項(xiàng)目啟動(dòng)工作就越困難,特別有新人加入。
我一直在從事一個(gè)項(xiàng)目,我必須在本地運(yùn)行 2 個(gè)不同的依賴項(xiàng),在 CLI 中登錄到 AWS 帳戶,并連接到虛擬網(wǎng)絡(luò)才能在我的 PC 上運(yùn)行測(cè)試?;驹O(shè)置需要一兩天才能完成,我想我不必告訴你這些測(cè)試有多么不穩(wěn)定[9]。
關(guān)于 linting 建議使用 golangci-lint[10]。啟用對(duì)你的項(xiàng)目來(lái)說(shuō)似乎合理的所有 linter。通常使用默認(rèn)啟用的 linter 規(guī)則可能是一個(gè)好的開(kāi)始。
02 扁平結(jié)構(gòu)(單包)
讓我們從最推薦的方法開(kāi)始:只要你不被迫添加新包,就將整個(gè)代碼保存在根文件夾中。在項(xiàng)目開(kāi)始時(shí),這種方式真的挺好。當(dāng)我開(kāi)始使用它時(shí),我發(fā)現(xiàn)它很有幫助,并且對(duì)它最終將如何工作有一個(gè)模糊的想法。
將所有內(nèi)容放在一個(gè)地方有助于避免包之間的循環(huán)依賴。當(dāng)你將某些內(nèi)容放入單獨(dú)的包時(shí),你會(huì)發(fā)現(xiàn)需要根文件夾中的其他內(nèi)容,你將被迫為此共享依賴項(xiàng)創(chuàng)建第三個(gè)包。隨著項(xiàng)目的發(fā)展,情況變得更糟。你最終可能會(huì)擁有許多包,其中大多數(shù)包幾乎都依賴于其他包。許多函數(shù)或類型必須是公開(kāi)的。這種情況模糊了 API,使其更難閱讀、理解和使用。
使用單個(gè)包,你不必在文件夾之間跳來(lái)跳去并思考架構(gòu),因?yàn)樗械膬?nèi)容都在一個(gè)地方。這并不意味著你必須將所有內(nèi)容都保存在一個(gè)文件中,例如:
- courses/
- main.go
- server.go
- user_profile.go
- lesson.go
- course.go
在上面的例子中,每個(gè)邏輯部分都被組織成單獨(dú)的文件。當(dāng)你犯了錯(cuò)誤并將結(jié)構(gòu)放入錯(cuò)誤的文件時(shí),你所要做的就是將其剪切并粘貼到新位置。你可以這樣考慮:?jiǎn)蝹€(gè)文件代表應(yīng)用程序的一個(gè)實(shí)體部分。你可以按代碼(HTTP 處理程序、數(shù)據(jù)庫(kù)存儲(chǔ)庫(kù))或其提供的內(nèi)容(管理用戶的配置文件)對(duì)代碼進(jìn)行分組。當(dāng)你需要某樣?xùn)|西時(shí),你就會(huì)知道在哪里可以找到它。
什么時(shí)候創(chuàng)建一個(gè)新包?如果你有充分的理由這樣做,比如:
1)當(dāng)你有不止一種啟動(dòng)應(yīng)用程序的方式時(shí)
假設(shè)你有一個(gè)項(xiàng)目,并且希望以兩種模式運(yùn)行它:CLI 命令和 Web API。在這種情況下,創(chuàng)建一個(gè)/cmd包并包含 cli和web子包是很常見(jiàn)的。
- courses/
- cmd/
- cli/
- main.go
- config.go
- web/
- main.go
- server.go
- config.go
- user_profile.go
- lesson.go
- course.go
你可以將多個(gè)main()函數(shù)放入單個(gè)文件夾中的單獨(dú)文件中。要運(yùn)行它們,你必須提供一個(gè)明確的文件列表來(lái)編譯,而其中只有一個(gè)要 main()。這使應(yīng)用程序的運(yùn)行變得非常復(fù)雜。更簡(jiǎn)單的方式是直接輸入 go run ./cmd/cli。
當(dāng)你有一個(gè)子包時(shí),./cmd/ 文件夾的使用可能聽(tīng)起來(lái)過(guò)于復(fù)雜。我發(fā)現(xiàn)它在需要添加時(shí)很有用,例如,使用來(lái)自消息代理的消息。此主題將在拆分依賴項(xiàng)的部分中更詳細(xì)地介紹。
2)當(dāng)你想提取更詳細(xì)的實(shí)現(xiàn)時(shí)
標(biāo)準(zhǔn)庫(kù)就是一個(gè)很好的例子。讓我們看看 net/http/pprof[11] 包。該net包為網(wǎng)絡(luò) I/O 提供了一個(gè)可移植的接口,包括 TCP/IP、UDP、域名解析和 Unix 域套接字。你可以根據(jù)此包提供的內(nèi)容構(gòu)建你想要的任何協(xié)議。net/http 包使我們能夠發(fā)送或接收 HTTP 請(qǐng)求。HTTP 協(xié)議使用 TCP/UDP,因此http包是 net 的子包是很自然的。net/http/pprof包中的所有類型和方法都可以返回 HTTP 協(xié)議,因此自然是一個(gè) http 子包。
database/sql包也是如此。如果你有更多非關(guān)系數(shù)據(jù)庫(kù)的實(shí)現(xiàn),它們將放在database包下,和 sql包同級(jí)。
你看出來(lái)模式了嗎?數(shù)據(jù)包(packet )在樹(shù)中(tree)越深,傳遞的細(xì)節(jié)就越多。換句話說(shuō),每個(gè)子包都在父包上提供了更具體的實(shí)現(xiàn)。
3)當(dāng)你開(kāi)始為密切相關(guān)的事物添加公共前綴時(shí)
一段時(shí)間后,你可能會(huì)注意到,為了避免誤解或命名沖突,你開(kāi)始為函數(shù)或類型添加前綴或后綴。通過(guò)這樣做,我們?cè)噲D模擬項(xiàng)目中缺少包的情況可能是一個(gè)好兆頭。很難說(shuō)什么時(shí)候提取新的子包。每次當(dāng)你看到它提高了 API 的可讀性并使代碼更清晰時(shí)請(qǐng)?zhí)崛⌒碌淖影?/p>
- r := networkReader{}
- //
- r := network.Reader{}
如你所見(jiàn),扁平結(jié)構(gòu)既簡(jiǎn)單又強(qiáng)大。在某些用例中,你可能會(huì)發(fā)現(xiàn)它很有用且很有幫助。這種組織代碼的方式不僅僅適用于小型或新建項(xiàng)目。以下是遵循單包模式的庫(kù)示例:
- https://github.com/go-yaml/yaml
- https://github.com/tidwall/gjson
值得記住的是,你不需要不惜一切代價(jià)堅(jiān)持這種組織代碼的方式。保持簡(jiǎn)單是有原因的,但添加更多包可能會(huì)使你的代碼更好。不幸的是,沒(méi)有銀彈。你需要做的是嘗試并詢問(wèn)你的同事或維護(hù)人員哪個(gè)選擇對(duì)他們來(lái)說(shuō)更具可讀性。
03 模塊化
之前描述的組織代碼的方式在某些場(chǎng)景可能效率不高。我花了很多時(shí)間試圖獲得“正確的”項(xiàng)目結(jié)構(gòu)。一段時(shí)間后,我注意到對(duì)于業(yè)務(wù)應(yīng)用程序,我開(kāi)始嘗試另一種類似的方式組織代碼。
當(dāng)我們開(kāi)發(fā)直接為客戶提供客戶端的應(yīng)用程序時(shí),扁平結(jié)構(gòu)可能效率不高。你希望創(chuàng)建提供一組與控制器、基礎(chǔ)設(shè)施或業(yè)務(wù)領(lǐng)域的一部分相關(guān)的功能的模塊。讓我們仔細(xì)看看兩種最流行的方法,并談?wù)勊鼈兊膬?yōu)缺點(diǎn)。
按種類(kind)組織
這個(gè)模型很受歡迎。我認(rèn)識(shí)的人沒(méi)有提倡使用這種策略來(lái)組織代碼的,但我在新舊項(xiàng)目中都發(fā)現(xiàn)了它。按種類組織是一種策略,它試圖通過(guò)將部分放入基于其結(jié)構(gòu)的桶中,從而為過(guò)于復(fù)雜的代碼單元帶來(lái)秩序。將包稱為repositories 或 model 是很常見(jiàn)的。這樣做的結(jié)果是你會(huì)創(chuàng)建類似 utils 或者 helpers 的包,因?yàn)槟阌X(jué)得應(yīng)該把一個(gè)函數(shù)或一個(gè)結(jié)構(gòu)放在一個(gè)單獨(dú)的地方,但沒(méi)有找到任何合適的地方。
- .
- ├── handlers
- │ ├── course.go
- │ ├── lecture.go
- │ ├── profile.go
- │ └── user.go
- ├── main.go
- ├── models
- │ ├── course.go
- │ ├── lecture.go
- │ └── user.go
- ├── repositories
- │ ├── course.go
- │ ├── lecture.go
- │ └── user.go
- ├── services
- │ ├── course.go
- │ └── user.go
- └── utils
- └── stings.go
在上面的示例中,你可以看到項(xiàng)目是按類型組織的。你什么時(shí)候想添加新功能或修復(fù)與課程相關(guān)的錯(cuò)誤,你會(huì)從哪里開(kāi)始尋找?在一天結(jié)束時(shí),你將開(kāi)始從一個(gè)包跳到另一個(gè)包,希望能在那里找到有用的東西。
Graph that shows dependencies between packages
這種方法有其后果。每個(gè)類型、常量或函數(shù)都必須是公共的,才能在項(xiàng)目的另一部分中訪問(wèn)。你最終將大多數(shù)類型標(biāo)記為公有。即使對(duì)于那些不應(yīng)該公開(kāi)的人。它混淆了應(yīng)用程序的這一部分中的重要內(nèi)容。其中許多是可能隨時(shí)更改的細(xì)節(jié)。
另一方面,按種類組織對(duì)我們來(lái)說(shuō)是很自然的。我們是在處理程序或數(shù)據(jù)庫(kù)表的類別中思考的技術(shù)人員。這就是我們的成長(zhǎng)方式,也是我們被教導(dǎo)的方式。如果你沒(méi)有經(jīng)驗(yàn),這種方法可能更有益,因?yàn)樗梢詭椭愀斓亻_(kāi)始。從長(zhǎng)遠(yuǎn)來(lái)看,你可能會(huì)遇到不便,但這并不意味著你的項(xiàng)目會(huì)失敗 — 恰恰相反,有很多成功的應(yīng)用程序都是以這種方式設(shè)計(jì)的。
按組件組織
組件是應(yīng)用程序的一部分,它提供獨(dú)立的特性,很少或沒(méi)有外部依賴。你可以將其視為插件,當(dāng)你將其中之一移除時(shí),整個(gè)應(yīng)用程序仍然可以運(yùn)行,但功能有限。它可能發(fā)生在運(yùn)行數(shù)月或數(shù)年的生產(chǎn)應(yīng)用程序中。
應(yīng)用程序可能具有一個(gè)或多個(gè)提供業(yè)務(wù)價(jià)值的核心組件。在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)術(shù)語(yǔ)中,組件是有界上下文。我們將在以后的文章中用 Go 的上下文來(lái)描述 DDD。
包的 API 應(yīng)該描述包提供的內(nèi)容而不是更多。它不應(yīng)該暴露任何從消費(fèi)者的角度來(lái)看不重要的低級(jí)細(xì)節(jié)。它應(yīng)該盡可能簡(jiǎn)約。消費(fèi)者可能是另一個(gè)包或另一個(gè)導(dǎo)入我們代碼的開(kāi)發(fā)人員。
該組件應(yīng)包含提供業(yè)務(wù)價(jià)值所需的一切。這意味著,每個(gè)存儲(chǔ)、HTTP 處理程序或業(yè)務(wù)模型都應(yīng)該存儲(chǔ)在文件夾中。
- .
- ├── course
- │ ├── httphandler.go
- │ ├── model.go
- │ ├── repository.go
- │ └── service.go
- ├── main.go
- └── profile
- ├── httphandler.go
- ├── model.go
- ├── repository.go
- └── service.go
由于以這種方式組織代碼,當(dāng)你擁有與任務(wù)相關(guān)的課程時(shí),你就知道從哪里開(kāi)始尋找。它沒(méi)有分布在整個(gè)應(yīng)用程序中。然而,要實(shí)現(xiàn)良好的模塊化并不容易??赡苄枰啻蔚拍軐?shí)現(xiàn)一個(gè)好的封裝 API。
還有一個(gè)挑戰(zhàn)。如果這些包相互依賴怎么辦?假設(shè)你想在用戶的個(gè)人資料上顯示最近的課程。它們應(yīng)該共享相同的存儲(chǔ)庫(kù)或服務(wù)嗎?
在這種特殊情況下,從個(gè)人信息(profile)文件的角度來(lái)看,課程是一種外部依賴。解決該問(wèn)題的最佳方法是在 profile 必須需要的方法的包中創(chuàng)建一個(gè)接口。
- type Courses interface {
- MostRecent(ctx context.Context, userID string, max int) ([]course.Model, error)
- }
在course包中,你公開(kāi)了一個(gè)實(shí)現(xiàn)此接口的服務(wù)。
- type Courses struct {
- // maybe some unexported fields
- }
- Func (c Courses) MostRecent(ctx context.Context, userID string, max int) ([]Model, error) {
- // return most recent coursers for specific user
- }
在main.go中你從course包中創(chuàng)建Courses的結(jié)構(gòu)實(shí)例并將其傳遞給profile包。在profile包中的測(cè)試中,你創(chuàng)建了一個(gè) mock 實(shí)現(xiàn)。因此,你甚至可以在沒(méi)有course實(shí)現(xiàn)包的情況下開(kāi)發(fā)和測(cè)試 profile 功能。
如你所見(jiàn),模塊化使代碼更具可維護(hù)性和可讀性,但它需要你更加努力地思考你的決策和依賴關(guān)系。該邏輯可能看起來(lái)非常適合新包,但似乎太小了。另一方面,在處理項(xiàng)目期間,現(xiàn)有包的部分可能會(huì)開(kāi)始增多,并在一段時(shí)間后提升為自主代碼段。
當(dāng)代碼在包內(nèi)部增多時(shí),你可能會(huì)問(wèn)自己:如何組織單個(gè)模塊內(nèi)部的代碼?這是另一個(gè)難以回答的問(wèn)題。在本節(jié)中,我展示了使用應(yīng)用程序組件時(shí)的扁平結(jié)構(gòu)。但是,有時(shí)這還不夠……
04 簡(jiǎn)潔的架構(gòu)
你可能聽(tīng)說(shuō)過(guò)以下術(shù)語(yǔ):Clean Architecture[12]、Onion Architecture 或類似術(shù)語(yǔ)。Uncle Bob 寫(xiě)了一本書(shū)[13],詳細(xì)描述了每一層的含義以及應(yīng)該或不應(yīng)該包含的內(nèi)容。這個(gè)想法很簡(jiǎn)單。你的應(yīng)用程序或模塊有 4 層(取決于你的代碼庫(kù)有多大):Domain、Application、Ports、Adapters。在某些來(lái)源中,名稱可能不同。例如,作者沒(méi)有使用 Ports 和 Adapters,而是使用 Inbound 和 Outbound。核心思想類似。讓我們用例子來(lái)描述每一層。
Domain
這是我們應(yīng)用程序的核心。每個(gè)業(yè)務(wù)邏輯都應(yīng)該在這里。這意味著如果更改或添加任何業(yè)務(wù)需求,你必須更新我們的域部分。這個(gè)包應(yīng)該沒(méi)有任何外部依賴。它不應(yīng)該知道這段代碼是在哪個(gè)上下文中執(zhí)行的。這意味著,它不應(yīng)該依賴任何基礎(chǔ)設(shè)施部分或知道任何 UI 細(xì)節(jié)。
- course := NewCourse("How to use Go with smart devices?")
- s := course.AddSection("Getting started")
- l := s.AddLecture("Installing Go")
- l.AddAttachement("https://attachement.com/download")
- // etc
請(qǐng)注意,此時(shí)你并不關(guān)心課程的存儲(chǔ)位置或如何添加新課程(使用 HTTP 請(qǐng)求或使用 CLI)。在domain包中,你描述了課程可能包含的內(nèi)容以及你可以對(duì)其進(jìn)行的操作。就這些!
Application
該層包含應(yīng)用程序的每個(gè)用例。它是基礎(chǔ)設(shè)施和 Domain 之間的粘合點(diǎn)。在這個(gè)地方,你獲得輸入(從它來(lái)自的任何地方),將其應(yīng)用于域?qū)ο?,然后將其保存或發(fā)送到其他地方。
- func (c Course) Enroll(ctx context.Context, courseID, userID string) error {
- course, err := c.courseStorage.FindCourse(ctx, courseID)
- if err != nil {
- return fmt.Errorf("cannot find the course: %w")
- }
- user, err := c.userStorage.Find(ctx, userID)
- if err != nil {
- return fmt.Errorf("cannot find the user: %w")
- }
- if err = user.EnrollCourse(course); err != nil {
- return fmt.Errorf("cannot enroll the course: %w")
- }
- if err = c.userStorage(ctx, user); err != nil {
- return fmt.Errorf("cannot save the user: %w")
- }
- return nil
- }
在上面的代碼中,你可以找到用戶注冊(cè)課程的用例。它是兩部分的組合:與域?qū)ο?User,Course)和基礎(chǔ)設(shè)施(存儲(chǔ)和獲取數(shù)據(jù))交互。
Adapters
適配器也稱為 Outbound 或基礎(chǔ)設(shè)施(Infrastructure)。該層負(fù)責(zé)與外界存儲(chǔ)和獲取數(shù)據(jù)。它可以是數(shù)據(jù)庫(kù)、blob 存儲(chǔ)、文件系統(tǒng)或其他(微)服務(wù)。通常,該層在應(yīng)用程序?qū)拥慕涌谥芯哂衅浔硎拘问健K兄谠诓贿\(yùn)行數(shù)據(jù)庫(kù)或?qū)⑽募?xiě)入文件系統(tǒng)的情況下測(cè)試應(yīng)用程序?qū)印?/p>
適配器是對(duì)低級(jí)細(xì)節(jié)的抽象,因此你軟件的其他部分不必“知道”你使用的是哪個(gè)數(shù)據(jù)庫(kù)版本、SQL 查詢長(zhǎng)什么樣或你存儲(chǔ)文件的位置。
Ports
Ports(稱為 Inbound)是應(yīng)用程序的這一部分,負(fù)責(zé)從用戶那里獲取數(shù)據(jù)。它可以是 HTTP 處理程序、事件處理程序或 CLI 命令。它獲取用戶的輸入并將其傳遞給 Application 層。此操作的結(jié)果返回到 Port。
- func enrollCourse(w http.ResponseWriter, r *http.Request) {
- body, err := io.ReadAll(r.Body)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- logger.Errorf("cannot read the body: %s", err)
- return
- }
- req := enrollCourseRequest{}
- if err = json.Unmarshal(body, &req); err != nil {
- w.WriteHeader(http.StatusBadRequest)
- logger.Errorf("cannot unmarshal the request: %s", err)
- return
- }
- if err = validate.Struct(req); err != nil {
- w.WriteHeader(http.StatusBadRequest)
- logger.Errorf("cannot validate the request: %s", err)
- return
- }
- if err = app.EnrollCourse(req.CourseID, req.UserID); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- logger.Errorf("cannot enroll the course: %s", err)
- return
- }
- }
請(qǐng)注意,編寫(xiě)執(zhí)行相同邏輯的 CLI 命令很簡(jiǎn)單。唯一的區(qū)別是輸入的來(lái)源。
- var userID string
- var courseID string
- var enroleCourseCmd = &cobra.Command{
- Use: "courseID userID",
- Args: cobra.MinimumNArgs(2),
- Run: func(cmd *cobra.Command, args []string) {
- if err = app.EnrollCourse(courseID, userID); err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- logger.Errorf("cannot enroll the course: %s", err)
- return
- }
- },
- }
保持這些層的整潔和一致可能會(huì)給你的代碼帶來(lái)很多價(jià)值。它易于測(cè)試,職責(zé)明確,從哪里開(kāi)始尋找要更改的代碼更加明顯。如果是與課程相關(guān)的錯(cuò)誤并且是業(yè)務(wù)邏輯問(wèn)題,則你將開(kāi)始檢查 Domain 或 Application 層。
另一方面,很難保持界限清晰和一致。它需要大量的自律、經(jīng)驗(yàn)和至少幾次迭代才能正確完成。這就是為什么很多人在這個(gè)領(lǐng)域失敗的原因。
05 總結(jié)
組織代碼很困難。更困難的是,應(yīng)用程序的體系結(jié)構(gòu)在其生命周期內(nèi)可能會(huì)更改幾次,進(jìn)化。你可以從扁平結(jié)構(gòu)開(kāi)始,但最終會(huì)得到多個(gè)模塊和許多子包。不要期望第一次就做對(duì)。它可能需要多次迭代并從其他人那里收集反饋。
此外,你可以根據(jù)應(yīng)用程序的不同部分混合不同的代碼組織方式。在你的業(yè)務(wù)邏輯部分,可以從模塊化開(kāi)始。但是,許多應(yīng)用程序需要的實(shí)用程序就不適合用現(xiàn)有的包,你可以在那里遵循扁平結(jié)構(gòu)模式。
原文鏈接:https://developer20.com/how-to-structure-go-code/
參考資料
[1]Robert Griesemer: https://en.wikipedia.org/wiki/Robert_Griesemer
[2]Rob Pike: https://www.youtube.com/watch?v=PAAkCSZUG1c
[3]pkg.go.dev: https://pkg.go.dev/
[4]doc.go: https://github.com/golang/go/blob/master/src/net/http/doc.go
[5]sake: http://tonyfischetti.github.io/sake/
[6]mage: https://github.com/magefile/mage
[7]zim: https://github.com/fugue/zim/
[8]opctl: https://opctl.io/
[9]不穩(wěn)定: https://testing.googleblog.com/2020/12/test-flakiness-one-of-main-challenges.html
[10]golangci-lint: https://github.com/golangci/golangci-lint
[11]net/http/pprof: https://pkg.go.dev/net/http/pprof
[12]Clean Architecture: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
[13]寫(xiě)了一本書(shū): https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164
本文轉(zhuǎn)載自微信公眾號(hào)「幽鬼」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系幽鬼公眾號(hào)。