淺談如何組織 Go 代碼結構

原文地址:https://changelog.com/posts/on-go-application-structure

原文作者:Jon Calhoun

本文永久鏈接:https://github.com/gocn/translator/blob/master/2021/w20_Thoughts_on_how_to_structure_Go_code.md

譯者:Fivezh

應用程序的結構很是困擾開發者。

好的程序結構可以改善開發者的體驗。它可以幫助你隔離正在進行中的內容,而不必將整個代碼庫放在腦中。一個結構良好的應用程序可通過解耦和易於編寫的測試來幫助避免錯誤。

一個結構不佳的應用程序卻適得其反。它使得測試更加困難,找到相關代碼也異常複雜,並可能引入非必要的複雜性和冗長的代碼,這些將拖慢開發速度卻沒有一點好處。

最後一點很重要:使用一個遠比需要複雜的程序結構,實際上對項目的傷害比幫助更大。

我正在寫的東西可能對任何人來說都不是新聞。程序員很早就被告知合理組織代碼的重要性。無論是變量和函數命名,還是文件的命名和組織,這幾乎是每一個編程課中早期涉及的主題。

所有這些都引出了一個問題:搞清楚如何構建 Go 代碼爲何如此困難?

通過上下文來組織

在過去的 Q&A 中,我們被問及 Go 應用的結構問題,Peter Bourgon 的回答是這樣的:

很多語言都有這樣的慣例(我猜),對於同一類型的項目,所有項目的結構都大致相同... 如果你用 Ruby 做一個 web 服務,你會有這樣的佈局,程序包會以你使用的架構模式來命名。以 MVC 爲例,控制器等等。但是在 Go 中,這並不是我們真正要做的。我們的程序包和項目結構基本能反映出我們正在實施事務所在的領域。不是所使用的模式,也不是腳手架,而是取決於當前項目所在領域中的特定類型和實體。** 因此,從定義上講,不同項目在習慣上總是有所不同。** 在一個項目中有意義的,在另一個項目中可能就沒有意義。不是說這裏是唯一的方法,但這是我們傾向於的一種選擇...... 因此,是的,這個問題沒有答案,那種關於語言中約定俗成讓很多人非常困惑,但結果可能是錯誤的選擇...... 我不知道,但我想這是主要的一點。Peter Bourgon 在 Go Time #147 中的回答。其中的加粗是我標註的。

總的來說,大多數成功的 Go 應用程序的結構並不能從一個項目複製 / 粘貼到另一個項目。也就是說,我們不能把一般的文件夾結構複製到一個新的應用程序,並期望它能正常工作,因爲新的應用程序很可能有一套獨特的上下文來工作。

與其尋找一個可以複製的模板,不如讓我們從思考應用程序的上下文來開始。爲了幫助你能理解我的意思,讓我們來一起看下,我是如何構建用於託管 Go 課程的網絡應用程序的。

背景信息:這個 Go 課程應用程序是一個網站,學生在這裏註冊課程並查看課程內容。大多數課程都有視頻、鏈接(課程中使用的代碼)、以及其他相關信息。如果你曾經使用過任何視頻課程網站,你應該對它的外觀有一個大致的瞭解,但如果你想進一步挖掘,你可以免費註冊 Gophercises。。

在這一點上,我對應用程序的需求相當熟悉,但我要試着帶領你瞭解最初開始創建程序時的思考過程,因爲那是經常要開始的狀態。

開始的時候,有兩個主要內容上下文需要考慮:

學生上下文 管理員 / 老師上下文 學生上下文是大多數人所熟悉的。在這種情況下,用戶登錄到一個賬戶,查看他們可以訪問的課程儀表板,然後向下導航到課程內容。

管理員的上下文有點不同,大多數人不會看到它。作爲管理員,不用擔心消費課程,而更關心如何管理它們。我們需要能添加新的課程,更新現有課程視頻,以及其他。除了能夠管理課程之外,管理員還需要管理用戶、購買和退款。

爲了創建這種分離,我的倉庫將從兩個包開始:

