之前写Geecache的时候,遇到了接口型函数,当时没有搞懂,现在重新回过头研究复习Geecache的时候,发现看得懂一些了,刚好能梳理下。
什么是接口型函数?比如下面这个 。
type Getter interface {
Get(key string) ([]byte, error)
}
// A GetterFunc implements Getter with a function.
type GetterFunc func(key string) ([]byte, error)
// Get implements Getter interface function
func (f GetterFunc) Get(key string) ([]byte, error) {
return f(key)
}
还是上面图中的代码,我们来看看。
首先定义了一个接口 Getter
,只包含一个方法 Get(key string) ([]byte, error)
,紧接着定义了一个函数类型 GetterFunc
,GetterFunc
参数和返回值与 Getter
中 Get
方法是一致的。
而且 GetterFunc 还定义了 Get
方式,并在 Get
方法中调用自己,这样就实现了接口 Getter
。所以 GetterFunc
是一个实现了接口的函数类型,简称为接口型函数。
接口型函数只能应用于接口内部只定义了一个方法的情况,例如接口 Getter
内部有且只有一个方法 Get
。既然只有一个方法,为什么还要多此一举,封装为一个接口呢?
定义参数的时候,直接用 GetterFunc
这个函数类型不就好了,让用户直接传入一个函数作为参数,不更简单吗?
看案例之前,我们再梳理一下原理。
Getter
,它要求实现一个 Get 方法GetterFunc
,其签名与 Get 方法相同GetterFunc
类型实现了 Get
方法,该方法内部直接调用函数本身。这样,任何符合 GetterFunc
签名的函数都可以被转换为 Getter 接口类型,从而进行使用。
假设 GetFromSource
的作用是从某数据源获取结果,接口类型 Getter
是其中一个参数,代表某数据源:
func GetFromSource(getter Getter, key string) []byte {
buf, err := getter.Get(key)
if err == nil {
return buf
}
return nil
}
我们可以用多种方式来实现这个这个函数。
比如方式一:GetterFunc
类型的函数作为参数。下面就是用一个匿名函数(GetterFunc类型)来作为参数。使用 GetterFunc()
将这个匿名函数转换为 GetterFunc
类型,这样它就实现了 Getter 接口
GetFromSource(GetterFunc(func(key string) ([]byte, error) {
return []byte(key), nil
}), "hello")
也可以用普通的函数。
func test(key string) ([]byte, error) {
return []byte(key), nil
}
func main() {
GetFromSource(GetterFunc(test), "hello")
}
将test
函数强制转换为GetterFunc
,而GetterFunc
实现了接口Getter
,是一个合法的参数。
本质上,上面两种方式是类型转换,在go中我们定义了一个新类型,可以用这个新的类型名作为函数来进行类型转换,比如下面 字符串类型转换。
type String string
// 将普通字符串转换为 String 类型
str := String("1234")
我们把“函数”也看做是一种类型,(字符串、整数这些都是类型),那么也可以实现 函数 类型转换,比如。
type GetterFunc func(key string) ([]byte, error)
// 将匿名函数转换为 GetterFunc 类型
getter := GetterFunc(func(key string) ([]byte, error) {
return []byte(key), nil
})
这两种情况本质上是相同的:都是将一个值转换为自定义类型。区别在于一个转换的是函数,另一个转换的是字符串。
// 使用普通函数作为数据源
func dbGetter(key string) ([]byte, error) {
// 从数据库获取数据
return []byte("value from db"), nil
}
// 将函数转换为Getter接口
var getter Getter = GetterFunc(dbGetter)
// 现在可以在任何需要Getter接口的地方使用
data, err := getter.Get("some_key")
type DB struct{ url string}
func (db *DB) Query(sql string, args ...string) string {
// ...
return "hello"
}
func (db *DB) Get(key string) ([]byte, error) {
// ...
v := db.Query("SELECT NAME FROM TABLE WHEN NAME= ?", key)
return []byte(v), nil
}
func main() {
GetFromSource(new(DB), "hello")
}
DB 实现了接口 Getter
,也是一个合法参数。这种方式适用于逻辑较为复杂的场景,如果对数据库的操作需要很多信息,地址、用户名、密码,还有很多中间状态需要保持,比如超时、重连、加锁等等。这种情况下,更适合封装为一个结构体作为参数。
综上,这样,既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性在使用函数(方式1)的时候某种程度上会更好,这就是接口型函数的价值。
上面的特性,在标准库中用得很多,net/http
的 Handler
和 HandlerFunc
就是一个典型。
看看Handler
定义。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
我们可以 http.Handle
来映射请求路径和处理函数,Handle 的定义如下所示。
func Handle(pattern string, handler Handler)
这里需要的第二参数是接口类型Handler
。
func home(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello, index page"))
}
func main() {
http.Handle("/home", HandlerFunc(home))
_ = http.ListenAndServe("localhost:8000", nil)
}
通常还有另一个函数,http.HandleFunc
,HandleFunc
的定义如下:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
第二个参数是一个普通的函数类型,那可以直接将 home
传递给 HandleFunc
,实现代码如下。
func main() {
http.HandleFunc("/home", home)
_ = http.ListenAndServe("localhost:8000", nil)
}
看看 HandleFunc
的内部实现逻辑。
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
可以看到,mux.Handle(pattern, HandlerFunc(handler))
两种写法是完全等价的,内部将第二种写法转换为了第一种写法。