首页  >  文章  >  后端开发  >  为什么清洁架构在 Golang 中举步维艰以及什么更有效

为什么清洁架构在 Golang 中举步维艰以及什么更有效

Mary-Kate Olsen
Mary-Kate Olsen原创
2024-11-07 01:59:02493浏览

Why Clean Architecture Struggles in Golang and What Works Better

Golang 作为一种优先考虑简单性的快速、高效语言,赢得了良好的声誉,这也是它如此普遍用于后端服务、微服务和基础设施工具的原因之一。然而,随着越来越多的开发人员从 Java 和 C# 等语言过渡到 Go,出现了关于实施清洁架构的问题。对于那些习惯使用 Clean Architecture 基于层的方法构建应用程序的人来说,将相同的原则应用于 Go 会感觉很直观。然而,正如我们将要探讨的那样,尝试在 Go 中实现简洁架构通常会适得其反。相反,我们将研究一种针对 Go 优势量身定制的结构,该结构更直接、灵活,并且符合 Go 的“保持简单”理念。

为什么干净的架构在 Go 中感觉格格不入

由鲍勃叔叔(罗伯特·C·马丁)倡导的清洁架构的目标是创建模块化、可测试且易于扩展的软件。这是通过强制各层之间的关注点分离来实现的,同时核心业务逻辑与外部关注点保持隔离。虽然这在 Java 等高度面向对象的语言中效果很好,但在 Go 中却引入了摩擦。原因如下:

1. Go 的极简主义与过度抽象作斗争

在 Go 中,非常强调可读性、简单性和减少开销。 Clean Architecture 引入了一层又一层的抽象:接口、依赖倒置、复杂的依赖注入和业务逻辑的服务层。然而,这些额外的层在 Go 中实现时往往会增加不必要的复杂性。

我们以 Kubernetes 为例。 Kubernetes 是一个用 Go 构建的大型项目,但它不依赖于清洁架构原则。相反,它采用扁平的、面向功能的结构,重点围绕包和子系统。您可以在 Kubernetes GitHub 存储库中看到这一点,其中包是按功能而不是严格的层组织的。通过根据功能对代码进行分组,Kubernetes 无需复杂的抽象即可实现高度模块化。

Go 哲学优先考虑实用性和速度。该语言的创建者一直主张避免过度架构,倾向于简单的实现。如果抽象不是绝对必要的,那么它就不属于 Go 代码。 Go 的创建者甚至设计了没有继承的语言,以避免过度设计的陷阱,鼓励开发人员保持设计干净清晰。

2. 依赖注入受到设计的限制

清洁架构严重依赖依赖注入来解耦不同的层并使模块更易于测试。在 Java 等语言中,借助 Spring 等框架,DI 自然成为生态系统的一部分。这些框架自动处理 DI,让您可以轻松地将依赖项连接在一起,而不会使您的代码变得混乱。

然而,Go 缺乏原生 DI 系统,并且大多数 Go 的 DI 库要么过于复杂,要么感觉不惯用。 Go 依赖于通过构造函数或函数参数进行显式依赖注入,保持依赖关系清晰并避免“魔法”隐藏在 DI 容器中。 Go 的方法使代码更加明确,但这也意味着如果引入太多层,依赖关系管理就会变得难以管理且冗长。

例如,在 Kubernetes 中,您看不到复杂的 DI 框架或 DI 容器。相反,依赖项是使用构造函数以简单的方式注入的。这种设计保持了代码的透明性,避免了 DI 框架的缺陷。 Golang 鼓励仅在真正有意义的地方使用 DI,这就是为什么 Kubernetes 避免仅仅为了遵循模式而创建不必要的接口和依赖项。

3. 层数过多,测试变得更加复杂

Go 中干净架构的另一个挑战是它会使测试变得不必要的复杂。例如,在 Java 中,清洁架构支持健壮的单元测试,并大量使用依赖项的模拟。模拟允许您隔离每一层并独立测试它。然而,在 Go 中,创建模拟可能很麻烦,并且 Go 社区通常倾向于集成测试或尽可能使用实际实现进行测试。

