一文带你分析Go语言中的http请求过程

 更新时间:2026年02月26日 08:42:46   作者:Nyarlathotep0113  
在go中开发后端,最基础的就是使用net/http包,本文将使用一个hello,world程序来进行debug,来探究在Go语言中http请求的过程,希望对大家有所帮助

net/http包

在go中开发后端,最基础的就是使用net/http包,本文我将使用一个hello,world程序来进行debug,来探究在代码内部究竟发生了什么。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}
func main() {
    http.HandleFunc("/hello", HelloHandler)
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

Debug

http.ListenAndServe()

整个程序的启动在于http.ListenAndServe("localhost:8080", nil)这行代码: 传入地址localhost:8080以及nil

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

可以看出这个函数的作用就是封装Server对象的创建并调用server.ListenAndServe()Server对象代表着一个http服务器,内部封装了很多http相关的参数。

从这里可以看出,如果是需要一个快速简单的的http服务器,直接使用http.ListenAndServe("localhost:8080", nil),如果需要更加精细化参数的http服务器,可以这样写:

    // 创建并配置Server
    server := &http.Server{
        Addr:    ":8080",  // 监听所有接口的8080端口
        Handler: mux,      // 使用自定义多路复用器
        
        // 重要的超时设置
        ReadHeaderTimeout: 5 * time.Second,  // 5秒内必须读完请求头
        ReadTimeout:       10 * time.Second, // 10秒内必须读完整个请求
        WriteTimeout:      10 * time.Second, // 10秒内必须写完响应
        IdleTimeout:       120 * time.Second, // 空闲连接2分钟后关闭
        
        MaxHeaderBytes: 1 << 20, // 请求头最大1MB
    }

    fmt.Println("服务器正在 8080 端口监听...")
    if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        fmt.Printf("服务器错误: %v\n", err)
    }

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

既然方法叫ListenAndServe,自然便有ListenServe的部分,从源码可以看出ln, err := net.Listen("tcp", addr)在对应的主机端口上启动一个TCP监听器,最后调用s.Serve(ln)

TCP端口监听:Serve(ln)

func (s *Server) Serve(l net.Listener) error {
    if fn := testHookServerServe; fn != nil {
       fn(s, l) // call hook with unwrapped listener
    }

    origListener := l
    l = &onceCloseListener{Listener: l}
    defer l.Close()

    if err := s.setupHTTP2_Serve(); err != nil {
       return err
    }

    if !s.trackListener(&l, true) {
       return ErrServerClosed
    }
    defer s.trackListener(&l, false)

    baseCtx := context.Background()
    if s.BaseContext != nil {
       baseCtx = s.BaseContext(origListener)
       if baseCtx == nil {
          panic("BaseContext returned a nil context")
       }
    }

    var tempDelay time.Duration // how long to sleep on accept failure

    ctx := context.WithValue(baseCtx, ServerContextKey, s)
    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)
    }
}

源码过长,因此下面的分析中省略不重要的代码

origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
...
if !s.trackListener(&l, true) {
    return ErrServerClosed
}
defer s.trackListener(&l, false)

这部分代码先是备份了先前创建的TCP监听器,然后将其封装为oneCloseListener,随后使用s.trackListener(&l, true)将监听器注册到http服务器中,退出方法时调用s.trackListener(&l, false)将监听器注销。

baseCtx := context.Background()
if s.BaseContext != nil {
    baseCtx = s.BaseContext(origListener)
    if baseCtx == nil {
       panic("BaseContext returned a nil context")
    }
}

http服务器中的BaseContext是一个回调函数,用于为整个http服务器设置服务器级别的基础上下文。 用法示例如下:

ctx, cancel := context.WithCancel(context.Background())
server := &http.Server{
    BaseContext: func(l net.Listener) context.Context {
        // 可以在这里为每个连接添加上下文值
        return context.WithValue(ctx, "listener_addr", l.Addr().String())
    },
}
// 将cancel函数注册到实际server的关闭钩子
server.RegisterOnShutdown(cancel)

最后是持续的监听环节

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

l.Accept()接收到请求后,通过http服务器的ConnContext在原有的上下文的基础上再次设置上下文,之后封装TCPConnhttp连接,并设置状态为StateNew,并允许http服务器中的ConnState钩子触发。最后启动一个协程开始该连接的服务。

trackListener()

以下是trackListener()的源码

func (s *Server) trackListener(ln *net.Listener, add bool) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    if s.listeners == nil {
       s.listeners = make(map[*net.Listener]struct{})
    }
    if add {
       if s.shuttingDown() {
          return false
       }
       s.listeners[ln] = struct{}{}
       s.listenerGroup.Add(1)
    } else {
       delete(s.listeners, ln)
       s.listenerGroup.Done()
    }
    return true
}

先判断http服务器中的监听器容器是否已经初始化了,如果没有则分配一个map对象,利用add来判断是增加监听器还是移除监听器。从这份源码可以看出,一个http服务器是支持监听多个端口的。只需要同时运行多个s.Serve(ln)

HTTP请求处理serve()

因为这部分源码实在过多,因此只展示部分代码

for{
    w, err := c.readRequest(ctx)
    ...
    serverHandler{c.server}.ServeHTTP(w, w.req)
}

方法中有一段循环,实现在同一个TCP连接上用于处理多个HTTP请求,实现HTTP/1.1Keep-Alive特性。w, err := c.readRequest(ctx)返回一个responserequest被包含在这个response对象中,后续通过serverHandler{c.server}.ServeHTTP(w, w.req)来处理http请求

