看到一个 Golang 的模式,用一个 function 来实现一个 interface,function 本身就是 interface 的实现。初次看到看了好久才想明白。在这里记录一下。
以 Golang 内置库中的 server.go1 为例。Handler 的定义如下:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
如果我们要定义一个 Handler,需要这么写:
// 定义一个结构体
type MyHandler struct{}
// 实现 http.Handler 接口的方法
func (h MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from MyHandler!")
}
http.Handle("/hello", MyHandler{})
有两个问题:略显啰嗦;距离函数内容最近的 ServeHTTP 是一个 interface 规定的具体的名字,这个函数名字不能变,但是又没有意义,所有的 Handler function 都要写成这个名字。
我们现在写 Golang 显然不是这么写的。我们会这样定义一个 Handler:
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
http.Handle("/hello", http.HandlerFunc(myHandler))
为什么我们可以这么写呢?因为源代码中有这样几行2:
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// [Handler] that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
虽然这里的注释只有短短几行,但是意义深刻。
首先,第一行定义的 type HandlerFunc func(ResponseWriter, *Request) 让我们的 myHanlder 函数变成了一个 type HandlerFunc 类型。
然后,所有的 HandlerFunc 对象都有一个方法,叫做 ServeHTTP,这就实现了 Handler 这个 interface。实现的内容,就是调用对象本身,对象本身是一个函数,所以就是调用这个函数。
综上,所有符合 ServeHTTP(w ResponseWriter, r *Request) 签名的函数都可以转换成 HandlerFunc 对象,(虽然它是函数,但是函数也是对象。)即所有签名如此的函数,都可以是一个 Handler 了。
我们就可以这么写:
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
http.Handle("/hello", http.HandlerFunc(myHandler))
那么为什么不直接把 Handler 定义成一个函数呢?
type Handler func(ResponseWriter, *Request)
就可以实现一样的效果了。
这是因为,Handler 可以变得很复杂,比如,Golang 的 middleware 本质上就是基于 Handler 的链式调用来实现的。复杂的 Handler 需要维护一些内部的状态,这种情况下,struct 就比 function 好用很多了。比如 httpauth3 这个库,就先初始化成 Handler 再使用。
那如果还是把 Handler 定义成一个 function,三方库规定在使用的时候,先初始化一个三方库定义的对象,然后三方库提供兼容 Handler 的函数,好像能达到一样的效果?
这样的话,多个 middleware 的入参和返回是不一样的对象,就无法串起来了。而如果把 Handler 定义成一个标准库里面的对象,就可以做到:middleware 接收的是一个 Handler,返回的还是一个 Handler4。只要 middleware 是这样的接口,它们就可以串联使用。
还有一个有趣的一点,Golang 里面不光函数可以实现 interface,任何类型都可以5。(Golang 还真是一切皆对象呢。)
type Counter uint64
func (i *Counter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
*i++
response := fmt.Sprintf("%v", *i)
w.Write([]byte(response))
}
func main() {
var c Counter
http.Handle("/count", &c)
http.ListenAndServe(":8080", nil)
}
- https://cs.opensource.google/go/go/+/refs/tags/go1.24.1:src/net/http/server.go;l=88 ↩︎
- https://cs.opensource.google/go/go/+/refs/tags/go1.24.1:src/net/http/server.go;l=2290 ↩︎
- https://github.com/goji/httpauth?tab=readme-ov-file#nethttp ↩︎
- https://github.com/goji/httpauth/blob/master/basic_auth.go#L153 ↩︎
- I read it from here: Functions implementing interfaces in go | Karthik Karanth ↩︎
文章提到的代码肯定编译不过:
> func myHandler(w http.ResponseWriter, r *http.Request) {
> fmt.Fprintln(w, “Hello, World!”)
> }
> http.Handle(“/hello”, myHandler)
>
> 为什么我们可以这么写呢?
应该是 http.HandleFunc。
>其中,HandlerFunc(…) 是一个显式类型转换,可以省略。
省略不了。
不过这确实是golang我最喜欢的特性。
> 不过这确实是golang我最喜欢的特性。
指的是必须类型转换,还是 one-function interface?
指的是万物皆可实现interface
谢谢,已经改正,如 https://www.kawabangga.com/posts/6903#comment-51146 说的,我以为这个类型转换可以省略呢,其实不行。
> 那么为什么不直接把 Handler 定义成一个函数呢?
也蛮常见的吧,echo就是的:
https://pkg.go.dev/github.com/labstack/echo/v4#HandlerFunc