Go的标准库http原理解析

发布于:2025-07-08 ⋅ 阅读:(17) ⋅ 点赞:(0)

Go 语言的 net/http 包是标准库中用于构建 HTTP 客户端和服务器的核心模块,提供了简洁高效的 API 来处理 HTTP 协议,本章节就来看看Go语言原生的操作

一.基础知识介绍

一个web应用从客户端(浏览器)发起请求(request)到服务端(服务器),服务端从HTTP Request中提取请求路径(URL)并找到对应的处理程序(Handler)处理请求,最后返回结果。

这就是一个简单的访问过程,具体的知识涉及http的相关知识,所以呢,在此之前建议大家先自行查看一下计算机网络的相关知识,里面有涉及的内容,我会简单介绍一下

首先需要了解一些基本概念:

  • Request:用户请求的信息,用来解析用户的请求信息,包括post,get,Cookie,url等信息。
  • Response:服务器需要反馈给客户端的信息。
  • Conn:用户的每次请求链接。
  • Handle:处理请求和生成返回信息的处理逻辑。

Go实现web服务的流程:

  • 创建Listen Socket,监听指定的端口,等待客户端请求到来。
  • Listen Socket接受客户端的请求,得到Client Socket,接下来通过Client Socket与客户端通信。
  • 处理客户端请求,首先从Client Socket读取HTTP请求的协议头,如果是POST方法,还可能要读取客户端提交的数据,然后交给相应的handler处理请求,handler处理完,将数据通过Client Socket返回给客户端。

Go 语言中处理 HTTP 请求主要跟两个东西相关:ServeMux 和 Handler。

ServrMux 本质上是一个 HTTP 请求路由器(或者叫多路复用器,Multiplexor)。它把收到的请求与一组预先定义的 URL 路径列表做对比,然后在匹配到路径的时候调用关联的处理器(Handler)。

处理器(Handler)负责输出HTTP响应的头和正文。任何满足了http.Handler接口的对象都可作为一个处理器。通俗的说,对象只要有个如下签名的ServeHTTP方法即可

二.基本操作讲解

2.1 搭建服务端和客户端

服务端

既然是一个web应用,在知晓一个web应用是前后端,那么我们使用go语言要如何搭建这个服务端呢?在go语言的标准库里面提供了一个http包,用于处理http请求,从而搭建一个web服务。让我们看看一个最基本的服务搭建吧:

在运行下面的代码之后,大家可以去浏览器使用这个url来访问你写的这个代码

package main

import (
	"fmt"
	"net/http"
)

// 处理器函数
func handler(writer http.ResponseWriter, request *http.Request) {
	fmt.Fprintf(writer, "Hello World!")
}

func main() {
	//设置多路复用器:
	mux := http.NewServeMux()

	mux.HandleFunc("/", handler)

	//搭建服务器
	err := http.ListenAndServe(":8080", mux)
	//参数为1.网络地址和2.负责处理请求的处理器。
	//如果不采用的话就是一个mux,就是nil,就会使用默认的多路复用器,其他均为http.

	if err != nil {
		fmt.Printf("failed ,err:%v\n", err)
	}
	//上一句的代码还可以使用server结构(也就是服务器的搭建啦)
	/*   其实就是一个扩展内容,除了下面的参数,还有其他参数
	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
	server.ListenAndServe()
	*/
}

这里对其做一个简单的介绍:

多路复用器

在go语言web开发中,请求到达服务器时,多路复用器 (multiplexer)会对请求进行检查,并将请求重定向到正确的处理器进行处理。 处理器在接收到多路复用器转发的请求之后,会从请求中取出相应的信息,并对请求进行处理。

换句话说就是起到一个重定向的作用,转发给对应的处理器,一个url对应一个处理器,后续会介绍。

//设置多路复用器:
mux := http.NewServeMux()

http.NewServeMux()这个是自带的多路复用器,可以直接调用赋值。

他的本质,其实还是一个处理器。

实际上,他有默认的多路复用器,你也可以选择使用它默认的。

处理器函数

所谓的处理器函数,其实就是处理对应的请求的函数,比如GET,POST等等方法,是一个简单的逻辑处理函数。

他本质是一个接口,要求必须带上两个参数,让我来看一下

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

这里我们可能会好奇,它明明是一个接口,按理说我应该使用一个结构体去实现这个接口才对,为什么之前的例子并没有看见呢?让我们看看它正确的写法:

package main

import (
	"fmt"
	"net/http"
)

type hello struct{}

