基础部分#
使用非常完善的http库创建一个简单的web服务器
func main() {
http.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "What is the http.Handler")
})
_ = http.ListenAndServe(":8080", nil)
}
go- 首先使用
HandleFunc
注册到一个路由处理函数,后面的处理函数必须是func(w http.ResponseWriter, r *http.Request)
这样的函数类型 - 第二行,
ListenAndServe
第二个参数接收的是一个Handler
http.ResponseWriter
接口:
type ResponseWriter interface {
Header() Header
Writer([]byte) (int, error)
WriteHeader(statusCode) int
}
go查看HandleFunc
的源码:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if use121 {
DefaultServeMux.mux121.handleFunc(pattern, handler)
} else {
DefaultServeMux.register(pattern, HandlerFunc(handler))
}
}
gouse121
是一个标志变量,代表着是否使用Go1.21版本的注册方式,否则使用默认的路由注册方式DefaultServeMux
是一个ServeMux
的一个实例
在Go的许多库中,都可以看到这样的默认实例声明的模式,在默认参数就已经足够的场景中使用默认实现很方便。
type ServeMux struct {
mu sync.RWMutex
tree routingNode
index routingIndex
mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}
// DefaultServeMux is the default [ServeMux] used by [Serve].
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
goGo的变量声明和初始化, 一共分为两个阶段进行:
- 编译器会首先处理所有的变量声明,确定每一个变量的类型和作用域
- 在所有变量都声明完毕之后,编译器根据源代码中的变量出现顺序进行初始化。
在121版本下,查看ServeMux.HandleFunc
:
func (mux *serveMux121) handleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.handle(pattern, HandlerFunc(handler))
}
go这里使用调用serveMux
的方法,将handler类型转换成HandlerFunc
函数类型。
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
goHandlerFunc
函数类型,使用func(ResponseWriter, *Request)
作为底层类型,为其定义了方法ServeHTTP
。也就是说,这里仅仅只是,一个类型约束,只是HandlerFunc,为其定义了一个名叫ServeHTTP
的特殊方法,但是还是调用的自己。
为什么要这样做呢?为什么要进行类型转换:
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func (mux *serveMux121) handle(pattern string, handler Handler) {}
go因为handle
只接受实现了Handler
接口的类型作为参数,此时将其转换为HandlerFunc
类型,也就等同于实现了Handler
接口了。
所以,我们可以自己定义一个实现了Handler
接口的类型,将其注册为路由处理器(Handler)
type greeting string
func (g *greeting) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, *g)
}
func main() {
var g = greeting("hello")
http.Handle("/greeting", &g)
}
go- 这里的
ServeHTTP
方法是绑定在*greeting
指针类型上的,所以在传入Handle
函数中时,应该传入greeting
的指针。
为了便于区分,我们将通过
HandleFunc()
注册的称为处理函数,将通过Handle()
注册的称为处理器。通过上面的源码分析不难看出,它们在底层本质上是一回事。
接着我们再看调用http.ListenAndServe(":8080", nil)
, 开始监听8080端口处理请求。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
go- 在该函数中,实例化一个
server
对象。
type Server struct {
Addr string
Handler Handler
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
}
go可以通过改变Server对象之中的参数,设置web服务器的各种参数(超时时长等等)。
在Server
对象ListenAndServe
方法中:
func (s *Server) ListenAndServe() error {
if s.shuttingDown() {
return ErrServerClosed
}
addr := s.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ln)
}
go通过调用net.Listen
,将获取到的Listener
对象传入到Serve
方法中。
for {
rw, err := l.Accept()
if err != nil {
if s.shuttingDown() {
return ErrServerClosed
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := s.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := s.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
go在Serve方法中,使用一个无限for循环,不断地调用Accept
,接收新连接。开启goroutine处理新的连接
注意到在最后,如果获取到了连接对象rw
,会将其封装成为一个自己的conn
对象,然后开启goroutine执行serve
方法。
serve()
方法其实就是不停地读取客户端发送地请求,创建serverHandler
对象调用其ServeHTTP()
方法去处理请求,然后做一些清理工作
简化后的serve()
方法如下:
func (c *conn) serve(ctx context.Context) {
for {
w, err := c.readRequest(ctx)
serverHandler{c.server}.ServeHTTP(w, w.req)
w.finishRequest()
}
}
go如果你也和我一样一直在思考Server
是怎么拿到我们之前注册好的handle
处理器或者说是handleFunc
处理器函数逻辑的,在serve
方法中就可以看到,借助了一个中间的辅助结构serverHandler
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
handler.ServeHTTP(rw, req)
}
go通过该结构体取出server
中的serverMux
,在之前我们传入的是nil
,所以这里就默认的使用DefaultServeMux
,这样就能够使用我们之前定义好的路由处理逻辑了。
创建ServeMux
#
在示例程序中,我们一直使用的是,DefaultServeMux
。使用默认的对象存在问题:不可控
Server
参数都使用了默认值。- 第三方库也可能使用这个默认对象注册一些处理,容易冲突
- 我们在不知情中调用
http.ListenAndServe()
开启 Web 服务,那么第三方库注册的处理逻辑就可以通过网络访问到,有极大的安全隐患
所以我们可以自定义ServeMux
:
type greeting string
func (g greeting) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, g)
}
func main() {
var g = greeting("hello")
mux := http.NewServeMux()
mux.Handle("/greeting", g)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 20 * time.Second,
WriteTimeout: 20 * time.Second,
}
server.ListenAndServe()
}
go整个流程如下图所示: ![[Pasted image 20250512155920.png]]
中间件#
该部分主要内容为HTTP库编写中间件。中间件的存在的原因是:可能在每一次请求处理的PipLine中增加一些所有请求都通用的逻辑。
- 统计处理耗时
- 记录日志
- 捕获宕机 不同于Java的AOP切片思想,Go的中间件是依靠闭包实现的。
// 定义一个 middleware 中间件类型
type Middleware func(http.Handler) http.Handler
func WithLogger(handler http.Handler) http.Handler {
// 将函数转换为中间类型 handlerFunc
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
now := time.Now()
log.Default().Printf("start to Record the time, now: %s", now)
defer func() {
log.Default().Printf("The total request used %d ms", time.Since(now).Milliseconds())
}()
handler.ServeHTTP(rw, req)
})
}
func (g greeting) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, g)
}
func main() {
var g = greeting("hello")
mux := http.NewServeMux()
mux.Handle("/greeting", WithLogger(g))
go因为编写中间件是由开发者,也就是我们编写的(而不是本身的库作者)。所以我们需要捕获可能出现的panic。
func PanicRecover(handler http.Handler) http.Handler {
return http.HandlerFunc((func(rw http.ResponseWriter, req *http.Request) {
if err := recover(); err != nil {
log.Default().Fatal(string(debug.Stack()))
}
handler.ServeHTTP(rw, req)
}))
}
go很容易就会发现,这样进行传入middlewares是很繁琐的,类似于:
mux.Handle("/", PanicRecover(WithLogger(Metric(http.HandlerFunc(index)))))
mux.Handle("/greeting", PanicRecover(WithLogger(Metric(greeting("welcome, dj")))))
go所以我们可以根据原本Mutex的思路,重新封装一个属于我们自己的ServeMutex,思路如下:
- 提供
Handle
方法处理传入的处理器 - 提供
HandleFunc
方法处理传入的处理器方法 - 提供一个
Use
方法,处理传入的中间件
package mhttp
import (
"net/http"
)
// 定义一个 middleware 中间件类型
type Middleware func(http.Handler) http.Handler
// 设计一个自己的Mutex, 简化middlewares应用逻辑
type MyServeMutex struct {
Sm *http.ServeMux
Middlewares []Middleware
}
// new 一个ServeMutex
func NewServeMux() *MyServeMutex {
return &MyServeMutex{
Sm: http.NewServeMux(),
}
}
// use 一middlewares列表
func (m *MyServeMutex) Use(mw ...Middleware) {
m.Middlewares = append(m.Middlewares, mw...)
}
// 应用middlewares集
// 注意这里应该是 右结合
func applyMiddlewares(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i -= 1 {
handler = middlewares[i](handler)
}
return handler
}
func (m *MyServeMutex) Handel(pattern string, handler http.Handler) {
newHandler := applyMiddlewares(handler, m.Middlewares...)
m.Sm.Handle(pattern, newHandler)
}
// 处理函数
func (m *MyServeMutex) HandleFunc(pattern string, handleFunc http.HandlerFunc) {
handler := http.Handler(handleFunc)
newHandler := applyMiddlewares(handler, m.Middlewares...)
m.Sm.Handle(pattern, newHandler)
}
go参考: