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-Length
、Content-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)
}