在生产级 Go 项目中,例如 Kubernetes,测试不是通过隔离每个组件来处理的,而是通过专注于涵盖现实场景的集成和端到端测试来进行。通过减少抽象层,像 Kubernetes 这样的 Go 项目可以实现较高的测试覆盖率,同时保持测试接近实际行为,从而在生产中部署时更有信心。

Golang 的最佳架构方法

那么,如果 Clean Architecture 不太适合 Go,那么什么才适合呢?答案在于更简单、更实用的结构,强调包并注重模块化而不是严格的分层。 Go 的一种有效架构模式是基于六角形架构,通常称为端口和适配器。这种架构允许模块化和灵活性,而无需过多的分层。

Golang 标准项目布局是在 Go 中创建生产就绪项目的一个很好的起点。这种结构为按目的和功能而不是按架构层组织代码提供了基础。

Go 项目结构:一个实际示例

你说得完全正确!使用以包为中心的方法构建 Go 项目,其中功能按包而不是分层文件夹结构进行分解,更符合 Go 的设计原则。在 Go 中,创建内聚的包更惯用,每个包都封装自己的模型、服务和存储库,而不是逐层创建顶级目录(例如控制器、服务、存储库)。这种基于包的方法减少了耦合并保持代码模块化,这对于生产级 Go 应用程序至关重要。

让我们看一下适合 Go 的精致的、以包为中心的结构:

/myapp
   /cmd                   // Entrypoints for different executables (e.g., main.go)
      /myapp-api
         main.go          // Entrypoint for the main application
   /config                // Configuration files and setup
   /internal              // Private/internal packages (not accessible externally)
      /user               // Package focused on user-related functionality
         models.go        // Data models and structs specific to user functionality
         service.go       // Core business logic for user operations
         repository.go    // Database access methods for user data
      /order              // Package for order-related logic
         models.go        // Data models for orders
         service.go       // Core order-related logic
         repository.go    // Database access for orders
   /pkg                   // Shared, reusable packages across the application
      /auth               // Authorization and authentication package
      /logger             // Custom logging utilities
   /api                   // Package with REST or gRPC handlers
      /v1
         user_handler.go  // Handler for user-related endpoints
         order_handler.go // Handler for order-related endpoints
   /utils                 // General-purpose utility functions and helpers
   go.mod                 // Module file

基于封装的结构中的关键组件

  1. /cmd

此文件夹是应用程序入口点的常规位置。这里的每个子文件夹代表应用程序的不同可执行文件。例如,在微服务架构中,每个服务都可以拥有自己的目录及其 main.go。这里的代码应该是最少的,只负责引导和设置依赖项。

  1. /配置

存储配置文件和设置逻辑,例如加载环境变量或外部配置。该包还可以定义应用程序配置的结构。

  1. /内部

这是应用程序的核心逻辑所在,根据功能分为包。 Go 限制从外部模块访问内部包,使这些包对应用程序保持私有。每个包(例如用户、订单)都是独立的,具有自己的模型、服务和存储库。这是 Go 的封装哲学、无需过多分层的关键。

  • /internal/user – 管理所有与用户相关的功能,包括模型(数据结构)、服务(业务逻辑)和存储库(数据库交互)。这将用户相关的逻辑保留在一个包中,使其易于维护。

  • /internal/order – 同样,这个包封装了订单相关的代码。每个功能区域都有自己的模型、服务和存储库。

  1. /pkg

pkg 包含可在应用程序中使用的可重用组件,但不特定于任何一个包。可以独立使用的库或实用程序,例如用于身份验证的 auth 或用于自定义日志记录的 logger,都保存在这里。如果这些包特别有用,以后也可以将它们提取到自己的模块中。

  1. /api

API 包充当 HTTP 或 gRPC 处理程序的层。这里的处理程序处理传入的请求、调用服务并返回响应。按 API 版本(例如 v1)对处理程序进行分组是版本控制的良好实践,有助于隔离未来的更改。

  1. /utils

通用实用程序,不与任何特定包绑定,但在代码库中提供横切用途(例如,日期解析、字符串操作)。保持最小并专注于纯粹的实用功能是有帮助的。

