对 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,其实很难在编码过程中避开这些坑,除非你有完善的测试用例(和足够的运气)。了解这些,至少在踩到坑后快速意识到发生了什么,尽快从坑里爬出来。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。