關(guān)于 Go 代碼結(jié)構(gòu)的思考
應(yīng)用程序結(jié)構(gòu)復(fù)雜。
良好的應(yīng)用程序結(jié)構(gòu)可提升開發(fā)人員體驗。開發(fā)者可以在不記住整個代碼倉庫的情況下, 專注于他們正在處理的內(nèi)容。一個結(jié)構(gòu)良好的應(yīng)用程序可以通過解耦組件和容易編寫有用的測試來幫助防止錯誤。
結(jié)構(gòu)不佳的應(yīng)用程序可能會適得其反;它會使測試變得更難,并難以查找相關(guān)代碼。它還會引入不必要的復(fù)雜性和冗余,從而拖累您的開發(fā)速度。
最后一點很重要——使用比實際所需復(fù)雜得多的結(jié)構(gòu)是弊大于利的。
我在這里寫的東西對任何人來說可能都不是新鮮事。程序員很早就被教導(dǎo)組織代碼的重要性。無論是命名變量和函數(shù),還是命名和組織文件,這幾乎是每門編程課程的早期主題。
所有這些都引出了一個問題—— 為什么很難弄清楚如何結(jié)構(gòu)化 Go 代碼?

按環(huán)境組織

在過去的 Go Time 問答集中,我們被問及如何構(gòu)建 Go 應(yīng)用程序,Peter Bourgon 回答如下:
很多語言對所有項目結(jié)構(gòu)的約定大致相同,對于相同類型的項目......就像,如果你在 Ruby 中做一個 Web 服務(wù),你會有這個布局,并且這些包將以您正在使用的架構(gòu)模式命名。例如,MVC、控制器等。但在 Go 中,這并不是我們真正要做的。我們的包和項目結(jié)構(gòu)基本上反映了我們正在實現(xiàn)的內(nèi)容。不是我們使用的模式,不是腳手架,而是我們正在從事的項目領(lǐng)域中的特定類型和實體。因此,在 Go 中程序結(jié)構(gòu)和項目本身有很大的關(guān)系。。對一個項目有意義的事情可能對另一個是沒有意義的。這并不是說這是做事的唯一方式,但這是我們傾向于做的事情......所以是的,這是沒有答案的,毋庸置疑的是在語言中使用慣用語會讓很多人感到非常困惑,而且結(jié)果可能也會說明是錯誤的選擇……我不知道,但我認為這是重點。Peter Bourgon 在 Go Time #147上提到。
大體上,大多數(shù)成功的 Go 應(yīng)用程序的結(jié)構(gòu)都不會是從別的項目照搬的。也就是說,我們不能采用通用文件夾結(jié)構(gòu)并將其復(fù)制到新應(yīng)用程序并期望它能夠工作,因為新應(yīng)用程序很可能有一組獨特的環(huán)境可供使用。
開始的最好方法是考慮應(yīng)用程序的環(huán)境,而不是尋找要復(fù)制的模板。為了幫助您理解我的意思,讓我們嘗試了解如何構(gòu)建用于托管我的 Go 課程的 Web 應(yīng)用程序。

