文章:开发实践|关于100以内的加减乘除法问题之我在客户现场遇到的bug 评语:这篇文章主要讲述了作者在客户现场解决一个数据分析问题的过程。客户发现饼图的百分比总和不等于100%,这是由于数据精度丢失和四舍五入导致的。作者通过分析数据源、对比不同的处理方式,最终采用了一种补偿算法:在最后一个百分比计算中,通过减去前面所有百分比的和来确保总和为100%。作者总结了饼图数据展示中可能遇到的精度问题,提出了排查数据、优化工具配置以及与客户协商折中方案的重要性,并表达了在解决问题中学习与客户沟通的体会。
你维护的 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
是实现整洁架构(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
项目架构采用分层方式组织代码,各层职责分明:
这只是一个示例项目,具体项目的架构设计应根据实际需求、团队开发习惯以及规范灵活调整。核心目标是保持分层原则,确保代码易于理解、测试和维护,同时支持系统的长期扩展和演进。
你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。
成功的路上并不拥挤,有没有兴趣结个伴?
关注我,加我好友,一起学习一起进步!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。