1

对 interface 的使用想必是一件简单到自然的事。定义一组方法描述特定的行为,然后在某个类上实现这组方法。如此一来,这个类就能作为某些函数的输入或输出参数,而外界无从知晓用作参数的实际的类到底是怎么实现的。interface 带来的“隐藏性”,可以让它的使用者和实现者之间解耦,无需暴露不必要的细节。但是在某些特殊情况下,这种“隐藏性”却会带来预期之外的结果,为程序的正确运行留下隐藏的坑。

本文便聊聊其中几个有趣的“想见太子,却遇狸猫”的案例。

使用了 interface,但是没有想要的方法……

设想一种情景:张三写了个库,向外提供两个函数。其中函数甲返回一个 interface A,而函数乙接受 interface A(这个 interface A 可能是函数甲返回的,也可能不是)。接着有一天,张三发现有必要往函数乙里面添加新的能力,而这个能力要求函数甲内部才知道的数据。直接的做法是,在 interface A 里面提供一个可供调用的新方法。但是这样做会导致第三方的当前的 interface A 实现需要修改,否则无法适配 interface A 的变化。于是张三灵机一动,提供另一个 interface B。函数甲返回的类,既实现了 interface A,也实现了 interface B。这样在函数乙内部,检查输入是否也实现了 interface B,即可决定是否执行新的代码。对于之前由第三方提供的 interface A,还是走原来的路径。

看上去一切都进行得天衣无缝,直到某一天,库的使用者,李四,告诉张三,升级了库之后无论如何都执行不到新的代码。原来李四在函数甲的返回上包了一层,用来做调用次数的统计。在 Go 里面,如果一个类作为某个 interface 被包了一层,那么除非显式 type cast 成原本的类,否则只能调用到 interface 的方法。也就是说,被包了一层后,“函数甲返回的类实现了 interface B” 这一信息被丢失了。

这并非抽象的坐而论道。在真实世界里,interface A 是 http.ResponseWriter,interface B 是 http.Flusher。有时候需要包一层 Go 的 ResponseWriter,比如做 metrics 统计。但传过来的 ResponseWriter 的实现可能也会实现其他接口,如果只是简单实现 ResponseWriter 会导致能力丢失,比如 Flush 调用不了。现实生活中,为了应对这一问题,人们想出了不同的解法。https://github.com/openzipkin/zipkin-go/blob/e84b2cf6d2d915fe0ee57c2dc4d736ec13a2ef6a/middleware/http/server.go#L218 这里直接用排列组合转包了几个可能的 interface。还有一种做法是,自己把每个 interface 实现一遍,类似于 https://github.com/traefik/plugin-rewritebody/blob/082c42bffdd33b8d6351ef7146d48b56012838f9/rewritebody.go#L135

一个更好的解法是让库的开发者提供 Unwrap 方法,就像标准库里面的 errors.Unwrap 一样。这样库的使用者在实现包一层的逻辑时可以 Unwrap 一下,不是直接包传过来的 interface A,而是用函数甲里原始的,同时实现 interface A 和 interface B 的类。

使用了 interface,但是有想要的方法……

在另外的场景里,interface 里面有想要的方法,但却不是真正想要的那个。听起来比较奇怪,对吧?所以让我们看看一个示例:

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Person struct {
    Name      string    `json:"name"`
    Birthdate time.Time `json:"birthdate"`
}

func (p Person) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }{
        Name: p.Name,
        Age:  time.Now().Year() - p.Birthdate.Year(),
    })
}

type Programmer struct {
    Person
    Job string `json:"job"`
}

func main() {
    p := Programmer{
        Person: Person{
            Name:      "张三",
            Birthdate: time.Date(1990, time.January, 1, 0, 0, 0, 0, time.UTC),
        },
        Job: "后端开发",
    }
    b, _ := json.Marshal(p)
    fmt.Printf("%+v\n", string(b))
}

上面的例子的输出结果如下:

{"name":"张三","age":34}

我们会看到张三失去了后端开发的工作。interface 的奇妙误用,不仅导致张三编写的新功能无法让李四用上,还让他丢掉了工作。对张三来说,真是糟糕的一天!

这一悲剧源于代码里 Person 已经自定义了一个 MarshalJSON,而继承自 Person 的 Programmer 没有自定义的 MarshalJSON,就会调用 Person 的 MarshalJSON,导致所有的 Programmer 加入了“裁员广进”优化计划。

除了 MarshalJSON,诸如 MarshalText、String 等方法也会出现因为基类实现的存在,造成子类不提供默认实现的情况。

附赠一个锦囊妙计,下面的函数可用来判断某个函数是否存在,以及它是否被重载:

func IsMethodOverridden(obj any, methodName string) (bool, error) {
    v := reflect.ValueOf(obj)
    m, found := v.Type().MethodByName(methodName)
    if !found {
        return false, errors.New("method not found")
    }

    // Quoted from the doc:
    // the returned pointer is an underlying code pointer, but not necessarily enough to identify a
    // single function uniquely.
    // But as the obj is created statically and Go doesn't do JIT, it should be enough.
    p := uintptr(m.Func.UnsafePointer())
    f := runtime.FuncForPC(p)
    if f == nil {
        return false, errors.New("invalid function")
    }

    fileName, _ := f.FileLine(f.Entry())
    overridden := fileName != "<autogenerated>"
    return overridden, nil
}

继续上面的示例,

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "reflect"
    "runtime"
    "time"
)

type Person struct {
    Name      string    `json:"name"`
    Birthdate time.Time `json:"birthdate"`
}

func (p Person) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }{
        Name: p.Name,
        Age:  time.Now().Year() - p.Birthdate.Year(),
    })
}

type Programmer struct {
    Person
    Job string `json:"job"`
}

func (p Programmer) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        Name string `json:"name"`
        Job  string `json:"job"`
    }{
        Name: p.Name,
        Job:  p.Job,
    })
}

func main() {
    p := Programmer{
        Person: Person{
            Name:      "张三",
            Birthdate: time.Date(1990, time.January, 1, 0, 0, 0, 0, time.UTC),
        },
        Job: "后端开发",
    }
    ok, err := IsMethodOverridden(p, "MarshalJSON")
    if err == nil {
        fmt.Printf("is MarshalJSON overridden? %v\n", ok)
    } else {
        fmt.Printf("MarshalJSON: %s\n", err)
    }
    b, _ := json.Marshal(p)
    fmt.Printf("%+v\n", string(b))
}

输出:

is MarshalJSON overridden? true
{"name":"张三","job":"后端开发"}

用上锦囊妙计里提供的函数,我们可以知道 MarshalJSON 这个方法是否已经存在,以及我们直接使用的子类是否有重载该方法。这下张三们就不必遭受无妄之灾了。

总结

上面提到的几个例子,都是在实践过程中我遇到过的。作为有趣的 edge case,其实很难在编码过程中避开这些坑,除非你有完善的测试用例(和足够的运气)。了解这些,至少在踩到坑后快速意识到发生了什么,尽快从坑里爬出来。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.