關於 Go 程式碼結構的思考

lsj1342 發表於 2022-01-18
Go

關於 Go 程式碼結構的思考


應用程式結構複雜。

良好的應用程式結構可提升開發人員體驗。開發者可以在不記住整個程式碼倉庫的情況下, 專注於他們正在處理的內容。一個結構良好的應用程式可以通過解耦元件和容易編寫有用的測試來幫助防止錯誤。

結構不佳的應用程式可能會適得其反;它會使測試變得更難,並難以查詢相關程式碼。它還會引入不必要的複雜性和冗餘,從而拖累您的開發速度。

最後一點很重要——使用比實際所需複雜得多的結構是弊大於利的。

我在這裡寫的東西對任何人來說可能都不是新鮮事。程式設計師很早就被教導組織程式碼的重要性。無論是命名變數和函式,還是命名和組織檔案,這幾乎是每門程式設計課程的早期主題。

所有這些都引出了一個問題—— 為什麼很難弄清楚如何結構化 Go 程式碼?

按環境組織

在過去的 Go Time 問答集中,我們被問及如何構建 Go 應用程式,Peter Bourgon 回答如下:

很多語言對所有專案結構的約定大致相同,對於相同型別的專案......就像,如果你在 Ruby 中做一個 Web 服務,你會有這個佈局,並且這些包將以您正在使用的架構模式命名。例如,MVC、控制器等。但在 Go 中,這並不是我們真正要做的。我們的包和專案結構基本上反映了我們正在實現的內容。不是我們使用的模式,不是腳手架,而是我們正在從事的專案領域中的特定型別和實體。

因此,在 Go 中程式結構和專案本身有很大的關係。。對一個專案有意義的事情可能對另一個是沒有意義的。這並不是說這是做事的唯一方式,但這是我們傾向於做的事情......所以是的,這是沒有答案的,毋庸置疑的是在語言中使用慣用語會讓很多人感到非常困惑,而且結果可能也會說明是錯誤的選擇……我不知道,但我認為這是重點。

Peter BourgonGo Time #147上提到。

大體上,大多數成功的 Go 應用程式的結構都不會是從別的專案照搬的。也就是說,我們不能採用通用資料夾結構並將其複製到新應用程式並期望它能夠工作,因為新應用程式很可能有一組獨特的環境可供使用。

開始的最好方法是考慮應用程式的環境,而不是尋找要複製的模板。為了幫助您理解我的意思,讓我們嘗試瞭解如何構建用於託管我的 Go 課程的 Web 應用程式。

A screenshot of Jon's Go courses dashboard

_背景資訊:我的 Go 課程應用程式是一個網站,學生可以在其中註冊課程並檢視課程中的個別課程。大多數課程都有視訊元件、課程中使用的程式碼連結以及其他相關資訊。如果您曾經使用過任何視訊課程網站,您應該對它的外觀有一個大致的瞭解,但如果您想進一步挖掘,您可以免費註冊 Gophercises _

在這一點上,我已經非常熟悉應用程式的需求,但我將嘗試引導您完成我最初開始建立應用程式時的思考過程,因為這是您經常在開始時的狀態。

開始時,我考慮了兩個主要環境:

  1. 學生
  2. 管理員/教師

學生環境是大多數人所熟悉的。在這種情況下,使用者登入帳戶,檢視包含他們有權訪問的課程的儀表板,然後可以導航到各個課程。

管理員環境有點不同,大多數人不會看到它。作為管理員,我們不太擔心課程的消費,而是更關心管理它們。我們需要能夠為課程新增新課程、更新現有課程的視訊等等。除了能夠管理課程之外,管理員環境還需要管理使用者、購買和退款。

為了建立這種分離環境,我的程式碼倉庫將從兩個包開始:

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

通過分離這兩個包,我能夠在每個環境中以不同的方式定義實體。例如,Lesson 從學生 a 的角度來看,主要由資源的 URL 組成,並且它具有特定於使用者的資訊,例如 CompletedAt 代表該特定使用者何時/是否完成課程的欄位。

package student

type Lesson struct {
  Name         string 
  Video        string 

  SourceCode   string 
  CompletedAt  *time.Time 

}

