大家好 我是寸铁👊
总结了一篇Go Web服务器(go net/http) 处理Get、Post请求的文章✨
喜欢的小伙伴可以点点关注 💝
前言
go http
请求如何编写简单的函数去拿到前端的请求(Get和Post) 服务器(后端)接收到请求后,又是怎么处理请求,再把响应返回给客户端?
操作步骤:
Step1:注册监听和服务的端口
一开始,我们需要注册监听和服务的端口
我们需要调用go中提供的net/http
这个函数包的ListenAndServe
函数
方式一:
http.ListenAndServe(":8080", nil)
注意:这里:
号要带上,不能只写8080
(踩坑经历)
等价于
http.ListenAndServe("127.0.0.1:8080", nil)
方式二:
考虑到很多时候,可能并不是直接访问本机127.0.0.1:8080
端口,这里可以写具体地址:端口号
(其实就是一个Socket)
如:
http.ListenAndServe("192.168.0.1:3306", nil)
在知道怎么操作后,我们来了解一下底层源码具体是怎么实现?了解后,便于更清楚的知道整体的流程!
ListenAndServe()
函数有两个参数,当前监听的端口号和事件处理器Handler。
事件处理器的Handler接口定义如下:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler
接口:Handler
是一个接口类型,声明了一个方法ServeHTTP
。这个接口规定了任何实现了ServeHTTP
方法的类型都可以被视为一个 HTTP 请求处理器。
ServeHTTP
方法:ServeHTTP
方法有两个参数,分别是ResponseWriter
和*Request
。
ResponseWriter
接口用于构建HTTP 响应
。它提供了一系列方法,允许你设置响应的状态码、头部信息以及响应主体内容。*Request
类型表示 HTTP 请求。它包含了所有关于客户端请求
的信息,比如请求方法、请求头、URL 等。PS:实现
Handle
接口不用写ServeHttp
方法,会比较方便。后面的代码都是用的封装好的
handler
处理器,帮我们写好ResponseWriter
和*Request
,只需专注业务逻辑即可。后续分析这两个参数的包含的具体信息
只要实现了这个接口,就可以实现自己的handler
处理器。Go语言在net/http
包中已经实现了这个接口的公共方法:
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
如果ListenAndServe()
传入的第一个参数地址为空,则服务器在启动后默认使用http://localhost:8080
地址进行访问;
如果这个函数传入的第二个参数为nil
,则服务器在启动后将使用默认的多路复用器DefaultServeMux
。
再来分析一下内层的server
代码
server结构体
server := &Server{Addr: addr, Handler: handler}
这是一种原生的写法,我们也可以自己根据这段代码来对server
进行更详细的定义。如下:
s := &http.Server{
Addr: ":8081", //地址
Handler: myHandler,//处理器
ReadTimeout: 20 * time.Second, //读的超时设置
WriteTimeout: 20 * time.Second, //写的超时设置
MaxHeaderBytes: 1 << 10, //传输的最大头部字节
}
net/http.Server
是 HTTP
服务器的主要结构体,用于控制 HTTP
服务器的行为。
其结构体定义为:
type Server struct { //服务器监听的地址和端口号,格式为 "host:port",例如 "127.0.0.1:8080" Addr string //HTTP 请求的处理器。对于每个收到的请求,服务器会将其路由到对应的处理器进行处理。通常使用 http.NewServeMux() 方法创建一个默认的多路复用器,并将其作为处理器。如果没有设置该字段,则使用 DefaultServeMux Handler Handler //一个布尔值,用于指示是否禁用 OPTIONS 方法的默认实现。如果该值为 true,则在收到 OPTIONS 请求时,服务器不会自动返回 Allow 头部,而是交给用户自行处理。默认为 false,即启用 OPTIONS 方法的默认实现 DisableGeneralOptionsHandler bool //HTTPS 服务器的 TLS 配置,用于控制 HTTPS 服务器的加密方式、证书、密钥等安全相关的参数 TLSConfig *tls.Config //HTTP 请求的读取超时时间。如果服务器在该时间内没有读取到完整的请求,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制 ReadTimeout time.Duration //HTTP 请求头部读取超时时间。如果服务器在该时间内没有完成头部读取,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制 ReadHeaderTimeout time.Duration //HTTP 响应的写入超时时间。如果服务器在该时间内没有完成对响应的写入操作,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制 WriteTimeout time.Duration //HTTP 连接的空闲超时时间。如果服务器在该时间内没有收到客户端的请求,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制 IdleTimeout time.Duration //HTTP 请求头部的最大大小。如果请求头部的大小超过该值,服务器就会关闭连接。该字段为 int 类型,默认为 1 << 20(1MB) MaxHeaderBytes int TLSNextProto map[string]func(*Server, *tls.Conn, Handler) //连接状态变化的回调函数,用于处理连接的打开、关闭等事件 ConnState func(net.Conn, ConnState) //错误日志的输出目标。如果该字段为 nil,则使用 log.New(os.Stderr, "", log.LstdFlags) 创建一个默认的日志输出目标 ErrorLog *log.Logger //所有 HTTP 请求的基础上下文。当处理器函数被调用时,会将请求的上下文从基础上下文派生出来。默认为 context.Background()。 BaseContext func(net.Listener) context.Context //连接上下文的回调函数,用于创建连接上下文。每个连接上下文都与一个客户端连接相关联。如果未设置该字段,则每个连接的上下文都是 BaseContext 的副本 ConnContext func(ctx context.Context, c net.Conn) context.Context //标志变量,用于表示服务器是否正在关闭。该变量在执行 Shutdown 方法时被设置为 true,用于避免新的连接被接受 inShutdown atomic.Bool //标志变量,用于控制服务器是否支持 HTTP keep-alive。如果该变量为 true,则服务器在每次响应完成后都会关闭连接,即不支持 keep-alive。如果该变量为 false,则服务器会根据请求头部中的 Connection 字段来决定是否支持 keep-alive。该变量在执行 Shutdown 方法时被设置为 true,用于关闭正在进行的 disableKeepAlives atomic.Bool // 一个 sync.Once 类型的值,用于确保在多线程环境下,NextProtoOnce 方法只被调用一次。NextProtoOnce 方法用于设置 Server.NextProto 字段 nextProtoOnce sync.Once // error 类型的值,用于记录 NextProto 方法的调用结果。该值在多个 goroutine 之间共享,用于检测 NextProto 方法是否成功 nextProtoErr error //互斥锁,用于保护 Server 结构体的字段。因为 Server 结构体可能被多个 goroutine 并发访问,所以需要使用互斥锁来确保它们的安全性 mu sync.Mutex //存储 HTTP 或 HTTPS 监听器的列表。每个监听器都是一个 net.Listener 接口类型的实例,用于接收客户端请求。当调用 Server.ListenAndServe() 或 Server.ListenAndServeTLS() 方法时,会为每个监听地址创建一个对应的监听器,并将其添加到该列表中 listeners map[*net.Listener]struct{} //表示当前处于活动状态的客户端连接的数量。该字段只是一个计数器,并不保证一定准确。该字段用于判断服务器是否处于繁忙状态,以及是否需要动态调整服务器的工作负载等 activeConn map[*conn]struct{} //在服务器关闭时执行的回调函数列表。当服务器调用 Server.Shutdown() 方法时,会依次执行该列表中的每个回调函数,并等待它们全部执行完毕。该字段可以用于在服务器关闭时释放资源、保存数据等操作 onShutdown []func() //表示所有监听器的组。该字段包含一个读写互斥锁 sync.RWMutex 和一个映射表 map[interface{}]struct{}。在监听器启动时,会将监听器地址作为键添加到映射表中。该字段主要用于实现优雅地关闭服务器。在服务器关闭时,会遍历所有监听器,逐个关闭它们,并等待所有连接关闭。如果在等待连接关闭时,有新的连接进来,服务器会先将新连接添加到 activeConn 字段中,并等待所有连接关闭后再退出。这样可以保证服务器在关闭过程中,不会丢失任何连接 listenerGroup sync.WaitGroup }
Step2.1: 调用HandleFunc
函数处理get请求
HandleFunc
函数
在上面的代码的基础上,开始调用常用的处理请求的函数HandleFunc
函数
在调用前,不妨先了解一下HandleFunc
函数的作用是什么?
HandleFunc
函数的作用是创建一个处理器
并将其注册到指定的路径上
。这个处理器会调用提供的函数
来处理请求
。这种方式非常方便,因为你可以直接使用一个函数来处理请求,而不必实现完整的http.Handler
接口。
有了上面的基础后,编写一个案例摸清HandleFunc
函数。
测试案例
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
}
func main() {
http.ListenAndServe(":8080", nil)
http.HandleFunc("/hello", helloHandler)
}
输出结果如下:
分析一下这段代码
在这个例子中
helloHandler
函数处理/hello
路径上的请求。通过http.HandleFunc
将这个函数helloHandler
注册到指定的路径,然后使用http.ListenAndServe
启动 HTTP 服务器。当有请求访问
/hello
路径时,将调用helloHandler
函数来处理请求。
有了HandleFunc
函数的基础后,下面正式进入处理客户端发送过来的get
请求
处理客户端的get请求
运行效果及逐行分析
代码
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
//get请求
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query() //接收到客户端发送的get请求的URL
fmt.Print(request.URL.Path) //后端显示打印很多相关的内容
//io包下提供的写方法,将服务端get到的id和name以字符串的形式写到客户端显示
io.WriteString(writer, "query:"+query.Get("id")+query.Get("name"))
})
http.ListenAndServe(":8080", nil)
}
运行上面这段代码后,我们来看一下运行的结果,从结果去不断梳理整个处理的过程。
运行结果如下
分析运行结果
逐行逐行详解:
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request)
- 这一行代码使用
http.HandleFunc
函数将一个匿名函数注册到根路径("/")
上,也就是处理默认的请求。这个匿名函数接受两个参数,writer
是用于写入响应的http.ResponseWriter
接口,而request
是表示客户端请求的http.Request
结构。query := request.URL.Query()
- 通过
request.URL.Query()
获取请求的 URL 中的查询参数
,并将其存储在query
变量中。查询参数通常是在 URL 中以?
后面的键值对形式出现的,例如:http://localhost:8080/?id=123&name=John
。fmt.Print(request.URL.Path)
- 在控制台上查看请求的路径信息
io.WriteString(writer, "query:"+query.Get("id")+query.Get("name"))
- 使用
io.WriteString
将包含查询参数的字符串写入响应
。它会将 “query:” 后面连接着id
和name
参数的值。总结:
当有请求访问根路径
("/")
时,打印请求的路径并返回包含查询参数的字符串响应。
request的api的具体信息
在演示了基本的get
请求结果后,服务端往客户端返回的响应(写一句话)
下面开始在控制台输出request
其他相关的具体信息
先来看一下可以调用request
的哪些具体属性(字段)
request
可调出的属性比较多,每个属性中对应的api方法也比较多。
这里演示常用的api,需要获取request的具体api方法可以直接查。
演示结果如下:
demo
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
//get请求
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query()
//设置request请求头部的字段 如设置name为hello
fmt.Println("request.URL.Path内容:", request.URL.Path) //可以打印很多相关的内容
fmt.Println("request.URL.Host内容:", request.URL.Host) //可以打印很多相关的内容
fmt.Println("request.URL.Fragment内容:", request.URL.Fragment)
fmt.Println("request.URL.Scheme内容:", request.URL.Scheme)
fmt.Println("request.URL.User内容:", request.URL.User)
fmt.Println("request.Header内容:", request.Header)
fmt.Println("request.Body内容:", request.Body)
fmt.Println("request.ContentLength内容为:", request.ContentLength)
fmt.Println("request.Method内容为:", request.Method)
fmt.Println("request.Close内容为:", request.Close)
io.WriteString(writer, "query:"+query.Get("id")+query.Get("name"))
})
http.ListenAndServe(":8080", nil)
}
处理器writer和request的具体信息
在前面的应用基础上,下面正式分析处理器的writer
和request
的来源和相关信息
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request)
- 先看
writer http.ResponseWriter
http.ResponseWriter
是一个接口类型:
// net/http/server.go
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
用于向客户端发送响应,实现了
ResponseWriter
接口的类型显然也实现了io.Writer
接口。所以在处理函数index
中,可以调用fmt.Fprintln()
向ResponseWriter
写入响应信息。这也很好的解释了前面代码调用
io.writeString
方法将返回的响应信息写入实现了ResponseWriter
接口的对象writer
,再调用方法返回响应信息。io.WriteString(writer, "query:"+query.Get("id")+query.Get("name"))
代码剖析
定义了一个接口 ResponseWriter,该接口规定了用于写入 HTTP 响应的方法。
接口中的每个方法如下:
Header() Header:
Header() 方法返回一个 Header 类型的值。Header 是一个映射(map),用于表示 HTTP 响应头。响应头包括一系列的键值对,每个键值对表示一个响应头字段和它的值。
Write([]byte) (int, error):
Write 方法接受一个字节切片([]byte)作为参数,并返回写入的字节数和可能的错误。这个方法用于将字节数据写入 HTTP 响应体。
WriteHeader(statusCode int):
WriteHeader 方法接受一个整数参数 statusCode,用于设置 HTTP 响应的状态码。HTTP 状态码是一个三位数的代码,用于表示服务器对请求的处理结果。例如,
200
表示成功,404
表示资源未找到,500
表示服务器内部错误等。编写一个 HTTP 处理器时,会接收到一个实现了 ResponseWriter 接口的对象
writer
,可以通过该对象来设置响应头、写入响应体以及设置状态码等信息。
补充:
在 Go 中,通常会使用http.ResponseWriter
类型的变量来表示实现了 ResponseWriter
接口的对象。例如,在你的 HTTP 处理器函数中,如下:
func MyHandler(writer http.ResponseWriter, r *http.Request) {
// 使用 writer来设置响应头、写入响应体等
}
这样,便可以通过 writer
对象来操作 HTTP 响应。
- 再看
request
的具体信息
net/http
中的 Request
结构体表示一个HTTP
请求,包含请求方法
、URL
、请求头
、请求体
等信息。信息主要在文件 net/http/request.go中。
结构体中字段的信息:
type Request struct { Method string //HTTP请求方法,如GET、POST等 URL *url.URL //HTTP请求的URL地址,是一个指向url.URL类型的指针。 Proto string //HTTP协议版本,如"HTTP/1.0"或者"HTTP/1.1" ProtoMajor int //HTTP协议的主版本号,整数类型。如1 ProtoMinor int //HTTP协议的次版本号,整数类型。如0 Header Header //HTTP请求头信息,是一个http.Header类型的映射,用于存储HTTP请求头。 Body io.ReadCloser //HTTP请求体,是一个io.ReadCloser类型的接口,表示一个可读可关闭的数据流。 GetBody func() (io.ReadCloser, error) //HTTP请求体获取函数 ContentLength int64 //HTTP请求体的长度,整数类型。 TransferEncoding []string //HTTP传输编码,如"chunked"等。 Close bool //表示在请求结束后是否关闭连接。 Host string //HTTP请求的主机名或IP地址,字符串类型。 Form url.Values //HTTP请求的表单数据,是一个url.Values类型的映射,用于存储表单字段和对应的值。 PostForm url.Values //HTTP POST请求的表单数据,同样是一个url.Values类型的映射。 MultipartForm *multipart.Form //HTTP请求的multipart表单数据,是一个multipart.Form类型的结构体。 Trailer Header //HTTP Trailer头信息,是一个http.Header类型的映射,用于存储Trailer头部字段和对应的值。 RemoteAddr string //请求客户端的地址。 RequestURI string //请求的URI,包括查询字符串。 TLS *tls.ConnectionState //如果请求是使用TLS加密的,则该字段存储TLS连接的状态信息。 Cancel <-chan struct{} //一个只读通道,用于在请求被取消时发送信号。 Response *Response //一个指向http.Response类型的指针,表示HTTP响应信息。 ctx context.Context //一个context.Context类型的上下文,用于控制请求的超时和取消。 }
Step2.2: 调用HandleFunc
函数处理post请求
http.HandleFunc("/user/add", func(writer http.ResponseWriter, request *http.Request) {
// ...
})
这里使用
http.HandleFunc
函数注册了一个处理 “/user/add” 路径的回调函数。当有请求访问 “/user/add” 路径时,Go 将调用这个函数来处理请求。这是注册的回调函数的签名,它接收两个参数,一个是
http.ResponseWriter
,用于构建 HTTP 响应,另一个是http.Request
,包含了客户端的 HTTP 请求信息。
var params map[string]string //创建map
decoder := json.NewDecoder(request.Body)
//调用NewDecoder() 创建body的json解码器
decoder.Decode(¶ms)
//json解码成map后存储到params变量
这段代码使用 Go 标准库的
encoding/json
包创建了一个 JSON 解码器decoder
,然后将 HTTP 请求的主体(body)中的 JSON 数据解码到params
变量中。params
是一个map[string]string
类型,用于存储 JSON 解析后的键值对。其实就是将
json
转换为map
io.WriteString(writer, "postjson:"+params["name"])
最后,通过
io.WriteString
向http.ResponseWriter
写入响应。这个响应是一个字符串,包含了 “postjson:” 和从 JSON 中提取的名为 “name” 的字段的值。
总结:
代码用于处理 POST 请求,解析请求主体中的 JSON
数据,并返回一个字符串响应,其中包含从 JSON
中提取的 "name"
字段的值。
运行结果如下:
http.HandleFunc("/user/del", func(writer http.ResponseWriter, request *http.Request)
这里使用
http.HandleFunc
函数注册了一个处理 “/user/del” 路径的回调函数。当有请求访问 “/user/del” 路径时,Go 将调用这个函数来处理请求。
接着是注册的回调函数的签名,它接收两个参数,一个是
http.ResponseWriter
,用于构建 HTTP 响应,另一个是http.Request
,包含了客户端的 HTTP 请求信息。
request.ParseForm()
这行代码调用了
ParseForm
方法,用于解析请求的表单数据(包括 URL 中的查询参数和请求体中的表单数据)。这是因为后面的代码使用了request.Form.Get("name")
来获取表单中名为 “name” 的字段的值。
io.WriteString(writer, "form:"+request.Form.Get("name"))
最后,通过
io.WriteString
向http.ResponseWriter
写入响应。这个响应是一个字符串,包含了 “form:” 和从表单中提取的名为 “name” 的字段的值。
总结
其实就是用于处理·"/user/del"
路径的请求,解析表单数据,并返回一个字符串响应,其中包含从表单中提取的 "name"
字段的值。
运行结果如下:
demo
package main
import (
"encoding/json"
"io"
"net/http"
)
func main() {
//Post请求
//添加用户 post client - server post entype postman JSON client ajax
//客户端向服务端发送请求
http.HandleFunc("/user/add", func(writer http.ResponseWriter, request *http.Request) {
//POST DATA ENTYPE JSON
var params map[string]string
decoder := json.NewDecoder(request.Body)
decoder.Decode(¶ms)
//传入地址 将传入的json转换为对应的map格式
io.WriteString(writer, "postjson:"+params["name"]) //通过map获取对应的值
})
http.HandleFunc("/user/del", func(writer http.ResponseWriter, request *http.Request) {
request.ParseForm() //不能直接使用Get 需要转换一下
io.WriteString(writer, "form:"+request.Form.Get("name"))
})
http.ListenAndServe(":8080", nil)
}