serverHandler的ServeHTTP()

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
       handler = DefaultServeMux
    }
    if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
       handler = globalOptionsHandler{}
    }

    handler.ServeHTTP(rw, req)
}

此处会取出最早的时候创建http服务器时传入的Handler,如果这个接口对象是nil,将使用默认的DefaultServeMux,最终使用这个来处理http请求

handler的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
    }
    var h Handler
    if use121 {
       h, _ = mux.mux121.findHandler(r)
    } else {
       h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
    }
    h.ServeHTTP(w, r)
}

核心逻辑是根据use121的值来使用新版的findHandler还是旧版的mux121.findHandler,最终会根据请求的URI来寻找到对应的Handler接口对象,本例中是HelloHandler,它是一个HandlerFunc对象 因此调用

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

看起来这段代码似乎很多余,为什么不直接在原本的代码转成HnadlerFunc直接调用?

因为这样我们可以不使用HandlerFunc来处理逻辑,比如以下的代码

type LogMiddler struct{
    next Handler
}
func (logMiddler *LogMiddler) ServeHTTP(w ResponseWriter, r *Request){
    fmt.Println("处理的日期是....")
    logMiddler.next(w,r)
}

LogMiddler实现了Handler接口,因此上述的h可以是这个LogMiddler对象,将原本的HelloHandler封装在LogMiddler中,就实现了一种链式调用,增强原本的HelloHandler。 到目前为止,自定义的路径处理逻辑已经执行,后续都是一些善后处理。

总结

1.http.HandleFunc("/hello", HelloHandler)将处理逻辑注册到DefaultServeMux

2.创建一个http服务器

3.创建一个TCP监听器,并使用http服务器的BaseContext创建初始的context并设置一些其他值

4.当TCP请求到达时,使用http服务器的ConnContextcontext基础上创建当此次TCP连接的context,并设置当前的连接状态为StateNew,如果http服务器设置了ConnState钩子函数,在连接状态变动时会触发。最后开启一个新的协程来处理这个TCP连接

5.使用一个循环不断读取和处理http请求

6.处理http请求阶段,如果创建http服务器时传入nil,则使用DefaultServeMux,然后根据RequestURI来寻找对应的Handler接口对象,最终调用它的ServeHTTP进行处理

以上就是一文带你分析Go语言中的http请求过程的详细内容,更多关于Go语言http请求的资料请关注脚本之家其它相关文章!

相关文章

  • 详解go语言中type关键词的几种使用

    详解go语言中type关键词的几种使用

    这篇文章主要介绍了详解go语言中type的几种使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-02-02
  • 详解如何使用Go模块进行依赖管理

    详解如何使用Go模块进行依赖管理

    本文将介绍Go语言中的模块(module)概念,以及如何使用Go模块进行依赖管理,我们会探讨模块的基本概念、使用方法、配置和依赖关系管理等方面的内容,需要的朋友可以参考下
    2023-10-10
  • go设置多个GOPATH的方式

    go设置多个GOPATH的方式

    这篇文章主要介绍了go设置多个GOPATH的方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • 详解如何为Go中的无限循环添加时间限制

    详解如何为Go中的无限循环添加时间限制

    在 Go 语言的开发过程中,我们有时需要在后台执行长时间运行的任务,例如监听或轮询某些资源,这篇文章将通过一个实例详细介绍如何为 Go 语言中的无限循环设置时间限制,保证程序的健壮性和可控性,需要的朋友可以参考下
    2024-04-04
  • golang实现ip访问限制及提交次数

    golang实现ip访问限制及提交次数

    在 Web 应用中,通常会需要对 IP 访问进行限制以及控制提交次数,本文将使用中间件或者基于 Redis 这样的缓存服务来实现,感兴趣的可以了解下
    2024-10-10
  • 基于Go goroutine实现一个简单的聊天服务

    基于Go goroutine实现一个简单的聊天服务

    对于聊天服务,想必大家都不会陌生,因为在我们的生活中经常会用到,本文我们用 Go 并发来实现一个聊天服务器,这个程序可以让一些用户通过服务器向其它所有用户广播文本消息,文中通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-06-06
  • GO实现基于命令行的简单IPS程序代码

    GO实现基于命令行的简单IPS程序代码

    本文介绍了入侵防御系统IPS的工作原理和实现,IPS通过网络流量监控和实时响应,防止网络攻击,通过使用Go语言实现一个简单的IPS示例程序,展示了如何获取本地IP地址和探测网络中其他设备的IP地址,包括如何定义和加载规则文件,以及如何检测IP对相应端口的访问是否达到规定阈值
    2024-12-12
  • GO 使用Webhook 实现github 自动化部署的方法

    GO 使用Webhook 实现github 自动化部署的方法

    这篇文章主要介绍了GO 使用Webhook 实现github 自动化部署的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05
  • Golang 基于flag库实现一个简单命令行工具

    Golang 基于flag库实现一个简单命令行工具

    这篇文章主要介绍了Golang基于flag库实现一个简单命令行工具,Golang标准库中的flag库提供了解析命令行选项的能力,我们可以基于此来开发命令行工具,下文详细介绍。需要的小伙伴可以参考一下
    2022-08-08
  • go中结构体切片的实现示例

    go中结构体切片的实现示例

    Go语言中的结构体切片是一种结合了结构体和切片特点的数据结构,用于存储和操作多个结构体实例,具有一定的参考价值,感兴趣的可以了解一下
    2024-11-11

最新评论