前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >从理论到实践:Go 项目中的整洁架构设计

从理论到实践:Go 项目中的整洁架构设计

原创
作者头像
陈明勇
发布2024-12-06 23:04:55
发布2024-12-06 23:04:55
3127
举报

文章:开发实践|关于100以内的加减乘除法问题之我在客户现场遇到的bug 评语:这篇文章主要讲述了作者在客户现场解决一个数据分析问题的过程。客户发现饼图的百分比总和不等于100%,这是由于数据精度丢失和四舍五入导致的。作者通过分析数据源、对比不同的处理方式,最终采用了一种补偿算法:在最后一个百分比计算中,通过减去前面所有百分比的和来确保总和为100%。作者总结了饼图数据展示中可能遇到的精度问题,提出了排查数据、优化工具配置以及与客户协商折中方案的重要性,并表达了在解决问题中学习与客户沟通的体会。

前言

你维护的 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)。
    • 示例:
代码语言:go
复制
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)。
    • 示例:
代码语言:go
复制
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)。
    • 示例:
代码语言:go
复制
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)。
    • 示例:
代码语言:go
复制
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 项目大体的代码架构结构如下:

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

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


你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。

成功的路上并不拥挤,有没有兴趣结个伴?

关注我,加我好友,一起学习一起进步!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 整洁架构
    • 结构图
  • go-clean-arch 项目
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档