大数跨境

Go 中的依赖注入:让代码更易于测试的简单方法

Go 中的依赖注入:让代码更易于测试的简单方法 索引目录
2025-07-23
1
导读:关注【索引目录】服务号,更多精彩内容等你来探索!在本文中,将解释什么是依赖注入,以及它如何解决 Go 语言构建应用程序时的一个关键问题。

关注【索引目录】服务号,更多精彩内容等你来探索!

在本文中,将解释什么是依赖注入,以及它如何解决 Go 语言构建应用程序时的一个关键问题。为了更具体一点,在使用测试驱动开发 (TDD) 构建跟踪器应用程序后端时遇到的一个问题。

当开始编写身份验证服务,特别是像GetUserByIDGetUserByEmail这样的函数时,问题出现了。为了测试服务逻辑,使用模拟数据库和模拟记录器编写了一些初始测试。然而,犯了一个常见的错误:将 UserService 与这些模拟函数紧密耦合。

这意味着,要将应用程序迁移到生产环境,必须返回并手动修改 UserService 及其所有功能,使其使用真实的数据库和记录器。这个过程既累人,又容易出错,而且一点也不好玩。解决方案是一种名为“依赖注入”(DI)的模式。

问题:紧密耦合的代码

让我们看看这种紧耦合的代码是什么样子的。想象一下一个UserService需要与数据库通信并记录消息。

如果没有 DI,您可能会像这样编写,其中服务创建自己的依赖项。

package main

import (
    "fmt"
    "log"
)

// A hard-coded database connection (for demonstration)
type PostgresDB struct {
    // connection fields would go here
}

func (db *PostgresDB) GetUser(id int) (string, error) {
    // In a real app, this would query the database
    return fmt.Sprintf("user_%d", id), nil
}

// Our service that needs a database
type UserService struct {
    db *PostgresDB // A direct dependency on a concrete type
}

// The constructor creates its own dependency
func NewUserService() *UserService {
    // The problem is right here!
    // We are creating the dependency inside the service.
    // This makes it impossible to replace `PostgresDB` for tests or other environments.
    return &UserService{
        db: &PostgresDB{},
    }
}

func (s *UserService) GetUser(id int) (string, error) {
    log.Printf("Getting user %d", id)
    return s.db.GetUser(id)
}

func main() {
    userService := NewUserService()
    user, err := userService.GetUser(101)
    if err != nil {
        log.Fatalf("Failed to get user: %v", err)
    }
    fmt.Println(user)
}

这种方法的问题很明显:

难以测试:如果没有运行 PostgreSQL 数据库,如何对 UserService 进行单元测试?你无法轻松地用 mock替换PostgresDB 。

不灵活:如果要切换到 MySQL 或使用其他日志库怎么办?您必须直接修改 UserService 代码。

解决方案:引入依赖注入

依赖注入意味着我们将“注入”(传入)服务所需的依赖项。无需服务创建其依赖项,而是我们将其传递给服务。

步骤 1:定义接口(“契约”)
首先,我们定义接口来描述服务需要什么,而不是如何实现。这就是契约。

// UserStorer is an interface that defines the operations we need for user data.
type UserStorer interface {
    GetUser(id int) (string, error)
}

// Logger is an interface for logging messages.
type Logger interface {
    Log(message string)
}

步骤 2:依赖接口,而不是具体类型

接下来我们重构UserService来依赖这些接口。

// UserService now depends on the interfaces, not the concrete structs.
type UserService struct {
    store  UserStorer
    logger Logger
}

// The dependencies are "injected" as arguments to the constructor.
func NewUserService(s UserStorer, l Logger) *UserService {
    return &UserService{
        store:  s,
        logger: l,
    }
}

func (s *UserService) GetUser(id int) (string, error) {
    s.logger.Log(fmt.Sprintf("Getting user %d", id))
    return s.store.GetUser(id)
}

我们的 UserService 现在与任何特定的数据库或记录器实现完全分离!

