XingPiaoLiang's

Back

基础部分#

使用非常完善的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))
	}
}
go
  • use121是一个标志变量,代表着是否使用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
go

Go的变量声明和初始化, 一共分为两个阶段进行:

  • 编译器会首先处理所有的变量声明,确定每一个变量的类型和作用域
  • 在所有变量都声明完毕之后,编译器根据源代码中的变量出现顺序进行初始化。

在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)
}
go

HandlerFunc函数类型,使用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

参考:

初探Go标准库—net/http
https://astro-pure.js.org/blog/go-net-http
Author erasernoob
Published at May 12, 2025
Comment seems to stuck. Try to refresh?✨