浅析Golang中的net/http路由注册与请求处理

 更新时间:2023年12月17日 15:28:19   作者:小唐0101  
这篇文章主要为大家详细介绍了Golang中的net/http路由注册与请求处理的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

注册路由

初学web时,我们常用http.HandleFunc()来注册路由,然后用http.ListenAndServe()来启动服务,接下来让我们分析一下http包内部是如何注册路由的。

除了常用的http.HandleFunc()可以注册路由,还有http.Handle可以注册,先看一下源码。

func Handle(pattern string, handler Handler) { 
    DefaultServeMux.Handle(pattern, handler) 
}

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

对比一下两个函数的不同:

1、分别调用了DefaultServeMux的Handle和HandleFunc方法。

2、handler参数类型分别为http.Handler接口,和func(ResponseWriter, *Request)类型。说明一下,DefaultServerMux是http包的全局变量,如果不使用默认的复用器

接下来看一下Handle和HandleFunc的主要源码,和Handler接口

// 注册路由
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()
    ...
    
    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
	
    ...
}

// 调用Handle注册路由
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

// 实现了Handler的函数类型
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

可以看到ServeMux.Handle最终实现了路由的注册,mux.m记录了路由与处理器的映射;而ServeMux.HandleFunc将handler参数转换成了HandlerFunc类型,然后调用ServeMux.Handle。

那么问题来了,ServeMux.Handle的handler参数是Handler接口类型,我们调用http.HandleFunc()传入的处理器函数签名是func(ResponseWriter, *Request),我们传入的函数咋就实现了Handler接口?

答案就在于HandlerFunc类型,它实现了Handler接口。我们传入的处理器函数与HandlerFunc类型函数签名是一致的,如果没有HandlerFunc,要注册函数的话,我们就要自己定义结构体,写ServeHTTP方法,实现Handler接口,而有了HandlerFunc我们就可以把这一步省去了,在设计模式中,这叫装饰器模式。

处理请求

ServerMux

使用http.HandleFunc和http.Handle注册的路由都注册到了DefaultServerMux,它也实现了handler接口,那让我们来看一下ServerMux的ServeHTTP方法。

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

mux.Handler()中会调用mux.match(),从ServeMux.m(map[string]muxEntry类型),获取路由和对应处理器函数(我们传入的),然后就可以调用h.ServeHTTP(w,r)来处理对应的请求。

现在已得知我们传入的处理函数是被ServeMux.ServeHTTP()调用,那ServerMus.ServeHTTP()又是怎么被调用的呢?接下来跟踪一下http.ListenAndServe()的源码。

Server

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

用http.ListenAndServe()启动服务的话,handler参数一般传nil,使用DefaultServerMux做处理器,下面的分析都以此为前提。

在函数内部帮我们创建了一个Server结构体,如果想更灵活地使用,可以自己创建Server结构体,调用server.ListenAndServe()。

func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)	// 监听地址
    if err != nil {
        return err
    }
    return srv.Serve(ln)
}

Server.ListenAndServe()中完成了监听地址的绑定,然后再调用Server.Serve()

func (srv *Server) Serve(l net.Listener) error {
    
	...
    
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()

        ...
            
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks)
        go c.serve(connCtx)
    }
}

Server.Serve中开启了一个for循环来接收连接,并为每一个连接创建contexxt,开一个协程继续处理。

func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    var inFlightResponse *response
    defer func() {
        if err := recover(); err != nil && err != ErrAbortHandler {
            const size = 64 << 10
            buf := make([]byte, size)
            buf = buf[:runtime.Stack(buf, false)]
            c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
        }
        if inFlightResponse != nil {
            inFlightResponse.cancelCtx()
        }
        if !c.hijacked() {
            if inFlightResponse != nil {
                inFlightResponse.conn.r.abortPendingRead()
                inFlightResponse.reqBody.Close()
            }
            c.close()
            c.setState(c.rwc, StateClosed, runHooks)
        }
    }()

	...
    
    for {
        w, err := c.readRequest(ctx)

        ...

        inFlightResponse = w
        serverHandler{c.server}.ServeHTTP(w, w.req)  // 这里并不是Handler接口的ServeHTTP方法
        inFlightResponse = nil
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        
      	...
    }

    ...
}