同時,管理員的 Lesson 型別沒有 CompletedAt 欄位,因為在這種情況下這沒有意義。該資訊僅與登入使用者檢視課程相關,與管理課程內容的管理員無關。

相反,管理員的 Lesson 型別將提供對諸如 Requirement 之類的欄位用於確定使用者是否有權訪問內容。其他欄位看起來也會有些不同;Video 欄位可能不是視訊的 URL,而是有關視訊託管位置的資訊,因為這是管理員更新內容的方式。

package admin

type Lesson struct {
  Name string

  Video struct {
    Provider string 
    ExternalID string
  }

  SourceCode struct {
    Provider string 
    Repo     string 
    Branch   string 
  }

  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 包和實現了 admin.LessonStoresql 包.

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

這是應用程式的正確選擇嗎?我不知道。

使用這樣的介面確實可以更輕鬆地測試更小的程式碼片段,但這隻有在它提供真正的好處時才重要。否則,我們最終寫完了介面、解耦了程式碼、建立了新包都是無用功,基本上,我們的忙碌都是自己造成的。

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

除了這些結構之外,還有無數其他有意義的方式來構建(或不構建)程式碼,這取決於環境。我已經在多個專案中嘗試過使用扁平結構——一個單獨的包——但我仍然對它的效果感到震驚。當我第一次開始寫 Go 程式碼時,我幾乎只使用 MVC。這不僅比整個社群可能讓你相信的效果更好,而且它讓我克服了由於不知道如何構建我的應用程式而導致的決策癱瘓。

在 Q&A Go Time 的同一期中,我們被問及如何構建 Go 程式碼,Mat Ryer 闡明瞭沒有固定的程式碼結構方式的好處:

我認為這可能是非常自由的,也是說,沒有確切的方法可以做到這一點,也意味著你無法真正做錯了。這完全適用於你的情況。

Mat RyerGo Time #147 提到。

如今我有豐富的 Go 使用經驗,我完全同意 Mat 的觀點。這種方式來決定每個應用程式適合的哪些結構是解放式的。我喜歡沒有固定的做事方式,也沒有真正的錯誤方式。儘管現在有這種感覺,但我還記得在我經驗不足的時候沒有具體的例子可以用時感到非常沮喪。

事實是,如果沒有一些經驗,幾乎不可能決定哪種結構適合您的情況,但這又強迫我們在獲得任何經驗之前做出決定。這是我們入門之前的一個兩難情形。

在這種情況下我沒有放棄,而是選擇了我所知道的結構——MVC。這讓我可以編寫程式碼,讓某些東西正常工作,並從這些錯誤中吸取教訓。隨著時間的推移,我開始瞭解構建程式碼的其他方式,我的應用程式越來越不像 MVC,但這是一個非常漸進的過程。我懷疑如果我強迫自己立即獲知正確的應用程式結構,我根本不會成功。只在經歷了很大的挫折之後,我才會成功。

毫無疑問,MVC 永遠不會像為專案量身定製的應用程式結構那樣清晰。同樣,對於幾乎沒有構建 Go 程式碼經驗的人來說,為專案發現理想的應用程式結構並不是一個現實的目標。這需要練習、試驗和重構才能做到正確。MVC 簡單易懂。當我們沒有足夠的經驗或背景來想出更好的東西時,這是一個合理的起點。

總結

正如我在本文開頭所說,良好的應用程式結構旨在改善開發人員體驗。它旨在幫助您以對您有意義的方式組織程式碼。這並不意味著讓新來者陷入困境並不知道如何進行下去。

如果您發現自己陷入困境並且不確定如何繼續,請問下自己怎樣會更有效率 - 仍然陷入困境,或者選擇任何一個應用程式結構並嘗試一下?

使用前者,什麼都做不了。使用後者,即使你做錯了,你也可以從經驗中吸取教訓,下次做得更好。這聽起來都比從不開始要好得多。


更多原創文章乾貨分享,請關注公眾號
  • 關於 Go 程式碼結構的思考
  • 加微信實戰群請加微信(註明:實戰群):gocnio