前言
你维护的 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
模型层- 作用:定义领域的核心数据结构,负责描述项目中的业务实体,例如 文章 、作者 等。
- 对应理论层:实体层(
Entities
)。 - 示例:
gopackage 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"` }
-
Repository Layer
存储层- 作用:负责于数据源(如数据库、缓存)交互,为用例层提供统一的接口访问数据。
- 对应理论层:外部框架与驱动层(
Frameworks & Drivers
)。 - 示例:
gopackage 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 }
-
Usecase/Service Layer
用例/服务层- 作用:定义系统的核心应用逻辑,是领域模型和外部交互之间的桥梁。
- 对应理论层:用例层(
Use Cases
/Service
)。 - 示例:
gopackage 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 }
-
Delivery Layer
交付层- 作用:负责接收外部请求,调用用例层,并将结果返回给外部(如
HTTP
客户端或CLI
用户)。 - 对应理论层:接口适配器层(
Interface Adapters
)。 - 示例:
gopackage 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) }
- 作用:负责接收外部请求,调用用例层,并将结果返回给外部(如
go-clean-arch
项目大体的代码架构结构如下:
txt
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):处理外部请求并将结果返回。
这只是一个示例项目,具体项目的架构设计应根据实际需求、团队开发习惯以及规范灵活调整。核心目标是保持分层原则,确保代码易于理解、测试和维护,同时支持系统的长期扩展和演进。