admin/
  ... (some go files here)
student/
  ... (some go files here)

通過分離這兩個包,我能夠在每種情況下以不同的方式定義實體。例如,從學生的角度來看,Lesson類型主要由指向不同資源的一些 URL 組成,它有用戶相關信息,如 CompletedAt 字段表明該特定用戶何時 / 是否完成課程。

package student

type Lesson struct {
  Name         string // 課程名, 比如: "如何寫測試"
  Video        string // 課程視頻url,空則用戶無權訪問
  SourceCode   string // 課程源碼url
  CompletedAt  *time.Time // 表示該用戶是否完成課程的布爾值或完成時間
  // + 更多字段
}

同時,管理員的 Lesson 類型沒有 CompletedAt 字段,因爲在這種上下文情況下是沒有意義。這些信息只對登錄用戶查看課程有關,而不是管理員管理課程的內容。

相反,管理員 Lesson 類型將提供對 Requirement 等字段的訪問,這些字段被用來確定用戶是否可以訪問此內容。其他字段看起來也會有些不同;Video 字段不是視頻的 URL,而是視頻託管地點的信息,因爲這是管理員更新內容的方式。

Instead, the admin Lesson type will provide access to fields like Requirement, which will be used to determine if a user has access to content. Other fields will look a bit different as well; rather than a URL to the video, the Video field might instead be information about where the video is hosted, as this is how admins will update the content.

package admin

// 爲了簡潔起見,本例使用內聯結構
type Lesson struct {
  Name string
  // 視頻URL可以使用這些信息動態構建(在某些情況下,使用時間限制的訪問令牌)
  Video struct {
    Provider string // Youtube, Vimeo, 等
    ExternalID string
  }
  // 決定源碼資源URL的有關信息,如 `repo/branch`
  SourceCode struct {
    Provider string // Github, Gitlab, 等
    Repo     string // 比如 "gophercises/quiz"
    Branch   string // 比如 "solution-p1"
  }

  // 用來確定用戶是否有權限學習本課。
  // 通常是類似於 "twg-base "的字符串,當用戶購買課程許可證時,將有這些權限字符串鏈接到他們的賬戶。這可能不是最有效的方法,但現在已經足夠用了,而且可以很容易地製作提供多個課程訪問權限的程序包。
  Requirement string
}

我採用這種方式是因爲相信這兩種情況會有足夠的差異,以證明這種分離是合理的,但我也懷疑這兩種情況都不會發展到足夠大,以證明未來任何進一步的組織。

我可以用不同的方式組織這些代碼嗎?當然可以。

我可能改變結構的一個方法是進一步分離它。例如, admin包的一些代碼與管理用戶有關,而另一些代碼與管理課程有關。把它分成兩個部分是很容易的。另外,可以把所有與認證有關的代碼 (註冊、修改密碼等) 放到一個 auth 包中。

與其過度思考,挑選看起來合適的方案並按需進行調整更有意義。

以層的方式組織包結構

另一種分割程序的方法是通過依賴關係。Ben Johnson 在 gobeyond.dev,特別是在 Packages as layers, not groups 一文中對此進行了很好的討論。這個概念與 Kat Zien 在 GopherCon 演講中提到的六邊形架構非常相似,"你如何組織 Go 應用程序的結構"。

從較高的角度來看,我們的想法是我們擁有一個核心域,在其中定義資源和與之交互所使用的服務。

package app

type Lesson struct {
  ID string
  Name string
  // ...
}

type LessonStore interface {
  Create(*Lesson) error
  QueryByPermissions(...Permission) ([]Lesson, error)
  // ...
}

使用像 Lesson 這樣的類型和 LessonStore 這樣的接口,我們可以編寫一個完整的應用程序。如果沒有 LessonStore 的實現,我們就不能運行程序,但可以編寫所有的核心邏輯,而不必擔心如何實現。

當我們準備好實現像 LessonStore 接口時,我們會給程序添加一個新的層。在這種情況下,它可能是以 sql 包的形式出現。

package sql

type LessonStore struct {
  db *sql.DB
}