func (h *hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello World")
}

func main() {
	mux := http.NewServeMux()
	handler := &hello{}
	mux.Handle("/", handler) // 注册到自定义 mux

	server := &http.Server{
		Addr:    ":8080",
		Handler: mux, // 指定使用自定义 mux
	}
	server.ListenAndServe()
}

这样写,就会看到hello实现了这个接口,但是你会发现,这样写,岂不是太麻烦了,每一个处理器函数都要声明一个结构体再去实现方法,这显示不是开发者想要的,于是乎,就有了一种语法糖:

http.HandleFunc("/", handler)

回过头看之前的案例,你会发现你使用这个函数之后,就可以不用再声明这个结构体了。

启动我们的服务

服务监听

//搭建服务器
	err := http.ListenAndServe(":8080", mux)
	//参数为1.网络地址和2.负责处理请求的处理器。
	//如果不采用的话就是一个mux,就是nil,就会使用默认的多路复用器,其他均为http.

	if err != nil {
		fmt.Printf("failed ,err:%v\n", err)
	}

这里的启动时默认的服务器,你还可以为其更换server,添加你需要额参数

server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
server.ListenAndServe()

这里值得注意一点,这里的多路复用器mux,即使你不写也没有任何问题,因为它有默认的多路复用器

客户端

虽然我们实际开发中没人用go做为前端,但是http也提供了client的操作,我们来看看

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    // 创建请求
    req, err := http.NewRequest("GET", "https://httpbin.org/get", nil)
    if err != nil {
        panic(err)
    }

    // 发送请求
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close() // 确保关闭响应体

    // 读取响应内容
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    // 打印结果
    fmt.Println("Status Code:", resp.Status)
    fmt.Println("Response Body:", string(body))
}

2.2 请求和响应

2.1 小节实际上就类似对数据的接收,但是实际上真就只是接收,它可能会在http的请求头里面携带信息或者是在ul上携带信息,这都需要我们对其作出处理,这一小节就会介绍,除了处理,还会作出响应,返回给对应的客户端

如果对http有了解的同学,想必对http的请求和响应都很了解,他们都有类似的结构

我们先来说说请求,在go语言封装的结构体下,它有着http所具有的的字段,这里简单的介绍一下http报文的结构吧,一般是由报文首部和报文主体构成。

首先来看一下Request和Response的结构体:

type Request struct {
	Method string
	URL *url.URL
	ProtoMajor int    // 1
	ProtoMinor int    // 0
	Header Header
	Body io.ReadCloser
	GetBody func() (io.ReadCloser, error)
	ContentLength int64
	TransferEncoding []string
	Close bool
	Host string
	Form url.Values
	PostForm url.Values
	MultipartForm *multipart.Form
	Trailer Header
	RemoteAddr string
	RequestURI string
	TLS *tls.ConnectionState
	Cancel <-chan struct{}
	Response *Response
	Pattern string
	ctx context.Context
	pat         *pattern         
	matches     []string          
	otherValues map[string]string 
}

接下来我们会介绍一下里面的重点字段,看看他是如何从请求中提取数据的。

URL

Request的URL字段代表了报文请求行里面的内容。

URL字段是指向url.URL类型的一个指针,来看下它的结构体:

type URL struct {
	Scheme      string
	Opaque      string    // encoded opaque data
	User        *Userinfo // username and password information
	Host        string    // host or host:port (see Hostname and Port methods)
	Path        string    // path (relative paths may omit leading slash)
	RawPath     string    // encoded path hint (see EscapedPath method)
	OmitHost    bool      // do not emit empty host (authority)
	ForceQuery  bool      // append a query ('?') even if RawQuery is empty
	RawQuery    string    // encoded query values, without '?'
	Fragment    string    // fragment for references, without '#'
	RawFragment string    // encoded fragment hint (see EscapedFragment method)
}

URL的形式就是类似于我们的网址:

可以看到它的组成,这里就不在介绍组成了,我们直接看最后面的那一段信息,也就是请求参数

他其实属于是客户端传过来的信息,在http包下提供了对应的提取方法--RawQuery

通过这个函数可以获取对应URL上面的id=123&thread_id=456,但是他获取的直接就是id=123&thread_id=456的原始字符串。

有没有别的办法可以获取呢?当然是有的

可以通过Query函数,它返回的是一个map,可以通过key获取我们所要的value。

这里注意这个方法是URL结构体的方法,你要使用首先要从request里将请求行提取出来才可以哦

报文首部header

接下来看一下报文首段吧,实际上请求和响应的headers都是通过Header类型来描述的,他是一个map,用于描述http header里面的键值对。

他的类型如下,之所以是key并非只对应一个值,他可能有多个值。

map[string][]string

我们可以通过它获取对应http请求里面首部字段的消息,举个例子

和URL一个道理,首先要获取对应的Header才可以进行操作

那如何往请求头或者响应头添加我们想要设置的key-value呢?

也很简单,通过下面的一个Add函数和Set函数,两者也有着些许不同。

// 1. 直接设置 Header(会覆盖已存在的键)
req.Header.Set("User-Agent", "MyClient/1.0")

// 2. 添加 Header(允许多个值,不会覆盖)
req.Header.Add("Authorization", "Bearer abc123")
req.Header.Add("X-Custom-Header", "value1")
req.Header.Add("X-Custom-Header", "value2")

注意:

Header 键的规范化:Go 会自动将键名规范化为首字母大写的格式(如 content-type 会变为 Content-Type)。

如何获取header的所有信息嘞?

    if len(r.Header) > 0 {
        for k,v := range r.Header {
            fmt.Printf("%s=%s\n", k, v[0])
        }
    }

报文主体body

接下来看下主体,请求和响应的主体也是都是使用Body字段来表示的。

Contentlength方法是获取主题数据的字节长度,然后根据这个字节长度创建一个字节数组,然后调用在这个Read方法将主体数据读到字节数组内。

body里面是字符数组,要字符串转换才可以。

2.3 表单(也就是请求)

实际上数据的传递不单单只能通过url传递,还可以通过表单传递,也就是前端里面的,就好比我们登入系统,输入账号密码,然后提交,把这些内容传给后端,这些操作其实就不是通过url传递的,对于这些数据的提取,又该如何处理呢?

在http.request,他提供了三个数学,专门用于处理这些情况

http.request的三个字段Form、PostForm、MultipartForm:

  • Form:存储了post、put和get参数,在使用之前需要调用ParseForm方法。
  • PostForm:存储了post、put参数,在使用之前需要调用ParseForm方法。
  • MultipartForm:存储了包含了文件上传的表单的post参数,在使用前需要调用ParseMultipartForm方法。

Form字段

首先我们要知道的就是form,form这个获取的就是表单和url的查询参数,表单里面的值在靠前,而url的值靠后。很显然,这样肯定是多此一举

如果我们只想要表单里面的数据,我们就可以使用postform。

这里补充一点:我们提取表单里面的数据是需要先解析的,通过ParseForm或者ParseMultipartForm函数解析Request,然后才可以通过这三个字段获取。

来看一个案例吧:

对这个表单进行一个运行即可看到效果

PostForm字段

这个字段的使用和上一个字段一样,就是获取的内容不一样,该字段是只获取表单内容。

MultpartForm字段

他和PostForm的区别就是他返回的是一个map而不是一个map,这个struct里面有两个map:

  • key是string,value是[]string
  • key是string,value是文件

可以看出他也不是单纯的表单。来看一下他的效果,如果没有文件,那他就是一个空的

除了上述这些操作,还有其他的方式来获取表单的值:

FormValue和PostFormValue

案例:

r.PostFormValue()和r.PostForm区别是r.PostFormValue()只获取第一个参数,r.PostForm获取的是一个数组。

FormFile

我们来看下文件的传输,它实际上通过MultpartForm就可以实现,但是比较麻烦,不过肯定是有快捷方法的:

先来看前端的一个html代码,然后看后端时如何处理的:

这个方法当然也有它的缺点,就是他只能接收一张照片,它只返回指定key对应的第一个value,如果上传很多照片的化,可以使用上述注释掉的方法,通过数组的方式选择想要的照片。

2.4 JSON数据

实际上,我们并不会通过表单的形式这样转发,而是通过一种名为JSON的数据格式来转发前后端的信息,从而来规范传输形式,当然传输形式并不只有json,还有例如xml和proto等等,json则是比较常用的,接下来就来看看如何处理前端出过来的json数据吧:(如果对json不清楚的同学可以先了解一下,看下它的格式)

要想接收json格式的数据,go语言通过结体和tag来表示,各个字段,在通过 encoding/json 包实现json数据的绑定。

这里来看看我当时写的一个序列化和反序列化json工具吧,其实也很简单:

// DecodeJOSN 解码器
func DecodeJOSN(r *http.Request) (map[string]string, error) {
	// 初始化请求变量结构
	formData := make(map[string]string)
	// 调用json包的解析,解析请求body,绑定到formData上
	err := json.NewDecoder(r.Body).Decode(&formData)
    // 一定要注意关闭请求体
	defer r.Body.Close()
	if err != nil {
		return nil, err
	}
	return formData, nil
	//当然方法不止这一个,还有其他类似的方法。
}

当然一般来说,反序列化时针对结构体来操作,换一个例子,我们来看看

package main

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

// 定义接收 JSON 数据的结构体(字段需大写开头,与 JSON 的 key 对应)
type RequestData struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

// 处理 POST 请求的 Handler
func handlePostRequest(w http.ResponseWriter, r *http.Request) {
    // 1. 检查请求方法是否为 POST
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // 2. 设置响应头为 JSON 格式
    w.Header().Set("Content-Type", "application/json")

    // 3. 读取请求体(注意关闭)
    defer r.Body.Close()
    
    // 4. 解析 JSON 到结构体
    var data RequestData
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&data); err != nil {
        http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
        return
    }

    // 5. 处理数据(示例:直接返回解析后的数据)
    response := map[string]interface{}{
        "status":  "success",
        "message": fmt.Sprintf("Received: %s (%d years old), email: %s", data.Name, data.Age, data.Email),
    }
    
    // 6. 将响应编码为 JSON
    if err := json.NewEncoder(w).Encode(response); err != nil {
        http.Error(w, "Failed to encode response", http.StatusInternalServerError)
    }
}

func main() {
    http.HandleFunc("/api", handlePostRequest)
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

对应的我们处理返回的数据也要对其进行一个序列化,看下我之前写的一个简单的序列化函数

func EncodeJSON(w http.ResponseWriter, code int, message interface{}) {
	//先告诉前端是啥类型的数据
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code) //传入正确的状态码。
	//接着在处理错误
	encoder := json.NewEncoder(w)
	if err := encoder.Encode(message); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
	//这个编码器自带Write,会发送数据给前端
}

2.5 响应

来看一下响应的结构体

type ResponseWriter interface {
	Header() Header
	Write([]byte) (int, error)
	WriteHeader(statusCode int)
}

ResponseWriter 是一个接口,定义了三个方法:

  • Header():返回一个 Header 对象,可以通过它的 Set() 方法设置头部,注意最终返回的头部信息可能和你写进去的不完全相同,因为后续处理还可能修改头部的值(比如设置 Content-LengthContent-type 等操作)
  • Write(): 写 response 的主体部分,比如 html 或者 json 的内容就是放到这里的
  • WriteHeader():设置 status code,如果没有调用这个函数,默认设置为 http.StatusOK, 就是 200 状态码

我们先从简单的说起:

状态码

如果对http有所了解的同学,对状态码肯定不陌生,就是标识状态,返回给客户端目前的状态,常见的状态有200表示成功,404表示不存在等等。

WriteHeader()这个函数就是设置响应中的状态的,如果没有调用就默认是200

Write

再来看看Write这个函数,在之前就见到过,它可以返回一些数据,接下来看看他到底是干什么的吧

  • Write方法接收一个byte切片作为参数,然后把它写入到HTTP响应的Body里面
func EncodeJSON(w http.ResponseWriter, code int, message interface{}) {
	//先告诉前端是啥类型的数据
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code) //传入正确的状态码。
	//接着在处理错误
	encoder := json.NewEncoder(w)
	if err := encoder.Encode(message); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
	//这个编码器自带Write,会发送数据给前端
}

我写的这个序列化操作,其实这里也补充了一点,就是自带Write,不需要额外加了

Header

看看最后一个

  • Header方法返回headers的map,可以进行修改
  • 修改后的headers将会体现在返回给客户端的HTTP响应里面

其实说白了,就是给响应头加key-value呗,通过set函数

func (h *hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("test", "haha")
	fmt.Fprintf(w, "Hello World")
}

除此之外还支持其他操作,比如重定向,以及返回给前端照片等等

三.Cookie

cookie,session,还有token

3.1设置cookie

在net/http里面有定义的Cookie对象,可以先初始化一个cooki对象,在使用http.SetCookie(w, cookie)把cookie写入响应的头部。

package main

import (
    "net/http"
    "time"
)

func handler(w http.ResponseWriter, r *http.Request) {
    // 创建一个 Cookie
    cookie := &http.Cookie{
        Name:     "username",      // Cookie 名称
        Value:    "John Doe",      // Cookie 值,包含空格
        Path:     "/",             // Cookie 的路径
        Domain:   "example.com",   // Cookie 的域(可选)
        MaxAge:   3600,            // Cookie 的有效期,单位是秒(这里是1小时)
        HttpOnly: true,           // 设置 HttpOnly 标志,防止客户端脚本访问 Cookie
        Secure:   false,          // 是否仅通过 HTTPS 发送(可选,通常在生产环境中设置为 true)
        SameSite: http.SameSiteLaxMode, // 控制跨站请求中的 Cookie 发送
    }

    // 设置 Cookie 到响应头中
    http.SetCookie(w, cookie)

    // 也可以直接使用 w.Header().Set,但更推荐使用 http.SetCookie
    // w.Header().Set("Set-Cookie", cookie.String())

    // 响应客户端
    w.Write([]byte("Cookie has been set!"))
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

可以在浏览器使用f12查看网页的配置内容。

3.2读取cookie

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    // 尝试获取名为 "username" 的 Cookie
    cookie, err := r.Cookie("username")
    if err != nil {
        if err == http.ErrNoCookie {
            // 如果没有找到 Cookie
            http.Error(w, "Cookie not found", http.StatusNotFound)
            return
        }
        // 处理其他错误
        http.Error(w, "Error retrieving cookie", http.StatusInternalServerError)
        return
    }

    // 输出 Cookie 的值
    fmt.Fprintf(w, "Cookie value: %s\n", cookie.Value)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

四.http底层原理(后续了解即可)

这一节我们来深入http的底层,首先来看一下server

基于面向对象的思想,整个 http 服务端模块被封装在 Server 类当中

Handler 是 Server 中最核心的成员字段,实现了从请求路径 path 到具体处理函数 handler 的注册和映射能力.

type Server struct {
    // server地址
	Addr string
    
    // 这个字段其实就是最核心的,也就是路由处理器
	Handler Handler 
//....
}

在用户构造 Server 对象时,倘若其中的 Handler 字段未显式声明,则会取 net/http 包下的单例对象 DefaultServeMux(ServerMux 类型) 进行兜底.

这里的这个处理器就是所说的多路复用器。

这个的变化还不算是很大,但是对于ServeMux的实现却发生了很大的变化,从原本的map维护path到handler,到现在的前缀树,来看看吧。

type ServeMux struct {
	mu     sync.RWMutex
	tree   routingNode
	index  routingIndex
	mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}

之前的路由实现是基于 map[string]muxEntry 实现,通过字符串匹配和优先级规则处理路由。这种方式在复杂路由(如通配符、路径冲突)时性能较低。

再后来引入了routingNode的树形结构(类似前缀树或者Radix Tree),通过路径分解来提高匹配效率。

mux121 的作用就是为了兼容旧版本的逻辑。

type routingNode struct {
	pattern *pattern
	handler Handler
	children   mapping[string, *routingNode]
	multiChild *routingNode // child with multi wildcard
	emptyChild *routingNode // optimization: child with key ""
}

但是实际上,这个mapping还是一个map

type mapping[K comparable, V any] struct {
	s []entry[K, V] // for few pairs
	m map[K]V       // for many pairs
}

type entry[K comparable, V any] struct {
	key   K
	value V
}

4.1 Handler的注册

之前说过它其实是一个语法糖,就是对Handle的一个封装啦。可以自行往下查看,就不介绍了。

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if use121 {
		mux.mux121.handleFunc(pattern, handler)
	} else {
		mux.register(pattern, HandlerFunc(handler))
	}
}

主要看一下实现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)
}
func (s *Server) Serve(l net.Listener) error {
    //...
	ctx := context.WithValue(baseCtx, ServerContextKey, s)
	for {
		rw, err := l.Accept()
		//...
		connCtx := ctx
		//...
		c := s.newConn(rw)
		//...
		go c.serve(connCtx)
	}
}

conn.serve 是响应客户端连接的核心方法:

  • 从 conn 中读取到封装到 response 结构体,以及请求参数 http.Request
  • 调用 serveHandler.ServeHTTP 方法,根据请求的 path 为其分配 handler
  • 通过特定 handler 处理并响应请求
func (c *conn) serve(ctx context.Context) {
    // ...
    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)


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

在 serveHandler.ServeHTTP 方法中,会对 Handler 作判断,倘若其未声明,则取全局单例 DefaultServeMux 进行路由匹配,呼应了 http.HandleFunc 中的处理细节.

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    // ...
    handler.ServeHTTP(rw, req)
}


网站公告

今日签到

点亮在社区的每一天
去签到