前言

你维护的 Go 项目代码架构是什么样子的?六边形架构?还是洋葱架构?亦或者是 DDD?无论项目采用的是什么架构,核心目标都应是一致的:使代码能够易于理解、测试和维护。

本文将从 Bob 大叔的整洁架构(Clean Architecture)出发,简要解析其核心思想,并结合 go-clean-arch 仓库,深入探讨如何在 Go 项目中实现这一架构理念。

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

请在此添加图片描述

整洁架构

整洁架构(Clean Architecture)是 Bob 大叔提出的一个软件架构设计理念,旨在通过分层结构和明确的依赖规则,使软件系统更易于理解、测试和维护。其核心思想是分离关注点,确保系统中的核心业务逻辑(Use Cases)不依赖于实现细节(如框架、数据库等)。

Clean Architecture 的核心思想是 独立性:

结构图

4052df165b48a9a14e3b6f8a381ea5c1.png

如图所示,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 项目架构采用分层方式组织代码,各层职责分明:

这只是一个示例项目,具体项目的架构设计应根据实际需求、团队开发习惯以及规范灵活调整。核心目标是保持分层原则,确保代码易于理解、测试和维护,同时支持系统的长期扩展和演进。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。