func Create(l *Lesson) error {
  // ...
}

func QueryByPermissions(perms ...Permission) ([]Lesson, error) {
  // ...
}

想更多瞭解這一策略,建議去看看 Ben 的文章,網址是 https://www.gobeyond.dev/

譯者注:該文章的中文譯稿 以層的方式而不是組的方式進行包管理

分層打包的方式似乎與上述 Go 課程中選擇的方法大相徑庭,但實際上,混合這些策略要比最初看起來容易得多。例如,如果把 adminstudent 分別作爲一個定義了資源和服務的域,就可以使用分層打包的方法來實現這些服務。下面是使用 admin 包域和 sql 包的例子,其中 sql 包有一個 admin.LessonStore 的實現。

package admin

type Lesson struct {
  // ... same as before
}

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 { ... }

上面這些是對該應用的正確選擇嗎?我不知道。

使用這樣的接口會使測試代碼片斷變得容易,但這隻有在它真正有用時才重要。否則,我們寫接口,解耦代碼,並創建新的包,但卻沒有看到真正的好處。這樣一來,這種方案只會讓自己更加忙碌。

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

除了以上這些結構之外,還有無數種(或無結構)組織代碼的方法,根據不同的上下文這些方法也是有意義的。我曾在一些項目中嘗試扁平結構(單一的包),我仍然對這種方式的效果感到震驚。當剛開始寫 Go 代碼時,我幾乎只使用 MVC。這不僅比整個社區引導的更好,而且擺脫了因不知道如何佈局程序結構的困境,避免了不知道從哪裏開始的難題。

在同一 Q&A 中,我們被問到如何組織 Go 代碼,Mat Ryer 表達了沒有一個固定方式來組織代碼的好處:

我認爲,這裏是非常自由的,雖然說沒有真正的方法,但這也意味着你不會做錯。適合你的情況纔是好的選擇。Mat Ryer 在 Go Time #147 中發表的觀點

現在我有很多使用 Go 的經驗,我完全同意 Mat 的觀點。決定一個應用適合什麼樣的結構,這是一種自有。我喜歡沒有一個固定的方法,也沒有一個錯誤的方法。儘管現在有這種感覺,但也記得在我經驗不足的時候,因爲沒有具體的例子可以參考而感到非常沮喪。

事實是,如果沒有一些經驗,決定什麼結構適合你的情況幾乎是不可能的,但現實往往迫使我們在獲得任何經驗之前就得選擇。這是一個《第 22 條軍規》陷阱,在還沒有開始的時候就阻止了我們。

然而我並沒有放棄,而是選擇了使用所知道的東西:MVC。這使我能夠編寫代碼,獲得一些工作,並從這些錯誤中學習。隨着時間的推移,開始理解其他的代碼結構方式,我的應用程序與 MVC 的相似度越來越低,但這是一個漸進的過程。我甚至懷疑,如果一開始就強迫自己立即弄好程序的結構,這根本就不會成功。最多隻能在經歷了大量的挫折之後獲得成功。

絕對正確的是,MVC 永遠不會像爲項目量身定做的應用結構那樣提供清晰的信息。同樣正確的是,對於一個幾乎沒有 Go 代碼經驗的人來說,發掘項目的理想結構並不是一個現實的目標。它需要實踐、實驗和重構來獲得正確的結果。MVC 是簡單而平易近人的。當我們沒有足夠的經驗或上下文來想出更好的選擇時,它會是一個合理的開始。

總結

正如在本文開頭所說,好的程序結構是爲了改善開發者的體驗。它是爲了幫助你以一種有意義的方式來組織代碼。它並不是要讓新手們陷入癱瘓、不知道該如何繼續。

如果你發現自己被卡住了,不知道如何繼續,問問自己什麼是更有效的:繼續卡住,還是挑選一個結構並加以嘗試?

對於前者,什麼都做不了。對於後者,即使你做錯了,也可以從中學習經驗,並在下一次做得更好。這聽起來比永遠不開始要好得多。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/9_WQUpvHKli4btPqCA89Aw