go 依赖注入实践

简介: 依赖注入(DI)是一种软件设计模式,旨在降低代码耦合度,提高代码可测试性和可复用性。其核心思想是将依赖项从外部传入使用对象,而非由其内部创建。通过 DI,模块间关系更清晰,便于维护和扩展。常见实现包括方法注入和接口注入,适用于如 Go 等支持函数式编程和接口抽象的语言。

什么是依赖注入

在了解依赖注入之前,我们可以先分析下什么是依赖。

依赖

依赖的日常解释是依靠别人或事物而不能自利或自给,在软件开发中,依赖则表示的是函数,对象,模块之间的引用关系,比如函数调用,对象引用。

类似日常解释中,缺失依赖,人或事物不能自给或自利,同样,软件系统中缺失了依赖,也无法正常运行或发生正常的运行行为。

与日常依赖不同的是,软件系统的依赖,倡导的是单向依赖关系,也即A引用B,则不建议B再引用A,go语言中则直接通过不支持循环引用这一特点,迫使达到这一目标。一般有如下依赖关系:

  • 方法依赖

go

体验AI代码助手

代码解读

复制代码

 func A() {
     ...
  } 
     
 //B方法的依赖于A方法,才能完整运行 
 func B() { 
   ... 
   A() 
 }
  • 对象依赖

go

体验AI代码助手

代码解读

复制代码

type A struct{
}

// B结构依赖A结构,组成了一个完整体
type B struct {
    a A 
}

依赖关系不限于此,由此可以看出系统通过依赖运行,但因为依赖的存在,增强了模块或系统之间耦合。

降低耦合度是系统设计或代码设计中永恒的主题。

依赖注入

注入的意思,可以理解为从外部输入,而非内部产生。软件系统中,可以理解为依赖项不是由使用方(方法/对象)生成,而是从外部传入的一个过程。

依赖注入(Dependency injection, 缩写DI) 是一种软件设计模式,设计的目的是为了分离关注点,分离接收方和依赖,降低耦合度以及提升代码的重用性。优点如下:

  • 有助于代码测试
  • 提高代码的可复用性
  • 降低耦合度,有助于代码的维护

依赖注入不是编码的目标,它是为了达到松耦合的一种手段。

go实现的步骤

步骤

  1. 抽象,定义接口/类型(对于方法而言)
  2. 建立接口/方法具体实现
  3. 设计传入依赖项的方法
  4. 使用注入的依赖

方法依赖注入

假设我们要实现一个消息通知服务,可能会支持不同的发送方式,我们仅通过方法进行。

  • 常规写法

go

体验AI代码助手

代码解读

复制代码


func main() {
	sendMsg("DI Test!", "sms")
}

// 使用方,根据需要根据channel值选择依赖方法,后续有新的方法需要改动,且不好测试。
func sendMsg(msg string, channel string) error {

	if channel == "email" {
		return sendByEmail(msg)
	}

	if channel == "sms" {
		return sendByEmail(msg)
	}

	return errors.New("Invalid Channel")

}

func sendByEmail(msg string) error {
	fmt.Printf("send msg by Email, msg:%s\n", msg)
	return nil
}

func sendBySms(msg string) error {
	fmt.Printf("send msg by SMS, msg:%s\n", msg)
	return nil
}

上述代码中,假设我们实现一种新的发送方式, 除了需要新增一个sendByXX方法之外,还需要修改sendMsg方法,以适配新的sendByXX方法,sendMsg与发送方式耦合过紧,得益于go中函数一等公民特性,我们可以通过定义函数类型,进行改造, 如下。

  • 依赖注入写法

go

体验AI代码助手

代码解读

复制代码

// 定义方法类型
type SendFunc func(msg string) error

func main() {
	sendMsg("DI Test!", sendBySms)
}

// 允许注入依赖方法
func sendMsg(msg string, sf SendFunc) error {
	return sf(msg)
}

func sendByEmail(msg string) error {
	fmt.Printf("send msg by Email, msg:%s\n", msg)
	return nil
}

func sendBySms(msg string) error {
	fmt.Printf("send msg by SMS, msg:%s\n", msg)
	return nil
}

改造后,抽象了SendFunc的类型, 新增一个sendByXX方法时,只需在调用处传入此方法即可。sendMsg中无需关注sendByXX方法的存在。测试时,只需要实现一个sendByMock方法,传入即可。

接口依赖注入

同样还是实现消息通知,采取面向对象编程方式;

  • 常规写法

go

体验AI代码助手

代码解读

复制代码

type Notify struct {
	channel string
}

func (n *Notify) Send(msg string) error {
	channel := n.channel
        // 依赖channel值
	if channel == "email" {
		e := new(Email)
		return e.Send(msg)
	}
	if channel == "sms" {
		sms := new(SMS)
		return sms.Send(msg)
	}
	return errors.New("Invalid Channel:" + channel)
}

func NewNotify(channel string) *Notify {
	return &Notify{
		channel,
	}
}

