Learn Go with Tests 之依赖注入(Dependency Injection):用 io.Writer 让代码可测试、可复用

发布时间:2026/9/20 14:03:21
Learn Go with Tests 之依赖注入(Dependency Injection):用 io.Writer 让代码可测试、可复用 教程示例工程【免费下载链接】learn-go-with-testsLearn Go with test-driven development项目地址https://gitcode.com/gh_mirrors/le/learn-go-with-tests点击查看免费下载本文基于开源仓库 learn-go-with-tests 的 dependency-injection.md 章节展开。该章节是「Go 通过测试驱动开发学习」系列的第 9 篇核心主题是用标准库自带的io.Writer接口完成依赖注入不引入任何框架仅通过「面向接口编程」就解决了「打印函数难以测试」的问题。读完本文你将掌握依赖注入的本质思想、io.Writer接口的底层工作原理以及如何把同一个Greet函数复用到命令行程序与 HTTP 服务两种完全不同的场景中。为什么需要依赖注入从不可测试的 Greet 说起在 hello-world.md 章节中我们已经写过打招呼的函数但它只是返回字符串。现在我们想写一个真正执行「打印」的版本并对实际的打印行为做测试。最直观的写法是这样的func Greet(name string) { fmt.Printf(Hello, %s, name) }问题立刻浮现fmt.Printf把内容打印到 stdout而测试框架很难捕获标准输出。这正是「代码不可测试」的典型症状——数据写到了一个我们无法控制的地方。解决思路是注入inject直白地说就是「传入」打印这一依赖我们的函数不需要关心打印发生在哪里、以何种方式进行所以应该接受一个接口而非具体类型。这样在测试里我们可以注入一个自己能控制的写入目标来验证输出在真实场景里则注入写入 stdout 的实现。顺着 fmt.Printf 的源码找到挂载点io.Writer注入接口听起来抽象但 Go 标准库早就给了我们现成的挂载点。看fmt.Printf的源码// It returns the number of bytes written and any write error encountered. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) }有意思的是Printf内部只是把os.Stdout传给了Fprintf。继续看Fprintf期望的第一个参数类型func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p : newPrinter() p.doPrintf(format, a) n, err w.Write(p.buf) p.free() return }它接收的是一个io.Writertype Writer interface { Write(p []byte) (n int, err error) }由此可以推断os.Stdout实现了io.WriterPrintf只是把os.Stdout交给期望io.Writer的Fprintf。随着你写的 Go 代码越来越多会频繁遇到这个接口——它是一个非常通用、语义为「把数据写到某处」的抽象。既然底层最终是通过Writer把问候语送到某个目的地我们完全可以借用这个现成的抽象让Greet既可测试又更易复用。TDD 第一步先写测试按 TDD 的节奏先写测试。bytes包的Buffer类型实现了Write(p []byte) (n int, err error)方法因此也实现了Writer接口。我们用它在测试中充当注入的Writer调用Greet后检查缓冲区里写了什么func TestGreet(t *testing.T) { buffer : bytes.Buffer{} Greet(buffer, Chris) got : buffer.String() want : Hello, Chris if got ! want { t.Errorf(got %q want %q, got, want) } }这与仓库中的 di/v1/di_test.go 完全一致。运行测试让编译器告诉我们缺什么此时运行测试无法编译./di_test.go:10:2: undefined: Greet倾听编译器的声音写出让测试能运行的最小代码func Greet(writer *bytes.Buffer, name string) { fmt.Printf(Hello, %s, name) }测试运行结果Hello, Chris di_test.go:16: got want Hello, Chris测试失败而且很有信息量名字确实被打印出来了但它去了stdout而不是我们注入的缓冲区——所以缓冲区里什么都拿不到。这正是原始实现的问题被测试机制暴露出来的瞬间。让测试通过改用 fmt.Fprintf把输出改到writer上即可。记住fmt.Fprintf与fmt.Printf的区别前者多接收一个Writer参数把字符串写到该Writer后者默认写到 stdoutfunc Greet(writer *bytes.Buffer, name string) { fmt.Fprintf(writer, Hello, %s, name) }测试通过。重构从具体类型走向通用接口 io.Writer编译器之前告诉我们传入*bytes.Buffer这在技术上正确但不够实用。验证一下把Greet接进一个要打印到 stdout 的 Go 应用中func main() { Greet(os.Stdout, Elodie) }立刻编译失败./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet如前面分析fmt.Fprintf接受io.Writer而os.Stdout*os.File和bytes.Buffer都实现了它。把参数类型放宽到更通用的接口后同一个函数既能用于测试也能用于应用。这正是仓库 di/v1/di.go 中的最终形态package main import ( fmt io os ) // Greet sends a personalised greeting to writer. func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, Hello, %s, name) } func main() { Greet(os.Stdout, Elodie) }运行go run .在 di/v1 目录下即可在终端看到Hello, Elodie。深入 io.WriterGreet 到底有多通用既然参数是io.Writer那么任何实现了Write方法的类型都能成为输出目标。Greet的通用性远超「打印到终端」这一个场景。场景一命令行 / 文件 / 网络缓冲区os.Stdout*os.File打印到终端或重定向到文件bytes.Buffer测试时捕获输出、或临时拼接字节数据任何自定义实现了Write的类型比如写入网络连接、日志系统、压缩流等。场景二互联网——把 Greet 变成 HTTP 处理器http.ResponseWriter也实现了io.Writer因此Greet可以原封不动地放进 HTTP 处理器里。仓库 di/v2/di.go 给出了完整示例package main import ( fmt io log net/http ) // Greet sends a personalised greeting to writer. func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, Hello, %s, name) } // MyGreeterHandler says Hello, world over HTTP. func MyGreeterHandler(w http.ResponseWriter, r *http.Request) { Greet(w, world) } func main() { log.Fatal(http.ListenAndServe(:5000, http.HandlerFunc(MyGreeterHandler))) }在 di/v2 目录下运行go run .然后访问http://localhost:5000注意仓库实际代码监听的是:5000端口原文档示例中写的是:5001请以仓库代码为准就能在浏览器里看到Hello, world。这段代码揭示了两个要点编写 HTTP 处理器时Go 会给你一个http.ResponseWriter用于写出响应和http.Request请求对象实现服务器时就是通过 writer 把响应写出去接口复用的力量http.ResponseWriter恰好实现了io.Writer所以我们的Greet无需任何改动就直接在处理器中被复用了。HTTP 服务器的完整知识会在后续 http-server.md 章节展开这里只需理解「面向接口编程带来的即插即用」。小结依赖注入带来了什么最初版本的代码难以测试因为它把数据写到了我们无法控制的地方。在测试的驱动下我们通过「注入依赖」重构了代码从而能够控制数据的写入位置。这带来三个直接收益让代码可测试如果一个函数很难测试通常是因为依赖被硬编码进了函数内部或依赖了全局状态。比如某个服务层直接使用全局数据库连接池测试起来就会又慢又难依赖注入会促使你把数据库依赖通过接口注入进来测试时就能替换成可控的假实现。分离关注点把「数据写到哪」与「如何生成数据」解耦。如果你觉得某个方法/函数承担了过多职责既要生成数据又要写数据库既要处理 HTTP 请求又要做领域逻辑依赖注入很可能就是你需要的工具。让代码在不同场景复用第一个「新场景」就是测试往后任何想以新方式使用该函数的人都可以注入自己的依赖。补充澄清Mock 与 DI 的关系很多人一提到依赖注入就联想到 Mock甚至认为 Mock 是「邪恶」的。实际上Mock在后续 mocking.md 章节会详细介绍是用来把注入的真实依赖替换成可在测试中控制和检查的假版本它本身并不可怕。在本文的例子中我们甚至不需要自己写 Mock——标准库的bytes.Buffer已经现成地充当了「假 writer」。给读者的建议花时间研究 Go 标准库正是因为我们熟悉io.Writer接口才能在测试中用bytes.Buffer充当Writer又能在命令行应用和 Web 服务器中使用标准库的其他Writer实现。对标准库越熟悉就越能发现这些通用接口——把它们复用到自己的代码中你的软件就能在多种上下文中被复用。动手实践建议阅读 di/v1/di.go 与 di/v1/di_test.go理解「接口参数 测试注入」的最小闭环运行cd di/v1 go test与go run .分别观察测试输出与终端输出运行cd di/v2 go run .并访问http://localhost:5000验证同一个Greet在 HTTP 场景下的复用进阶练习自定义一个实现Write方法的类型例如写入strings.Builder或自己的日志结构把它注入Greet体会「面向接口」的扩展性。赞分享教程示例工程【免费下载链接】learn-go-with-testsLearn Go with test-driven development项目地址https://gitcode.com/gh_mirrors/le/learn-go-with-tests点击查看免费下载相关推荐Learn Go with Tests依赖注入指南构建可测试代码Learn Go with Tests依赖注入指南构建可测试代码 依赖注入是Go语言中构建可测试、松耦合代码的强大技术。本文将指导您如何使用依赖注入技术编写高教程示例工程让 os/exec 代码可测试用依赖注入拆解命令执行与业务逻辑Learn Go with Tests 实战解析让 os/exec 代码可测试用依赖注入拆解命令执行与业务逻辑Learn Go with Tests 实战解析 导读 本文源自 Learn Go with教程示例工程如何在spin.js中掌握依赖注入提升代码可测试性的完整指南如何在spin.js中掌握依赖注入提升代码可测试性的完整指南 在现代JavaScript开发中依赖注入Dependency Injection是一种重要UI组件前端上一篇如何掌握深度强化学习数学推导Spinning Up理论实践终极指南下一篇【亲测免费】 探索stdeb将Python包转换为Debian源包的利器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询