作者:wuyunhua | 2023-09-24
golang提供struct和interface特性来对事物进行抽象,抽象是程序设计的核心,本文来探讨一下Duck Typing思想在golang中的实现和应用
出自培根《自然论》:If it looks like a duck, walks like a duck, and quacks like a duck, it's a duck。如果一个动物看起来像鸭子,走起来像鸭子,叫起来像鸭子,则它就是鸭子。 我们不讨论他的哲学部分,我们讨论一种思维模式:关注行为而不关注属性。生活中有很多类似的例子:
与之对立的是关注类型,即关注行为也关注属性,一些例子:
所以,Duck Typing就是一个对象是否有效,只取决于对象当前方法集合
package http
type Request struct {}
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type ServeMux struct {
// ...
}
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
// ...
}
type Server struct {
Addr string
Handler Handler
// ...
}
func (srv *Server) ListenAndServe() error {
// ...
ln, err := net.Listen("tcp", addr)
return srv.Serve(ln)
}
func (srv *Server) Serve(l net.Listener) error {
// ...
c := srv.newConn(rw)
go c.serve(connCtx)
}
func (c *conn) serve(ctx context.Context) {
// ...
serverHandler{c.server}.ServeHTTP(w, w.req)
}
上面代码是go标准库net/http中节选的关键代码。先站在使用者的角度,我们来看看这段代码有什么特别的地方:
package main
mux := http.NewServeMux()
mux.HandlerFunc("/", func(w http.ResponseWriter, r *http.Request){})
srv := http.Server{
Handler: mux,
}
if err := srv.ListenAndServe(); err != http.ErrServerClosed{
panic(err)
}
这就是一个简单http服务的代码。只要有http.Server
对象,就可以启动一个http服务,而http.Server
需要http.Handler
,这是一个interface类型,所有实现了ServeHTTP(ResponseWriter, *Request)
方法的对象都可以作为http.Handler
使用。
上面的例子里,http.ServeMux
是一个官方实现的多路复用器,可以换成别的:
package main
router := gin.New()
router.GET("/", func(c *gin.Context){})
srv := http.Server{
Handler: router
}
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
panic(err)
}
gin
框架里有这样一段代码:func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request){}
,他实现了http.Handler
,所以我们很容易将官方的http.ServeMux
替换成了gin.Engine
。
现在站在net/http作者的角度,他在考虑http服务器这件事的时候,发现http的协议实现是不变的,而业务是变化的。他将变化的部分抽象成一个接口,任何具有ServeHTTP
行为的对象,都被视为有效的对象,不关心这个对象有什么数据,可以互换使用,这就是golang中的Duck Typing
关注行为舍弃次要东西,可以简化问题,有助于对复杂问题进行抽象,从而获得巨大的灵活性。
这种灵活性会牺牲可读性,使代码更难理解,而且这种抽象如果不合理,会使得问题变得更复杂,对技术人员程序设计能力要求较高。
由于上面分析的优缺点,Duck Typing更适合处理这样一类问题:
生活中这种设计思想随处可以见:汽车都有同样的行为(油门、刹车、方向盘),司机不需要关心汽车的工作原理就可以使用他,对于汽车厂商来说,实现油门刹车方向盘等行为,就是汽车暴露的接口。新能源车实现了这些行为,那他就是汽车,哪怕他没有内燃机。带来的巨大灵活性就是司机可以驾驶任何一辆车,而不用重新学习
golang中struct只关心数据,interface只关心行为,数据和行为是分离的,相比传统的面向对象class,他们的数据和行为是在一起的,go团队为什么要这样设计,有什么好处吗?个人观点,在使用上区别不大,可能和go语言的设计者思考哲学有关:更符合单一职责原则
很多设计原则都会提到依赖抽象而不依赖具体,这在go标准库中非常常见:
type error interface{}
type Context interface{}
func GetId(ctx context.Context) error {
// ...
}
看这个再常见不过的函数,他的入参和返回都是interface{},这就是典型的依赖抽象而不依赖具体,好处是你可以传context.Background()
,也可以传gin.Context
,可以返回errors.New()
,也可以返回gorm.ErrRecordNotFound
舍弃次要的东西有助于揭露事物的本质,并在抽象过程中分离出来普遍性质和关系,对于问题能有一般解来说是起决定性作用的 ---- A.D.亚历山大洛夫
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.wuyunhua.cn/golang-duck-typing