背景信息:我的 Go 課程應(yīng)用程序是一個網(wǎng)站,學(xué)生可以在其中注冊課程并查看課程中的個別課程。大多數(shù)課程都有視頻組件、課程中使用的代碼鏈接以及其他相關(guān)信息。如果您曾經(jīng)使用過任何視頻課程網(wǎng)站,您應(yīng)該對它的外觀有一個大致的了解,但如果您想進一步挖掘,您可以免費注冊 Gophercises
在這一點上,我已經(jīng)非常熟悉應(yīng)用程序的需求,但我將嘗試引導(dǎo)您完成我最初開始創(chuàng)建應(yīng)用程序時的思考過程,因為這是您經(jīng)常在開始時的狀態(tài)。
開始時,我考慮了兩個主要環(huán)境:
學(xué)生 管理員/教師
學(xué)生環(huán)境是大多數(shù)人所熟悉的。在這種情況下,用戶登錄帳戶,查看包含他們有權(quán)訪問的課程的儀表板,然后可以導(dǎo)航到各個課程。
管理員環(huán)境有點不同,大多數(shù)人不會看到它。作為管理員,我們不太擔心課程的消費,而是更關(guān)心管理它們。我們需要能夠為課程添加新課程、更新現(xiàn)有課程的視頻等等。除了能夠管理課程之外,管理員環(huán)境還需要管理用戶、購買和退款。
為了創(chuàng)建這種分離環(huán)境,我的代碼倉庫將從兩個包開始:
admin/
??...?(some?go?files?here)
student/
??...?(some?go?files?here)
通過分離這兩個包,我能夠在每個環(huán)境中以不同的方式定義實體。例如,Lesson 從學(xué)生 a 的角度來看,主要由資源的 URL 組成,并且它具有特定于用戶的信息,例如 CompletedAt代表該特定用戶何時/是否完成課程的字段。
package?student
type?Lesson?struct?{
??Name?????????string?
??Video????????string?
??SourceCode???string?
??CompletedAt??*time.Time?
}
同時,管理員的 Lesson 類型沒有 CompletedAt 字段,因為在這種情況下這沒有意義。該信息僅與登錄用戶查看課程相關(guān),與管理課程內(nèi)容的管理員無關(guān)。
相反,管理員的 Lesson 類型將提供對諸如 Requirement 之類的字段用于確定用戶是否有權(quán)訪問內(nèi)容。其他字段看起來也會有些不同;Video 字段可能不是視頻的 URL,而是有關(guān)視頻托管位置的信息,因為這是管理員更新內(nèi)容的方式。
package?admin
type?Lesson?struct?{
??Name?string
??Video?struct?{
????Provider?string?
????ExternalID?string
??}
??SourceCode?struct?{
????Provider?string?
????Repo?????string?
????Branch???string?
??}
??Requirement?string
}
我選擇這樣組織是因為我相信這兩種情況的差異足以證明分離是合理的,但我對其足以適用于任何進一步的組織表示質(zhì)疑。
我可以用不同的方式組織這段代碼嗎?絕對可以的!
我可能會改變結(jié)構(gòu)的一種方法是進一步分離它。例如,admin 包的一些代碼與管理用戶有關(guān),而其他代碼則與管理課程有關(guān)。將其分為兩個也會很容易。或者,我可以提取所有與身份驗證相關(guān)的代碼——注冊、更改密碼等,并將其放入一個 auth 包中。
與其想太多,不如選擇一些看起來相當合適的方式并根據(jù)需要進行調(diào)整更有用。

包作為層

另一種分解應(yīng)用程序的方法是依賴關(guān)系。Ben Johnson 在 gobeyond.dev 上對此進行了很好的討論,特別是在文章 Packages as layers, not groups 中。這個概念與 Kat Zien 在 GopherCon 演講 “你如何構(gòu)建你的 Go 應(yīng)用程序” 中提到的六邊形架構(gòu) 非常相似。
在更深層次上,我們的想法是我們有一個核心域,在其中定義我們的資源和用來與它們交互的服務(wù)。
package?app
type?Lesson?struct?{
??ID?string
??Name?string
}
type?LessonStore?interface?{
??Create(*Lesson)?error
??QueryByPermissions(...Permission)?([]Lesson,?error)
}
使用類似 Lesson 類型和類似LessonStore接口,我們可以編寫一個完整的應(yīng)用程序。沒有實現(xiàn) LessonStore 我們就無法運行我們的程序,但是我們可以編寫所有的核心邏輯而不用擔心它是如何實現(xiàn)的。
當我們準備好實現(xiàn) LessonStore 接口時,我們的應(yīng)用程序就添加一個新層。在這種情況下,它可能是一個 sql 包的形式。
package?sql
type?LessonStore?struct?{
??db?*sql.DB
}
func?Create(l?*Lesson)?error?{
}
func?QueryByPermissions(perms?...Permission)?([]Lesson,?error)?{
}
如果您想了解有關(guān)此策略的更多信息,我強烈建議您查看 Ben 在https://www.gobeyond.dev/ 上的文章。
逐層打包的方法似乎與我在 Go 課程中選擇的方法大不相同,但實際上混合適用這些策略比最開始的方法要容易得多。例如,如果我們將 admin 和student 視為定義資源和服務(wù)的域,我們可以通過逐層封裝的方法來實現(xiàn)這些服務(wù)。下面是一個使用 admin 包和實現(xiàn)了 admin.LessonStore 的 sql 包.
package?admin
type?Lesson?struct?{
}
type?LessonStore?interface?{
??Create(*Lesson)?error
}
package?sql
import?"github.com/joncalhoun/my-app/admin"
type?AdminLessonStore?struct?{?...?}
func?(ls?*AdminLessonStore)?Create(lesson?*admin.Lesson)?error?{?...?}
這是應(yīng)用程序的正確選擇嗎?我不知道。
使用這樣的接口確實可以更輕松地測試更小的代碼片段,但這只有在它提供真正的好處時才重要。否則,我們最終寫完了接口、解耦了代碼、創(chuàng)建了新包都是無用功,基本上,我們的忙碌都是自己造成的。

