陈明勇一名热爱技术、乐于分享的开发者,同时也是开源爱好者。
93文章
12分类
22标签
19评论
162点赞
100204浏览量
1
3
从理论到实践:Go 项目中的整洁架构设计
陈明勇
2024-12-08 13:47:41
阅读 329

扫码关注公众号,手机阅读更方便

Go技术干货

前言

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

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

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

整洁架构

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

Clean Architecture 的核心思想是 独立性

  • 独立于框架:不依赖特定的框架(如 GinGRPC 等)。框架应该是工具,而不是架构的核心。
  • 独立于 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)。
    • 示例:
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 存储层
    • 作用:负责于数据源(如数据库、缓存)交互,为用例层提供统一的接口访问数据。
    • 对应理论层:外部框架与驱动层(Frameworks & Drivers)。
    • 示例:
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 用例/服务层
    • 作用:定义系统的核心应用逻辑,是领域模型和外部交互之间的桥梁。
    • 对应理论层:用例层(Use Cases / Service)。
    • 示例:
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 交付层
    • 作用:负责接收外部请求,调用用例层,并将结果返回给外部(如 HTTP 客户端或 CLI 用户)。
    • 对应理论层:接口适配器层(Interface Adapters)。
    • 示例:
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 项目中,各层之间的依赖关系如下:

  • 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):处理外部请求并将结果返回。

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

1
评论
个人信息
清空
预览
提交
浩瀚星河 发表于 2025-01-05 13:19:53
回复

看起来不错,之前用学go的时候,发现市面上并没有很多关于golang代码目录结构方面的文章,今天有幸看到,然后我之前拉取了gin-vue-admin项目下来学习它的代码风格,现在看来我感觉也算是遵循了go-arch的代码风格,从外往里调用的,不过它没有repo层。不过这个go-clean-arch,我不是很明白把rest和repo还有worker这几个文件夹都放在internal包下面,求大佬解惑

陈明勇[作者]回复浩瀚星河
发表于 2025-01-05 15:31:19
回复

不用太纠结什么文件夹放里面什么文件夹不放里面,记住 internal 的作用:不让里面的模块被外部访问,你不想让外部访问的模块都可以放里面。其实 web 项目引入 internal 包的意义不大。

浩瀚星河回复陈明勇[作者]
发表于 2025-01-05 16:32:56
回复

感谢😀

不用太纠结什么文件夹放里面什么文件夹不放里面,记住 `internal` 的作用:不让里面的模块被外部访问,你不想让外部访问的模块都可以放里面。其实 `web` 项目引入 `internal` 包的意义不大。
陈明勇一名热爱技术、乐于分享的开发者,同时也是开源爱好者。
93文章
12分类
22标签
19评论
162点赞
100204浏览量