type Email struct {
}

func (e *Email) Send(msg string) error {
	fmt.Printf("Email Send, msg: %s \n", msg)
	return nil
}

type SMS struct {
}

func (sms *SMS) Send(msg string) error {
	fmt.Printf("SMS Send, msg: %s \n", msg)
	return nil
}

func main() {
	notify := NewNotify("email")
	notify.Send("Hello DI")
}

上述写法中,notify在调用Send方法时,需要感知发送方式,并创建对应的对象。如果新增一种通知类型,Notify的Send方法也需要修改,适配新增的类型。

  • 依赖注入

go

体验AI代码助手

代码解读

复制代码

// 通过接口抽象发送行为
type Sender interface {
	Send(string) error
}

type Notify struct {
	sender Sender
}

// 传入依赖项方法设计
func NewNotify(sender Sender) *Notify {
	return &Notify{
		sender,
	}
}

// 使用注入的依赖
func (notify *Notify) Send(msg string) error {
	return notify.sender.Send(msg)
}

type Email struct {
}

func (e *Email) Send(msg string) error {
	fmt.Printf("Email Send, msg: %s \n", msg)
	return nil
}

type SMS struct {
}

func (sms *SMS) Send(msg string) error {
	fmt.Printf("SMS Send, msg: %s \n", msg)
	return nil
}

func main() {
    // 发送msg的依赖,由调入方传入,后续如果新增一种发送方式,sendMsg方法无需修改
    notify := NewNotify(&Email{})
    notify.Send("Hello DI")
}

改造后,我们抽象了Sender接口,新增一种发送方式时,只需要实现Sender接口,并在NewNotify时,传入即可。Notify不需要感知新增发送方式的存在,解耦了Notify与新增的发送方式。

总结

依赖注入(DI)是一种降低代码耦合度的实现方式,具有比较实用的模板或实现步骤,我们在运用的过程,只需要把握依赖尽量不要由接收者生成,而是通过外部注入这一原则,就能比较容易实现依赖注入,写出松耦合的代码。


转载来源:https://juejin.cn/post/7408481531361886244

相关文章
|
2月前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
275 86
|
4月前
|
分布式计算 算法 安全
Go语言泛型-泛型约束与实践
Go语言中的泛型约束用于限制类型参数的范围,提升类型安全性。通过接口定义约束,可实现对数值类型、排序与比较等操作的支持。开发者既可使用标准库提供的预定义约束,如constraints.Ordered和constraints.Comparable,也可自定义约束以满足特定需求。泛型广泛应用于通用数据结构(如栈、队列)、算法实现(如排序、查找)及构建高效可复用的工具库,使代码更简洁灵活。
|
5月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:路由、中间件、参数校验
Gin框架以其极简风格、强大路由管理、灵活中间件机制及参数绑定校验系统著称。本文详解其核心功能:1) 路由管理,支持分组与路径参数;2) 中间件机制,实现全局与局部控制;3) 参数绑定,涵盖多种来源;4) 结构体绑定与字段校验,确保数据合法性;5) 自定义校验器扩展功能;6) 统一错误处理提升用户体验。Gin以清晰模块化、流程可控及自动化校验等优势,成为开发者的优选工具。
|
5月前
|
开发框架 安全 前端开发
Go Web开发框架实践:模板渲染与静态资源服务
Gin 是一个功能强大的 Go Web 框架,不仅适用于构建 API 服务,还支持 HTML 模板渲染和静态资源托管。它可以帮助开发者快速搭建中小型网站,并提供灵活的模板语法、自定义函数、静态文件映射等功能,同时兼容 Go 的 html/template 引擎,具备高效且安全的页面渲染能力。
|
5月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
6月前
|
设计模式 缓存 算法
Go如何进行高质量编程与性能调优实践
本文介绍了Go语言高质量编程与性能调优的实践方法。高质量编程包括良好的编码习惯(如清晰注释、命名规范)、代码风格与设计(如MVC模式)、简洁明了的代码原则,以及单元测试与代码重构的重要性。性能调优方面,涵盖算法优化、数据结构选择、I/O优化、内存管理、并行与并发处理优化及代码层面的改进。通过这些方法,可有效提升代码质量和系统性能。
145 13
|
4月前
|
Linux Go 开发者
Go语言泛型-泛型约束与实践
《Go语言实战指南》介绍了如何使用Go进行交叉编译,即在一个操作系统上编译出适用于不同系统和架构的二进制文件。通过设置GOOS和GOARCH环境变量,开发者可轻松构建跨平台程序,无需在每个平台上单独编译。Go从1.5版本起原生支持此功能,极大提升了多平台部署效率。
|
Go 调度 开发者
Go语言中的并发编程:深入理解与实践###
探索Go语言在并发编程中的独特优势,揭秘其高效实现的底层机制。本文通过实例和分析,引导读者从基础到进阶,掌握Goroutines、Channels等核心概念,提升并发处理能力。 ###
|
12月前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####
下一篇
oss云网关配置