唯一錯誤的決定是沒有決定

除了這些結(jié)構(gòu)之外,還有無數(shù)其他有意義的方式來構(gòu)建(或不構(gòu)建)代碼,這取決于環(huán)境。我已經(jīng)在多個項目中嘗試過使用扁平結(jié)構(gòu)——一個單獨的包——但我仍然對它的效果感到震驚。當我第一次開始寫 Go 代碼時,我?guī)缀踔皇褂?MVC。這不僅比整個社區(qū)可能讓你相信的效果更好,而且它讓我克服了由于不知道如何構(gòu)建我的應(yīng)用程序而導(dǎo)致的決策癱瘓。
在 Q&A Go Time 的同一期中,我們被問及如何構(gòu)建 Go 代碼,Mat Ryer 闡明了沒有固定的代碼結(jié)構(gòu)方式的好處:
我認為這可能是非常自由的,也是說,沒有確切的方法可以做到這一點,也意味著你無法真正做錯了。這完全適用于你的情況。Mat Ryer 在 Go Time #147 提到。
如今我有豐富的 Go 使用經(jīng)驗,我完全同意 Mat 的觀點。這種方式來決定每個應(yīng)用程序適合的哪些結(jié)構(gòu)是解放式的。我喜歡沒有固定的做事方式,也沒有真正的錯誤方式。盡管現(xiàn)在有這種感覺,但我還記得在我經(jīng)驗不足的時候沒有具體的例子可以用時感到非常沮喪。
事實是,如果沒有一些經(jīng)驗,幾乎不可能決定哪種結(jié)構(gòu)適合您的情況,但這又強迫我們在獲得任何經(jīng)驗之前做出決定。這是我們?nèi)腴T之前的一個兩難情形。
在這種情況下我沒有放棄,而是選擇了我所知道的結(jié)構(gòu)——MVC。這讓我可以編寫代碼,讓某些東西正常工作,并從這些錯誤中吸取教訓(xùn)。隨著時間的推移,我開始了解構(gòu)建代碼的其他方式,我的應(yīng)用程序越來越不像 MVC,但這是一個非常漸進的過程。我懷疑如果我強迫自己立即獲知正確的應(yīng)用程序結(jié)構(gòu),我根本不會成功。只在經(jīng)歷了很大的挫折之后,我才會成功。
毫無疑問,MVC 永遠不會像為項目量身定制的應(yīng)用程序結(jié)構(gòu)那樣清晰。同樣,對于幾乎沒有構(gòu)建 Go 代碼經(jīng)驗的人來說,為項目發(fā)現(xiàn)理想的應(yīng)用程序結(jié)構(gòu)并不是一個現(xiàn)實的目標。這需要練習(xí)、試驗和重構(gòu)才能做到正確。MVC 簡單易懂。當我們沒有足夠的經(jīng)驗或背景來想出更好的東西時,這是一個合理的起點。

總結(jié)

正如我在本文開頭所說,良好的應(yīng)用程序結(jié)構(gòu)旨在改善開發(fā)人員體驗。它旨在幫助您以對您有意義的方式組織代碼。這并不意味著讓新來者陷入困境并不知道如何進行下去。
如果您發(fā)現(xiàn)自己陷入困境并且不確定如何繼續(xù),請問下自己怎樣會更有效率 - 仍然陷入困境,或者選擇任何一個應(yīng)用程序結(jié)構(gòu)并嘗試一下?
使用前者,什么都做不了。使用后者,即使你做錯了,你也可以從經(jīng)驗中吸取教訓(xùn),下次做得更好。這聽起來都比從不開始要好得多。
原文信息
原文地址:https://changelog.com/posts/on-go-application-structure
原文作者:Jon Calhoun
本文永久鏈接:https://github.com/gocn/translator/blob/master/2022/w2_Thoughts_on_how_to_structure_Go_code.md
譯者:lsj1342
校對:xkkhy、zhuyaguang
想要了解關(guān)于 Go 的更多資訊,還可以通過掃描的方式,進群一起探討哦~
ps:群內(nèi)還有 13 個空位,拼手速的時候到了(bushi)!?還可以加 微信號:gocnio 讓小編拉你進群哦