在http包server.go 1991行,嵌套了Server结构体的serverHandler结构体调用ServeHTTP方法,需要注意的是这并不是Handler接口的ServeHTTP方法,我们传入的处理器函数的调用还在其内部。

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux	// 如果在创建Server时,Handler参数为nil,就使用DefaultServeMux
                                                                    //  通过http.ListenAndServe开启服务一般都用DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }

    if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
        var allowQuerySemicolonsInUse int32
        req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
            atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
        }))
        defer func() {
            if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
                sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
            }
        }()
    }

    handler.ServeHTTP(rw, req)	// 调用DefaultServeMux的ServeHTTP
}

最后一行调用DefaultServeMux的ServeHTTP,上文已介绍过,它的作用就是获取到请求对应的处理器函数并执行。

延伸

gin框架是基于http包封装的,gin匹配路由的方式不同于原生包,使用前缀路由树来匹配路由,通过engin.Run启动服务,其内部也是调用的http.ListenAndServe(),那gin是如何应用的自定义匹配方式呢?其实很简单,上文提到,调用http.ListenAndServe()时第二个参数handler是nil的话,会使用DefaultServeMux当做复用器,那engin.Run()中调用时传入gin定义的复用器就好了。

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    if engine.isUnsafeTrustedProxies() {
        debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
            "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
    }

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())	// engin.Handler()返回gin的自定义处理器
    return
}

以上就是浅析Golang中的net/http路由注册与请求处理的详细内容,更多关于go net/http的资料请关注脚本之家其它相关文章!

相关文章

  • Go语言编程通过dwarf获取内联函数

    Go语言编程通过dwarf获取内联函数

    这篇文章主要为大家介绍了Go语言编程通过dwarf获取内联函数详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • 解决GOPATH在GOLAND中的坑

    解决GOPATH在GOLAND中的坑

    这篇文章主要介绍了解决GOPATH在GOLAND中的坑,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go语言中结构体方法副本传参与指针传参的区别介绍

    Go语言中结构体方法副本传参与指针传参的区别介绍

    这篇文章主要给大家介绍了关于Go语言中结构体方法副本传参与指针传参的区别的相关资料,文中先对GO语言结构体方法跟结构体指针方法的区别进行了一些简单的介绍,来帮助大家理解学习,需要的朋友可以参考下。
    2017-12-12
  • Golang中Channel实战技巧与一些说明

    Golang中Channel实战技巧与一些说明

    channel是Go语言内建的first-class类型,也是Go语言与众不同的特性之一,下面这篇文章主要给大家介绍了关于Golang中Channel实战技巧与一些说明的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-11-11
  • Go语言中make和new函数的用法与区别

    Go语言中make和new函数的用法与区别

    这篇文章介绍了Go语言中make和new函数的用法与区别,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • 文字解说Golang Goroutine和线程的区别

    文字解说Golang Goroutine和线程的区别

    goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理,使用每一个 go 关键字将会额外开启一个新的协程 goroutine,今天通过本文给大家介绍下Golang Goroutine和线程的区别,感兴趣的朋友一起看看吧
    2022-03-03
  • 重学Go语言之JSON操作详解

    重学Go语言之JSON操作详解

    JSON的全称是Javascript Object Notation,是一种数据结构化交互的标准协议,这篇文章主要是来和大家介绍一下Go语言中JSON的相关操作,需要的可以参考下
    2023-08-08
  • Go语言实现枚举的示例代码

    Go语言实现枚举的示例代码

    本文主要介绍了Go语言实现枚举的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • Go语言学习之函数的定义与使用详解

    Go语言学习之函数的定义与使用详解

    这篇文章主要为大家详细介绍Go语言中函数的定义与使用,文中的示例代码讲解详细,对我们学习Go语言有一定帮助,需要的可以参考一下
    2022-04-04
  • 一篇文章带你轻松搞懂Golang的error处理

    一篇文章带你轻松搞懂Golang的error处理

    在进行后台开发的时候,错误处理是每个程序员都会遇到的问题,下面这篇文章主要给大家介绍了关于Golang中error处理的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07

最新评论