探索 Go 标准库中的优雅设计模式:函数与接口的结合
点击关注公众号,“技术干货”及时达!前言在 Go 语言的标准库(特别是在 net/http 包中),我们会发现一种非常优雅且简洁的编程模式,即通过函数类型与接口的结合来实现灵活且可扩展的代码。这不仅仅在代码复用、简化测试方面带来了极大方便,还提升了整体的开发效率和代码质量。这篇文章我们将探讨这一模式的工作原理及其诸多好处。
基础概念首先,我们需要理解 Go 语言中的几个基础概念:
接口(Interface):在 Go 中接口是一组方法的集合,任何实现了这些方法的类型都可以被认为实现了这个接口。类型定义(Type Definition):Go 语言允许定义新的类型。例如,可以将某种函数签名定义为一种类型。方法(Method):可以为某个类型定义方法。该类型可以是结构体、基础类型甚至是函数类型。实战分析我们来看 net/http 包中是如何使用这些概念构建一个非常灵活的 HTTP 服务器的。
先看一张图,总览一下这个灵活的 HTTP 服务器:
net/http 包中的设计?Go 语言源代码 net/http/server.go
版本:Go 1.22.2
?首先来看看标准库中 Handler 接口的定义:
typeHandlerinterface{
ServeHTTP(ResponseWriter,*Request)
}
任何实现了 ServeHTTP 方法的类型都可以被用作 HTTP 请求的处理器。
一般情况下,我们都习惯于使用一个结构体来实现一个接口,尽管可能使用空的结构体,例如:
typeMyHandlerstruct{}
func(h*MyHandler)ServeHTTP(whttp.ResponseWriter,r*http.Request){
w.Write([]byte("helloworld"))
}
而这样无疑会增加很多重复且无用的代码,因为我们并不关注 MyHandler 结构体。因此,我们不想每次都去定义一个结构体和绑定方法,能不能使用简单的函数实现?
那么,我们如何将一个普通的函数转变为符合 Handler 接口的类型呢?
这就需要用到以下的代码模式,我们接着来看源码:
typeHandlerFuncfunc(ResponseWriter,*Request)
func(fHandlerFunc)ServeHTTP(wResponseWriter,r*Request){
f(w,r)
}
这里定义了一种新的函数类型 HandlerFunc,其签名与 ServeHTTP 方法一致。并为其实现了 ServeHTTP 方法,这样就使得所有这种类型的函数都符合 Handler 接口。
那这种模式具体该怎么使用呢?
实战代码我们通过具体的代码示例来更好地理解这一模式的使用:
packagemain
import(
"net/http"
"fmt"
)
//一些通用的逻辑封装
funchelloHandler(whttp.ResponseWriter,r*http.Request){
w.Write([]byte("helloworld"))
}
funcmain(){
//http.HandlerFunc(helloHandler)将helloHandler函数转换为HandlerFunc类型
http.Handle("/hello",http.HandlerFunc(helloHandler))
//或者直接使用http.HandleFunc,效果完全相同
http.HandleFunc("/hello",helloHandler)
http.ListenAndServe(":8080",nil)
}
在这个例子中:
我们定义了一个处理请求的函数 helloHandler;然后通过 http.Handle 将 url 与处理函数绑定起来,而 http.Handle 函数需要一个实现了 Handler 接口的对象;我们通过 http.HandlerFunc(helloHandler) 将其转换为 HandlerFunc 类型,这样 helloHandler 就自动实现了 ServeHTTP 方法,从而成为一个合法的 Handler;我们还可以使用 http.HandleFunc 直接将函数关联到某个路径,这其实是标准库对上述转换的简单封装。我们点进去看一眼源码:
funcHandleFunc(patternstring,handlerfunc(ResponseWriter,*Request)){
//版本控制,暂时不用关注
ifuse121{
DefaultServeMux.mux121.handleFunc(pattern,handler)
}else{
DefaultServeMux.register(pattern,HandlerFunc(handler))
}
}
funcHandle(patternstring,handlerHandler){
//版本控制,暂时不用关注
ifuse121{
DefaultServeMux.mux121.handle(pattern,handler)
}else{
DefaultServeMux.register(pattern,handler)
}
}
源码底层实现就是将 “路径(url) + 处理器” 注册到 DefaultServeMux(默认的 HTTP 请求多路复用器),这样我们使用 url 访问 web 请求时,就可以路由到对应的处理器进行处理了。
这样就轻松实现了将一个简单处理函数,注册进默认的 HTTP 路由器中的逻辑,如果需要多个处理函数,也可以轻松实现,而不用为每一个处理函数都定义一个空的结构体,然后绑定方法,这样代码会更加简洁和易读。
http.Handle("/hello",http.HandlerFunc(helloHandler))
http.Handle("/hello",http.HandlerFunc(helloHandler2))
http.Handle("/hello",http.HandlerFunc(helloHandler3))
如果你仔细观察,会发现 http.ListenAndServe 的第二个参数也是接口类型 Handler,我们使用了标准库 net/http 内置的路由,因此传入的值是 nil。
http.ListenAndServe(":8080",nil)
标准库 net/http 内置的路由为 ServeMux,本身也实现了 ServeHTTP 方法,也是一个合法的 Handler。
typeServeMuxstruct{
musync.RWMutex
treeroutingNode
indexroutingIndex
patterns[]*pattern//TODO(jba):removeifpossible
mux121serveMux121//usedonlywhenGODEBUG=httpmuxgo121=1
}
func(mux*ServeMux)ServeHTTP(wResponseWriter,r*Request){
ifr.RequestURI=="*"{
ifr.ProtoAtLeast(1,1){
w.Header().Set("Connection","close")
}
w.WriteHeader(StatusBadRequest)
return
}
varhHandler
ifuse121{
h,_=mux.mux121.findHandler(r)
}else{
h,_,r.pat,r.matches=mux.findHandler(r)
}
h.ServeHTTP(w,r)
}
它的主要作用就是,通过传入的 url 匹配对应的处理函数 mux.findHandler(我们刚刚注册进去的 ),然后通过 h.ServeHTTP(w, r) 实现对不同 url 的处理逻辑,这样一个灵活的 HTTP 服务器就搭建完成了,通过这个 http.ListenAndServe(":8080", nil) 就可以轻松启动起来(具体内部如何建立连接,处理请求不是本文重点,后续再详细讲解源码)。
一个思考问题:那如果这个地方我们传入的是一个实现了 Handler 接口的结构体呢?
是不是就可以完全托管所有的 HTTP 请求,后续怎么路由,怎么处理,请求前后增加什么功能,都可以自定义了,慢慢地,就变成了一个功能丰富的 Web 框架了。(有兴趣的可以先看看 gin 框架,后续我们有机会也详细聊一下一个功能丰富的 Web 框架如何搭建)
结论通过了解和实践这种编程模式,我们可以更高效地编写简洁、灵活且高质量的代码,这不仅提高了开发效率,也使得代码更加易于维护和扩展。
除了代码简洁,这种模式还有什么优点呢?
接下来就总结一下这种模式的好处:
简洁性与可读性:通过这种方式,我们可以轻松地将简单的函数转换为复杂接口的实现。这使得代码更加简洁和直观,易于维护。灵活性:这种设计模式允许开发者在处理不同需求时能够灵活调整代码。通过定义不同的方法和处理器,能够轻松适应变化。减少样板代码:我们不必每次都去定义一个结构体和绑定方法,只需编写符合特定签名的函数即可。这减少了样板代码,让我们可以将更多时间花费在业务逻辑上。增强代码复用性:由于函数可以非常方便地转换为实现接口的类型,这使得代码模块化和复用性变得更加容易。我们可以编写通用函数来处理许多场景,然后通过这种模式组合实际需求。简化测试:函数的单元测试通常更为容易。通过这种模式,我们可以直接对函数进行测试,然后再将其集成到实际应用中,从而简化了测试流程。希望通过这篇文章,大家能更好地理解 Go 语言中函数类型和接口结合所带来的强大之处,并在实际开发中合理运用这一模式。
最后举个简单的例子,自己领悟一下“代码的整洁之道”:
packagemain
import"fmt"
//定义一个操作接口
typeOperationinterface{
Execute(a,bint)int
}
//使用一个类型,将函数适配成符合接口的方法
typeOperationFuncfunc(a,bint)int
//让函数类型实现接口
func(fOperationFunc)Execute(a,bint)int{
returnf(a,b)
}
//计算函数,接受一个Operation接口
funcCalculate(a,bint,opOperation)int{
returnop.Execute(a,b)
}
funcmain(){
//定义一些操作函数
addition:=OperationFunc(func(a,bint)int{
returna+b
})
subtraction:=OperationFunc(func(a,bint)int{
returna-b
})
//使用操作函数进行计算
fmt.Println("Addition:",Calculate(10,5,addition))//Output:Addition:15
fmt.Println("Subtraction:",Calculate(10,5,subtraction))//Output:Subtraction:5
}
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线