用户包的示例代码布局

为了说明结构,我们仔细看看用户包的样子:

模型.go

/myapp
   /cmd                   // Entrypoints for different executables (e.g., main.go)
      /myapp-api
         main.go          // Entrypoint for the main application
   /config                // Configuration files and setup
   /internal              // Private/internal packages (not accessible externally)
      /user               // Package focused on user-related functionality
         models.go        // Data models and structs specific to user functionality
         service.go       // Core business logic for user operations
         repository.go    // Database access methods for user data
      /order              // Package for order-related logic
         models.go        // Data models for orders
         service.go       // Core order-related logic
         repository.go    // Database access for orders
   /pkg                   // Shared, reusable packages across the application
      /auth               // Authorization and authentication package
      /logger             // Custom logging utilities
   /api                   // Package with REST or gRPC handlers
      /v1
         user_handler.go  // Handler for user-related endpoints
         order_handler.go // Handler for order-related endpoints
   /utils                 // General-purpose utility functions and helpers
   go.mod                 // Module file

服务.go

// models.go - Defines the data structures related to users

package user

type User struct {
    ID       int
    Name     string
    Email    string
    Password string
}

存储库.go

// service.go - Contains the core business logic for user operations

package user

type UserService struct {
    repo UserRepository
}

// NewUserService creates a new instance of UserService
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) RegisterUser(name, email, password string) error {
    // Business logic for registering a user
    newUser := User{Name: name, Email: email, Password: password}
    return s.repo.Save(newUser)
}

为什么这种基于包的结构非常适合 Go

这个结构与 Go 的习惯用法非常吻合:

  1. 封装

通过基于功能来组织包,代码自然地被封装和模块化。每个包都拥有自己的模型、服务和存储库,从而保持代码的内聚性和高度模块化。这使得导航、理解和测试各个包变得更加容易。

  1. 最小接口

接口仅在包边界(例如 UserRepository)使用,它们对于测试和灵活性最有意义。这种方法减少了不必要的接口的混乱,这会使 Go 代码更难维护。

  1. 显式依赖注入

依赖项是通过构造函数(例如 NewUserService)注入的。这使依赖关系保持明确,并避免了对复杂依赖注入框架的需要,忠于 Go 的简单性设计。

  1. /pkg 中的可重用性

pkg 目录中的 auth 和 logger 等组件可以跨包共享,从而提高可重用性,而无需过度耦合。

  1. 清晰的API结构

通过将处理程序分组到 /api 下,可以轻松扩展 API 层并随着应用程序的增长添加新版本或处理程序。每个处理程序都可以专注于处理请求并与服务协调,保持代码模块化和整洁。

这种以包为中心的结构使您可以在添加更多域(例如产品、库存)时进行扩展,每个域都有自己的模型、服务和存储库。按领域的分离符合 Go 组织代码的惯用方式,在严格的分层上保持简单性和清晰度。

意见和现实世界的经验

根据我使用 Go 的经验,干净的架构通常会使代码库变得复杂,而不会增加显着的价值。当使用 Java 等语言构建大型企业级应用程序时,清洁架构往往很有意义,因为 Java 中有很多对 DI 的内置支持,并且管理深层继承结构是一种常见需求。然而,Go 的极简主义、简单优先的心态以及简单的并发和错误处理方法完全创建了一个不同的生态系统。

结论:拥抱 Go 的惯用架构

如果您有 Java 背景,那么将干净架构应用于 Go 可能很诱人。然而,Go 的优势在于简单、透明和模块化,没有过多的抽象。 Go 的理想架构优先考虑按功能、最小接口、显式 DI、实际测试和灵活性适配器组织的包。

设计 Go 项目时,请参考 Kubernetes、Vault 和 Golang 标准项目布局等现实示例。这些展示了当架构拥抱简单而不是严格的结构时,Go 的强大之处。与其试图让 Go 适应干净的架构模式,不如拥抱像 Go 本身一样简单和高效的架构。通过这种方式,您构建的代码库不仅是惯用的,而且更易于理解、维护和扩展。

以上是为什么清洁架构在 Golang 中举步维艰以及什么更有效的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn