吴云华 / golang中的Duck Typing

作者:wuyunhua | 2023-09-24

golang提供struct和interface特性来对事物进行抽象,抽象是程序设计的核心,本文来探讨一下Duck Typing思想在golang中的实现和应用

什么是Duck Typing

出自培根《自然论》:If it looks like a duck, walks like a duck, and quacks like a duck, it's a duck。如果一个动物看起来像鸭子,走起来像鸭子,叫起来像鸭子,则它就是鸭子。 我们不讨论他的哲学部分,我们讨论一种思维模式:关注行为而不关注属性。生活中有很多类似的例子:

  • 交通罚款取决于司机的违法行为,而不取决于车的品牌、司机的性别、年龄等
  • 如果一个形状,他到定点的距离等于定长,那他就是圆,不关心他多大,什么颜色

与之对立的是关注类型,即关注行为也关注属性,一些例子:

  • 能否成为太阳系行星,既要围绕太阳公转的行为,也需要本身具有足够的质量
  • 相亲的时候,关注对我好(行为),身高180+(属性)

所以,Duck Typing就是一个对象是否有效,只取决于对象当前方法集合

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优点和缺点

关注行为舍弃次要东西,可以简化问题,有助于对复杂问题进行抽象,从而获得巨大的灵活性。

这种灵活性会牺牲可读性,使代码更难理解,而且这种抽象如果不合理,会使得问题变得更复杂,对技术人员程序设计能力要求较高。

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