在 main() 函数中将所有内容连接在一起。
主函数(或初始化应用程序的任何位置)将成为“组合根”。在这里,您可以选择并创建具体的实现,并将其注入到您的服务中。

package main

import (
    "fmt"
    "log"
)

/*
--- Interfaces (Contracts) ---
*/
type UserStorer interface {
    GetUser(id int) (string, error)
}
type Logger interface {
    Log(message string)
}

/*
--- Production (Concrete) Implementations ---
*/
// PostgresDB is our real database implementation.
type PostgresDB struct { /* ... */ }

func (db *PostgresDB) GetUser(id int) (string, error) {
    return fmt.Sprintf("user_%d from postgres", id), nil
}
func NewPostgresDB() *PostgresDB { return &PostgresDB{} }


// StandardLogger is our real logger implementation.
type StandardLogger struct{}

func (l *StandardLogger) Log(message string) {
    log.Println(message)
}
func NewStandardLogger() *StandardLogger { return &StandardLogger{} }


/*
--- Service ---
*/
// UserService depends only on interfaces.
type UserService struct {
    store  UserStorer
    logger Logger
}

func NewUserService(s UserStorer, l Logger) *UserService {
    return &UserService{store: s, logger: l}
}

func (s *UserService) GetUser(id int) (string, error) {
    s.logger.Log(fmt.Sprintf("Getting user %d", id))
    return s.store.GetUser(id)
}

/*
--- Main (Composition Root) ---
*/
func main() {
    // 1. Create our concrete dependencies for PRODUCTION.
    db := NewPostgresDB()
    logger := NewStandardLogger()

    // 2. Inject them into our service.
    userService := NewUserService(db, logger)

    // 3. Use the service.
    user, err := userService.GetUser(101)
    if err != nil {
        log.Fatalf("Failed to get user: %v", err)
    }
    fmt.Println(user) // Output: user_101 from postgres
}

回报:简单有效的测试✅

现在到了最精彩的部分。测试 UserService 逻辑非常简单。我们可以创建接口的模拟实现,使其行为完全符合我们的测试用例要求。

测试文件(main_test.go)可能如下所示:

package main

import (
    "testing"
    "fmt"
)

// MockUserStore is a mock implementation of UserStorer for testing.
type MockUserStore struct {
    users map[int]string
}

func (m *MockUserStore) GetUser(id int) (string, error) {
    user, ok := m.users[id]
    if !ok {
        return "", fmt.Errorf("user not found")
    }
    return user, nil
}

// MockLogger does nothing, perfect for silent testing.
type MockLogger struct {}
func (m *MockLogger) Log(message string) { /* do nothing */ }


// Test the UserService in isolation.
func TestUserService_GetUser(t *testing.T) {
    // 1. Setup our MOCK dependencies.
    mockStore := &MockUserStore{
        users: map[int]string{
            1: "Alice",
        },
    }
    mockLogger := &MockLogger{}

    // 2. Inject the mocks into our service.
    userService := NewUserService(mockStore, mockLogger)

    // 3. Run the test.
    user, err := userService.GetUser(1)
    if err != nil {
        t.Errorf("Expected no error, got %v", err)
    }

    if user != "Alice" {
        t.Errorf("Expected user 'Alice', got '%s'", user)
    }
}

我们刚刚测试了UserService逻辑,无需数据库或写入标准输出。它简洁、独立且快速。

结论:DI 是 Go 的核心模式

依赖注入并非添加一个复杂的框架。它是一种简单的模式,从外部赋予对象依赖项,而不是让对象在内部创建依赖项。

通过依赖接口并注入它们,您可以获得:

可测试性:轻松将真实实现替换为模拟实现。

灵活性:无需触及核心服务逻辑即可更改数据库或记录器的实现。

清晰度:NewUserService 的函数签名清楚地说明了其依赖关系。

对于大多数 Go 项目来说,这种手动的 DI 方法就足够了。它符合 Go 语言的语法,易于理解,而且非常有效。

关注【索引目录】服务号,更多精彩内容等你来探索!


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读1.3k
粉丝0
内容444