從理論到實踐:Go 項目中的整潔架構設計
前言
你維護的Go 項目代碼架構是什麼樣子的?六邊形架構?還是洋蔥架構?亦或者是DDD?無論項目採用的是什麼架構,核心目標都應是一致的:使代碼能夠易於理解、測試和維護。
本文將從Bob 大叔的整潔架構(Clean Architecture)出發,簡要解析其核心思想,並結合go-clean-arch 倉庫,深入探討如何在Go 項目中實現這一架構理念。
準備好了嗎?準備一杯你最喜歡的咖啡或茶,隨着本文一探究竟吧。
整潔架構
整潔架構(Clean Architecture)是Bob 大叔提出的一個軟件架構設計理念,旨在通過分層結構和明確的依賴規則,使軟件系統更易於理解、測試和維護。其核心思想是分離關注點,確保系統中的核心業務邏輯(Use Cases)不依賴於實現細節(如框架、數據庫等)。
Clean Architecture 的核心思想是獨立性:
-
獨立於框架:不依賴特定的框架(如
Gin、GRPC等)。框架應該是工具,而不是架構的核心。 -
獨立於
UI:用戶界面可以輕鬆更改,而不影響系統的其他部分。例如,Web UI可以被替換爲控制檯UI,無需修改業務規則。 -
獨立於數據庫:可以更換數據庫(如從
MySQL換成MongoDB),而不影響核心業務邏輯。 -
獨立於外部工具:外部依賴(如第三方庫)應該被隔離,避免其對系統核心的直接影響。
結構圖
如圖所示,Clean Architecture 以同心圓 的方式描述,其中的每一層表示不同的系統職責:
-
核心實體(
Entities)-
位置:最內層
-
職責:定義系統的業務規則。實體是應用中最核心的對象,具有獨立的生命週期。
-
獨立性:完全獨立於業務規則,只隨着業務規則變化。
-
-
用例(
Use Cases/Service)-
位置:緊鄰實體的一層
-
職責:實現應用的業務邏輯。定義系統中各種操作(用例)的流程,確保用戶的需求被滿足。
-
作用:用例調用實體層,協調數據流向,並確定響應。
-
-
接口適配器(
Interface Adapters)-
位置:更外的一層
-
職責:負責將外部系統的數據(如 UI、數據庫等)轉化爲內層能理解的格式,同時也用於將核心業務邏輯轉換爲外部系統可用的形式。 例如:將
HTTP請求的數據轉化爲內部的模型(例如類或結構體),或者將用例輸出的數據展示給用戶。 -
組件:包括控制器、網關(
Gateways)、Presenter等。
-
-
外部框架與驅動(
Frameworks & Drivers)-
位置:最外層
-
職責:實現與外部世界的交互,如數據庫、
UI、消息隊列等。 -
特點:這層依賴內層,反過來則不成立。這是系統中最容易更換的部分。
-
go-clean-arch 項目
go-clean-arch 是實現整潔架構(Clean Architecture)的一個Go 示例項目。該項目有四個領域層(Domain Layer):
-
Models Layer模型層package domain import ( "time" ) type Article struct { ID int64 `json:"id"` Title string `json:"title" validate:"required"` Content string `json:"content" validate:"required"` Author Author `json:"author"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` }-
作用:定義領域的核心數據結構,負責描述項目中的業務實體,例如文章、作者 等。
-
對應理論層:實體層(
Entities)。 -
示例:
-
-
Repository Layer存儲層package mysql import ( "context" "database/sql" "fmt" "github.com/sirupsen/logrus" "github.com/bxcodec/go-clean-arch/domain" "github.com/bxcodec/go-clean-arch/internal/repository" ) type ArticleRepository struct { Conn *sql.DB } // NewArticleRepository will create an object that represent the article.Repository interface func NewArticleRepository(conn *sql.DB) *ArticleRepository { return &ArticleRepository{conn} } func (m *ArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) { rows, err := m.Conn.QueryContext(ctx, query, args...) if err != nil { logrus.Error(err) return nil, err } defer func() { errRow := rows.Close() if errRow != nil { logrus.Error(errRow) } }() result = make([]domain.Article, 0) for rows.Next() { t := domain.Article{} authorID := int64(0) err = rows.Scan( &t.ID, &t.Title, &t.Content, &authorID, &t.UpdatedAt, &t.CreatedAt, ) if err != nil { logrus.Error(err) return nil, err } t.Author = domain.Author{ ID: authorID, } result = append(result, t) } return result, nil } func (m *ArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = ?` list, err := m.fetch(ctx, query, id) if err != nil { return domain.Article{}, err } if len(list) > 0 { res = list[0] } else { return res, domain.ErrNotFound } return }-
作用:負責於數據源(如數據庫、緩存)交互,爲用例層提供統一的接口訪問數據。
-
對應理論層:外部框架與驅動層(
Frameworks & Drivers)。 -
示例:
-
-
Usecase/Service Layer用例 / 服務層package article import ( "context" "time" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" "github.com/bxcodec/go-clean-arch/domain" ) type ArticleRepository interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } type AuthorRepository interface { GetByID(ctx context.Context, id int64) (domain.Author, error) } type Service struct { articleRepo ArticleRepository authorRepo AuthorRepository } func NewService(a ArticleRepository, ar AuthorRepository) *Service { return &Service{ articleRepo: a, authorRepo: ar, } } func (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { res, err = a.articleRepo.GetByID(ctx, id) if err != nil { return } resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID) if err != nil { return domain.Article{}, err } res.Author = resAuthor return }-
作用:定義系統的核心應用邏輯,是領域模型和外部交互之間的橋樑。
-
對應理論層:用例層(
Use Cases/Service)。 -
示例:
-
-
Delivery Layer交付層package rest import ( "context" "net/http" "strconv" "github.com/bxcodec/go-clean-arch/domain" ) type ResponseError struct { Message string `json:"message"` } type ArticleService interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } // ArticleHandler represent the httphandler for article type ArticleHandler struct { Service ArticleService } func NewArticleHandler(e *echo.Echo, svc ArticleService) { handler := &ArticleHandler{ Service: svc, } e.GET("/articles/:id", handler.GetByID) } func (a *ArticleHandler) GetByID(c echo.Context) error { idP, err := strconv.Atoi(c.Param("id")) if err != nil { return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error()) } id := int64(idP) ctx := c.Request().Context() art, err := a.Service.GetByID(ctx, id) if err != nil { return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()}) } return c.JSON(http.StatusOK, art) }-
作用:負責接收外部請求,調用用例層,並將結果返回給外部(如
HTTP客戶端或CLI用戶)。 -
對應理論層:接口適配器層(
Interface Adapters)。 -
示例:
-
go-clean-arch 項目大體的代碼架構結構如下:
go-clean-arch/
├── internal/
│ ├── rest/
│ │ └── article.go # Delivery Layer 交付層
│ ├── repository/
│ │ ├── mysql/
│ │ │ └── article.go # Repository Layer 存儲層
├── article/
│ └── service.go # Usecase/Service Layer 用例/服務層
├── domain/
│ └── article.go # Models Layer 模型層
在go-clean-arch 項目中,各層之間的依賴關係如下:
-
Usecase/Service層依賴Repository接口,但並不知道接口的實現細節。 -
Repository層實現了接口,但它是一個外層組件,依賴於Domain層的實體。 -
Delivery層(如REST Handler)調用Usecase/Service層,負責將外部請求轉化爲業務邏輯調用。
這種設計遵循了依賴倒置原則,確保核心業務邏輯獨立於外部實現細節,具有更高的可測試性和靈活性。
小結
本文結合Bob 大叔的整潔架構(Clean Architecture) 和go-clean-arch 示例項目,介紹瞭如何在Go 項目中實現整潔架構。通過核心實體、用例、接口適配器和外部框架等分層結構,清晰地分離關注點,使系統的核心業務邏輯(Use Cases)與外部實現細節(如框架、數據庫)解耦。
go-clean-arch 項目架構採用分層方式組織代碼,各層職責分明:
-
模型層(Domain Layer):定義核心業務實體,獨立於外部實現。
-
用例層(Usecase Layer):實現應用邏輯,協調實體與外部交互。
-
存儲層(Repository Layer):實現數據存儲的具體細節。
-
交付層(Delivery Layer):處理外部請求並將結果返回。
這只是一個示例項目,具體項目的架構設計應根據實際需求、團隊開發習慣以及規範靈活調整。核心目標是保持分層原則,確保代碼易於理解、測試和維護,同時支持系統的長期擴展和演進。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4iaGEZ8Kl7ZwMG6fkaq5yg