前言
你维护的 Go 项目代码架构是什么样子的?六边形架构?还是洋葱架构?亦或者是 DDD?无论项目采用的是什么架构,核心目标都应是一致的:使代码能够易于理解、测试和维护。
本文将从 Bob 大叔的整洁架构(Clean Architecture)出发,简要解析其核心思想,并结合 go-clean-arch 仓库,深入探讨如何在 Go 项目中实现这一架构理念。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
整洁架构
整洁架构(Clean Architecture)是 Bob 大叔提出的一个软件架构设计理念,旨在通过分层结构和明确的依赖规则,使软件系统更易于理解、测试和维护。其核心思想是分离关注点,确保系统中的核心业务逻辑(Use Cases)不依赖于实现细节(如框架、数据库等)。
Clean Architecture 的核心思想是 独立性:
结构图
如图所示,Clean Architecture 以 同心圆 的方式描述,其中的每一层表示不同的系统职责:
用例(Use Cases / Service)接口适配器(Interface Adapters)外部框架与驱动(Frameworks & Drivers)go-clean-arch 项目
go-clean-arch 是实现整洁架构(Clean Architecture)的一个 Go 示例项目。该项目有四个领域层(Domain 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"`
}
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
}
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
}
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)
}
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 项目中,各层之间的依赖关系如下:
这种设计遵循了依赖倒置原则,确保核心业务逻辑独立于外部实现细节,具有更高的可测试性和灵活性。
小结
本文结合 Bob 大叔的 整洁架构(Clean Architecture) 和 go-clean-arch 示例项目,介绍了如何在 Go 项目中实现整洁架构。通过核心实体、用例、接口适配器和外部框架等分层结构,清晰地分离关注点,使系统的核心业务逻辑(Use Cases)与外部实现细节(如框架、数据库)解耦。
go-clean-arch 项目架构采用分层方式组织代码,各层职责分明:
这只是一个示例项目,具体项目的架构设计应根据实际需求、团队开发习惯以及规范灵活调整。核心目标是保持分层原则,确保代码易于理解、测试和维护,同时支持系统的长期扩展和演进。