go 基于http库撸一个简易架子
http库
实现一个最简单的 http server
需要几行代码? 对于python
可能只需一行,对于 node
可能也要不了几行,那对于 golang
要几行?同样也要不了几行,这几乎是所有现代化高级语言的特性,提供了官方内置库,大大简化了开发工作量
http库就是做这个的,下面看瞎官方解释
Package http provides HTTP client and server implementations.
Get, Head, Post, and PostForm make HTTP (or HTTPS) requests:
最简单的http Server 实现
那说了这么多我们用代码实现下
新建 main.go
文件
package main // 声明 main 包,表明当前是一个可执行程序
import (
"fmt"
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "这是主页")
}
func main() { // main函数,是程序执行的入口
//路由注册
http.HandleFunc("/", home)
// 端口监听
http.ListenAndServe("127.0.0.1:8080", nil)
}
使用
go run main.go
启动,会监听8080端口
访问,这里用postman 测试
可以看到,已经可以访问了
http 库 —— Request 概览
- Body 和 GetBody
- URL
- Method
- Header
- Form
- ...
可以看到方法很多,具体方法的用法,有的猜方法名也能猜到,当然具体用法示例可以看官方文档
https://pkg.go.dev/net/http#Request
中文的
https://studygolang.com/pkgdoc
http 库 —— Request Body
Body:只能读取一次,意味着你读了别人 就不能读了;别人读了你就不能读了,并且再次读取也不会报错,只是什么也读不到
用下面这个例子可以测试出来
package main // 声明 main 包,表明当前是一个可执行程序
import (
"fmt"
"io"
"net/http"
)
func readBodyOnce(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Fprintf(w, "body 读取失败: %v", err)
// 记住要返回,不然就还会执行后面的代码
return
}
// 类型转换,将 []byte 转换为 string
fmt.Fprintf(w, "读取数据: %s \n", string(body))
// 尝试再次读取,哈也读不到,但是也不会报错
body, err = io.ReadAll(r.Body)
if err != nil {
// 不会进来这里
fmt.Fprintf(w, "再次读取数据出现错误: %v", err)
return
}
fmt.Fprintf(w, "再读一次数据: [%s] 读取数据长度 %d \n", string(body), len(body))
}
func main() { // main函数,是程序执行的入口
http.HandleFunc("/", readBodyOnce)
http.ListenAndServe("127.0.0.1:8080", nil)
}
http 库 —— Request Body - GetBody
- Body:只能读取一次,意味着你读了别人就不能读了;别人读了你就不能读了;
- GetBody:原则上是可以多次读取,但是在原生的http.Request里面,这个是 nil,就是没设置,框架一般都设置好了
- 在读取到 body 之后,我们就可以用于反序列化,比如说将json格式的字符串转化为一个对象等
package main // 声明 main 包,表明当前是一个可执行程序
import (
"fmt"
"net/http"
)
func getBodyIsNil(w http.ResponseWriter, r *http.Request) {
if r.GetBody == nil {
fmt.Fprint(w, "GetBody is nil \n")
} else {
fmt.Fprintf(w, "GetBody not nil \n")
}
}
func main() { // main函数,是程序执行的入口
http.HandleFunc("/", getBodyIsNil)
http.ListenAndServe("127.0.0.1:8080", nil)
}
http 库 —— Request Query
- 除了 Body,我们还可能传递参数的地方是 Query
- 也就是 http://xxx.com/your/path?id=123&b=456
- 所有的值都被解释为字符串,所以需要自己解析为数字等
例
package main // 声明 main 包,表明当前是一个可执行程序
import (
"fmt"
"net/http"
)
func queryParams(w http.ResponseWriter, r *http.Request) {
values := r.URL.Query()
fmt.Fprintf(w, "query is %v\n", values)
}
func main() { // main函数,是程序执行的入口
http.HandleFunc("/", queryParams)
http.ListenAndServe("127.0.0.1:8080", nil)
}
http 库 —— Request URL
包含路径方面的所有信息和一些很有用的 操作
需要注意的是
- URL 里面 Host 不一定有值
- r.Host 一般都有值,是Host这个header的值
- RawPath 也是不一定有
- Path肯定有
例
package main // 声明 main 包,表明当前是一个可执行程序
import (
"encoding/json"
"fmt"
"net/http"
)
func wholeUrl(w http.ResponseWriter, r *http.Request) {
data, _ := json.Marshal(r.URL)
fmt.Fprintf(w, string(data))
}
func main() { // main函数,是程序执行的入口
http.HandleFunc("/", wholeUrl)
http.ListenAndServe("127.0.0.1:8080", nil)
}
所以实际中记得自己输出来看一下,确认有没有
http 库 —— Request Header
-
header大体上是两类:
一类是http 预定义的
一类是自己定义的
-
Go 会自动将 header 名字转为标准名字(其实就是大小写调整
-
一般用 X 开头来表明是自己定义的,比如说 X-mycompanyyour=header
例
package main // 声明 main 包,表明当前是一个可执行程序
import (
"fmt"
"net/http"
)
func hander(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hender is %v \n", r.Header)
}
func main() { // main函数,是程序执行的入口
http.HandleFunc("/", hander)
http.ListenAndServe("127.0.0.1:8080", nil)
}
http 库 —— Form
- Form 和 ParseForm
- 要先调用 ParseForm
- 建议加上 Content-Type:application/x-www-formurlencoded
form-data、x-www-form-urlencoded 的区别
https://www.cnblogs.com/wbl001/p/12050751.html
form-data与x-www-form-urlencoded的区别
- multipart/form-data:可以上传文件或者键值对,最后都会转化为一条消息
- x-www-form-urlencoded:只能上传键值对,而且键值对都是通过&间隔分开的
post 方式这个方法只能 解析到 x-www-form-urlencoded
方式的,如果是get 方式 参数也可以解析到但是不建议这样用
例:
package main // 声明 main 包,表明当前是一个可执行程序
import (
"fmt"
"net/http"
)
func form(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "before parse form %v\n", r.Form)
err := r.ParseForm()
if err != nil {
fmt.Fprintf(w, "parse form error %v\n", r.Form)
}
fmt.Fprintf(w, "before parse form %v\n", r.Form)
}
func main() { // main函数,是程序执行的入口
http.HandleFunc("/", form)
http.ListenAndServe("127.0.0.1:8080", nil)
}
form-data 方式
x-www-form-urlencoded 方式
http 库使用:要点总结
- Body 和 GetBody:重点在于 Body 是一次性的,而 GetBody 默认情况下是没有,一般中间件会考虑帮你注入这个方法
- URL:注意 URL 里面的字段的含义可能并不如你期望的那样
- Form:记得调用前先用 ParseForm,别忘了请求里面加上 http 头
手撸一个简单的架子
初始化:基于http库,建立一个路由
package main
import (
"fmt"
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "这是主页")
}
func main() {
http.HandleFunc("/", home)
http.ListenAndServe("127.0.0.1:8080", nil)
}
至此路由创建完毕
抽象出路由注册和服务启动方法
package main
import (
"fmt"
"net/http"
)
// 抽象接口
type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)
Start(address string) error
}
// 结构体,相当于实现类
type sdkHttpServer struct {
Name string
}
// 实现接口方法
func (s *sdkHttpServer) Route(pattern string, handlerFunc http.HandlerFunc) {
http.HandleFunc(pattern, handlerFunc)
}
func (s *sdkHttpServer) Start(address string) error {
return http.ListenAndServe(address, nil)
}
// 暴露给外部使用,返回一个 Server 结构体(其实就是实例)
func NewSdkHttpServer(name string) Server {
// 由于引用类型,所以实际是返回 指针
return &sdkHttpServer{
Name: name,
}
}
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "这是主页")
}
func main() {
// 原生
// http.HandleFunc("/", home)
// http.ListenAndServe("127.0.0.1:8080", nil)
// 自己封装
server := NewSdkHttpServer("my-server")
// 注册路由
server.Route("/", home)
// 启动服务
server.Start("127.0.0.1:8080")
}
到这里我们封装完了,这里你们可能会说,你这不就是在原生的基础上套了一层改了个方法名吗?
别急还没完
基于原生实现简单的逻辑注册
我们首先基于原生的http库,实现一个简单注册,不涉及业务
原生注册
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
// 抽象接口
type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)
Start(address string) error
}
// 结构体,相当于实现类
type sdkHttpServer struct {
Name string
}
// 实现接口方法
func (s *sdkHttpServer) Route(pattern string, handlerFunc http.HandlerFunc) {
http.HandleFunc(pattern, handlerFunc)
}
func (s *sdkHttpServer) Start(address string) error {
return http.ListenAndServe(address, nil)
}
// 暴露给外部使用,返回一个 Server 结构体(其实就是实例)
func NewSdkHttpServer(name string) Server {
// 由于引用类型,所以实际是返回 指针
return &sdkHttpServer{
Name: name,
}
}
// 登录信息结构体
type signUpReq struct {
// 导出json字符串中key名字与结构体变量命名一致,
// 我们可以通过结构体定义时指定导出后key值;并且支持如果结构体中变量为空时不导出
Email string `json:"email"`
Password string `json:"password"`
ConfirmedPassword string `json:"confirmed_password"`
}
// 返回信息结构
type commonResponse struct {
BizCode int `json:"biz_code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// 简单注册
func SignUp(w http.ResponseWriter, r *http.Request) {
// 初始化
req := &signUpReq{}
// 读取body信息
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Fprintf(w, "read body failed: %v", err)
// 要返回掉,不然就会继续执行后面的代码
return
}
// 将body参数json信息 解析(反序列化)到req 结构体
err = json.Unmarshal(body, req)
if err != nil {
fmt.Fprintf(w, "Unmarshal failed: %v", err)
// 要返回掉,不然就会继续执行后面的代码
return
}
// 得到 req 请求参数结构体 ,处理业务逻辑,等等...
// 初始化返回信息结构体
resp := &commonResponse{
BizCode: 200,
Msg: "正常返回",
Data: 10086,
}
// 序列化结构体 转为 json 串
respJson, err := json.Marshal(resp)
if err != nil {
fmt.Fprintf(w, "Marshal failed: %v", err)
// 要返回掉,不然就会继续执行后面的代码
return
}
// 返回响应
fmt.Fprintf(w, string(respJson))
}
func main() {
// 自己封装
server := NewSdkHttpServer("my-server")
// 注册路由
server.Route("/SignUp", SignUp)
// 启动服务
server.Start("127.0.0.1:8080")
}
请求
127.0.0.1:8080/SignUp
{
"email": "makalo@qq.com",
"password": "password",
"confirmed_password": "confirmed_password"
}
响应
如下图正常返回
原生的缺点
如下图
你们有没有发现,这一块儿的代码,但凡你要读取和输出json,就得来一遍,那么有没有一个东西可以直接帮我自动序列化和反序列化呢?
如果接触过其他语言的框架,或者框架的话,你就会发现几乎所有的扩展,都有这个功能,比如node
koa
不管是数组还是对象,请求过来 参数自动解析 ,响应输出自动转json
这是啥呢?
就是 上下文 对象 Context
对应的就是一次http
请求
上下文对象 Context -> json 转换
实现
例:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
// 抽象接口
type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)
Start(address string) error
}
// 结构体,相当于实现类
type sdkHttpServer struct {
Name string
}
// 实现接口方法
func (s *sdkHttpServer) Route(pattern string, handlerFunc http.HandlerFunc) {
http.HandleFunc(pattern, handlerFunc)
}
func (s *sdkHttpServer) Start(address string) error {
return http.ListenAndServe(address, nil)
}
// 暴露给外部使用,返回一个 Server 结构体(其实就是实例)
func NewSdkHttpServer(name string) Server {
// 由于引用类型,所以实际是返回 指针
return &sdkHttpServer{
Name: name,
}
}
// 上下文结构体
type Context struct {
W http.ResponseWriter
R *http.Request
}
// 读取并反序列化
// req interface{} 接受任何类型
func (c *Context) ReadJson(req interface{}) error {
// 读取body信息
r := c.R
body, err := io.ReadAll(r.Body)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
// 将body参数json信息 解析(反序列化)到req 结构体
err = json.Unmarshal(body, req)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
return nil
}
// 响应
func (c *Context) WriteJson(code int, resp interface{}) error {
c.W.WriteHeader(code)
// 将响应结构体输入到浏览器
respJson, err := json.Marshal(resp)
if err != nil {
return err
}
_, err = c.W.Write(respJson)
return err
}
// 登录信息结构体
type signUpReq struct {
// 导出json字符串中key名字与结构体变量命名一致,
// 我们可以通过结构体定义时指定导出后key值;并且支持如果结构体中变量为空时不导出
Email string `json:"email"`
Password string `json:"password"`
ConfirmedPassword string `json:"confirmed_password"`
}
// 返回信息结构
type commonResponse struct {
BizCode int `json:"biz_code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// 简单注册
func SignUp(w http.ResponseWriter, r *http.Request) {
// 使用上下文对象
ctx := &Context{
W: w,
R: r,
}
// 初始化
req := &signUpReq{}
// 传入指针
err := ctx.ReadJson(req)
if err != nil {
fmt.Fprintf(w, "err: %v", err)
// 要返回掉,不然就会继续执行后面的代码
return
}
// 反序列化之后的 req 结构体
fmt.Printf("反序列化之后的 req: %#v \n", req)
// 得到 req 请求参数结构体 ,处理业务逻辑,等等...
// 初始化返回信息结构体
resp := &commonResponse{
BizCode: 200,
Msg: "正常返回",
Data: 10086,
}
err = ctx.WriteJson(http.StatusOK, resp)
if err != nil {
fmt.Fprintf(w, "写入响应失败: %v", err)
}
}
func main() {
// 自己封装
server := NewSdkHttpServer("my-server")
// 注册路由
server.Route("/SignUp", SignUp)
// 启动服务
server.Start("127.0.0.1:8080")
}
请求
127.0.0.1:8080/SignUp
{
"email": "makalo@qq.com",
"password": "password",
"confirmed_password": "confirmed_password"
}
响应
上下文对象 Context -> 常用状态码
通过上面代码,我们发现每次都要传入状态码,其实我们最常用状态码就两种
- 200
- 500
相关状态码查询文档
https://studygolang.com/pkgdoc
那么我们可以不用传入状态码
实现
例:
// 200
func (c *Context) OkJson(resp interface{}) error {
return c.WriteJson(http.StatusOK, resp)
}
// 500
func (c *Context) ErrorJson(resp interface{}) error {
return c.WriteJson(http.StatusInternalServerError, resp)
}
完整代码
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
// 抽象接口
type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)
Start(address string) error
}
// 结构体,相当于实现类
type sdkHttpServer struct {
Name string
}
// 实现接口方法
func (s *sdkHttpServer) Route(pattern string, handlerFunc http.HandlerFunc) {
http.HandleFunc(pattern, handlerFunc)
}
func (s *sdkHttpServer) Start(address string) error {
return http.ListenAndServe(address, nil)
}
// 暴露给外部使用,返回一个 Server 结构体(其实就是实例)
func NewSdkHttpServer(name string) Server {
// 由于引用类型,所以实际是返回 指针
return &sdkHttpServer{
Name: name,
}
}
// 上下文结构体
type Context struct {
W http.ResponseWriter
R *http.Request
}
// 读取并反序列化
// req interface{} 接受任何类型
func (c *Context) ReadJson(req interface{}) error {
// 读取body信息
r := c.R
body, err := io.ReadAll(r.Body)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
// 将body参数json信息 解析(反序列化)到req 结构体
err = json.Unmarshal(body, req)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
return nil
}
// 响应
func (c *Context) WriteJson(code int, resp interface{}) error {
c.W.WriteHeader(code)
// 将响应结构体输入到浏览器
respJson, err := json.Marshal(resp)
if err != nil {
return err
}
_, err = c.W.Write(respJson)
return err
}
// 200
func (c *Context) OkJson(resp interface{}) error {
return c.WriteJson(http.StatusOK, resp)
}
// 500
func (c *Context) ErrorJson(resp interface{}) error {
return c.WriteJson(http.StatusInternalServerError, resp)
}
// 登录信息结构体
type signUpReq struct {
// 导出json字符串中key名字与结构体变量命名一致,
// 我们可以通过结构体定义时指定导出后key值;并且支持如果结构体中变量为空时不导出
Email string `json:"email"`
Password string `json:"password"`
ConfirmedPassword string `json:"confirmed_password"`
}
// 返回信息结构
type commonResponse struct {
BizCode int `json:"biz_code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// 简单注册
func SignUp(w http.ResponseWriter, r *http.Request) {
// 使用上下文对象
ctx := &Context{
W: w,
R: r,
}
// 初始化
req := &signUpReq{}
// 传入指针
err := ctx.ReadJson(req)
if err != nil {
fmt.Fprintf(w, "err: %v", err)
// 要返回掉,不然就会继续执行后面的代码
return
}
// 反序列化之后的 req 结构体
fmt.Printf("反序列化之后的 req: %#v \n", req)
// 得到 req 请求参数结构体 ,处理业务逻辑,等等...
// 初始化返回信息结构体
resp := &commonResponse{
BizCode: 200,
Msg: "正常返回",
Data: 10086,
}
// err = ctx.WriteJson(http.StatusOK, resp)
// 不用传入状态码了
err = ctx.OkJson(resp)
if err != nil {
fmt.Fprintf(w, "写入响应失败: %v", err)
}
}
func main() {
// 自己封装
server := NewSdkHttpServer("my-server")
// 注册路由
server.Route("/SignUp", SignUp)
// 启动服务
server.Start("127.0.0.1:8080")
}
这样我们就不用传入状态码了
让框架去创建 Contex
如果有使用框架经验的大佬,都知道 Contex 这个对象一般都不是我们业务部分创建的
一般都是 web 框架来创建,有什么好处呢?
框架来创建context,就可以完全控制什么时候 创建,context可以有什么字段。作为设计者, 这种东西不能交给用户自由发挥。
我们希望 业务 部分是这样
// func SignUp(w http.ResponseWriter, r *http.Request) {
// 业务部分直接使用 Context
func SignUp(ctx *Context) {
实现
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
// 抽象接口
type Server interface {
Route(pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}
// 结构体,相当于实现类
type sdkHttpServer struct {
Name string
}
// 上下文结构体
type Context struct {
W http.ResponseWriter
R *http.Request
}
// 对外提供创建的方法
func NewContext(writer http.ResponseWriter, request *http.Request) *Context {
return &Context{
R: request,
W: writer,
}
}
// 实现接口方法
// 传入pattern 为路由,handlerFunc 参数为方法,handlerFunc 就是对应传入的方法名
func (s *sdkHttpServer) Route(pattern string, handlerFunc func(ctx *Context)) {
// 闭包创建
http.HandleFunc(pattern, func(writer http.ResponseWriter,
request *http.Request) {
ctx := NewContext(writer, request)
// 调用对应的 handlerFunc 方法, 将 ctx 传入
handlerFunc(ctx)
})
}
func (s *sdkHttpServer) Start(address string) error {
return http.ListenAndServe(address, nil)
}
// 暴露给外部使用,返回一个 Server 结构体(其实就是实例)
func NewSdkHttpServer(name string) Server {
// 由于引用类型,所以实际是返回 指针
return &sdkHttpServer{
Name: name,
}
}
// 读取并反序列化
// req interface{} 接受任何类型
func (c *Context) ReadJson(req interface{}) error {
// 读取body信息
r := c.R
body, err := io.ReadAll(r.Body)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
// 将body参数json信息 解析(反序列化)到req 结构体
err = json.Unmarshal(body, req)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
return nil
}
// 响应
func (c *Context) WriteJson(code int, resp interface{}) error {
c.W.WriteHeader(code)
// 将响应结构体输入到浏览器
respJson, err := json.Marshal(resp)
if err != nil {
return err
}
_, err = c.W.Write(respJson)
return err
}
// 200
func (c *Context) OkJson(resp interface{}) error {
return c.WriteJson(http.StatusOK, resp)
}
// 500
func (c *Context) ErrorJson(resp interface{}) error {
return c.WriteJson(http.StatusInternalServerError, resp)
}
// 登录信息结构体
type signUpReq struct {
// 导出json字符串中key名字与结构体变量命名一致,
// 我们可以通过结构体定义时指定导出后key值;并且支持如果结构体中变量为空时不导出
Email string `json:"email"`
Password string `json:"password"`
ConfirmedPassword string `json:"confirmed_password"`
}
// 返回信息结构
type commonResponse struct {
BizCode int `json:"biz_code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// 简单注册
// func SignUp(w http.ResponseWriter, r *http.Request) {
func SignUp(ctx *Context) {
// 初始化
req := &signUpReq{}
// 传入指针
err := ctx.ReadJson(req)
if err != nil {
fmt.Fprintf(ctx.W, "err: %v", err)
// 要返回掉,不然就会继续执行后面的代码
return
}
// 反序列化之后的 req 结构体
fmt.Printf("反序列化之后的 req: %#v \n", req)
// 得到 req 请求参数结构体 ,处理业务逻辑,等等...
// 初始化返回信息结构体
resp := &commonResponse{
BizCode: 200,
Msg: "正常返回",
Data: 10086,
}
// err = ctx.WriteJson(http.StatusOK, resp)
// 不用传入状态码了
err = ctx.OkJson(resp)
if err != nil {
fmt.Fprintf(ctx.W, "写入响应失败: %v", err)
}
}
func main() {
// 自己封装
server := NewSdkHttpServer("my-server")
// 注册路由
server.Route("/SignUp", SignUp)
// 启动服务
server.Start("127.0.0.1:8080")
}
支持 RESTFul API
有没有发现大部分框架都是支持,RESTFul API 呢?
那我们怎么实现?
定义
实现
这里我们简单理解,就是要让我们这个简单的框架支持 method 方法
那我们的路由接口必然要增加一个 method 参数
type Server interface {
Route(method string, pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}
对应的实现也要增加method
// 实现接口方法
// 传入method 为访问方式,pattern 为路由,handlerFunc 参数为方法,handlerFunc 就是对应传入的方法名
func (s *sdkHttpServer) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
// 闭包创建
http.HandleFunc(pattern, func(writer http.ResponseWriter,
request *http.Request) {
ctx := NewContext(writer, request)
// 调用对应的 handlerFunc 方法, 将 ctx 传入
handlerFunc(ctx)
})
}
Handler 抽象-自己实现路由路
我们先看看http.ListenAndServe
这个方法,之前我们一直传入的 是 nil
,这个参数到底有啥用?
// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
上面的解释说白了,就是第二个参数,让你传入一个 handler
类型,我们再看看handler
类型
// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it
// is not valid to use the ResponseWriter or read from the
// Request.Body after or concurrently with the completion of the
// ServeHTTP call.
//
// Depending on the HTTP client software, HTTP protocol version, and
// any intermediaries between the client and the Go server, it may not
// be possible to read from the Request.Body after writing to the
// ResponseWriter. Cautious handlers should read the Request.Body
// first, and then reply.
//
// Except for reading the body, handlers should not modify the
// provided Request.
//
// If ServeHTTP panics, the server (the caller of ServeHTTP) assumes
// that the effect of the panic was isolated to the active request.
// It recovers the panic, logs a stack trace to the server error log,
// and either closes the network connection or sends an HTTP/2
// RST_STREAM, depending on the HTTP protocol. To abort a handler so
// the client sees an interrupted response but the server doesn't log
// an error, panic with the value ErrAbortHandler.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
可以看到 Handler
类型是一个自定义类型,是个接口,我们只需实现 ServeHTTP
方法即可
有关 ServeHTTP
什么时候调用,可以看看这个:
https://www.jb51.cc/html/667637.html
https://lookcos.cn/archives/1214.html
ServeHTTP
你自己实现了,它会自己调用,相当于一个中间件
- 实现一个 Handler,它负责路由
- 如果找到了路由,就执行业务代码
- 找不到就返回 404
基于 map 实现路由
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
// 抽象接口
/*=============================== serve层抽象开始 ==============================*/
type Server interface {
Route(method string, pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}
// 结构体,相当于实现类
type sdkHttpServer struct {
// Name server 的名字
Name string
handler *HandlerBaseOnMap
}
/*=============================== 上下文层抽象开始 ==============================*/
// 上下文结构体
type Context struct {
W http.ResponseWriter
R *http.Request
}
// 读取并反序列化
// req interface{} 接受任何类型
func (c *Context) ReadJson(req interface{}) error {
// 读取body信息
r := c.R
body, err := io.ReadAll(r.Body)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
// 将body参数json信息 解析(反序列化)到req 结构体
err = json.Unmarshal(body, req)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
return nil
}
// 响应
func (c *Context) WriteJson(code int, resp interface{}) error {
c.W.WriteHeader(code)
// 将响应结构体输入到浏览器
respJson, err := json.Marshal(resp)
if err != nil {
return err
}
_, err = c.W.Write(respJson)
return err
}
// 200
func (c *Context) OkJson(resp interface{}) error {
return c.WriteJson(http.StatusOK, resp)
}
// 500
func (c *Context) ErrorJson(resp interface{}) error {
return c.WriteJson(http.StatusInternalServerError, resp)
}
// 对外提供创建的方法
func NewContext(writer http.ResponseWriter, request *http.Request) *Context {
return &Context{
R: request,
W: writer,
}
}
/*=============================== 上下文层抽象结束 ==============================*/
/*=============================== 路由层 map抽象开始 ==============================*/
// 路由结构体
type HandlerBaseOnMap struct {
// key 应该是 method + url
// value 应该是 与 path 对应的方法 如:signUp()
handlers map[string]func(ctx *Context)
}
// 生成key 方法
func (h *HandlerBaseOnMap) key(method string, pattern string) string {
// 将方法和路由拼接起来 最后如:GET#/index
key := method + "#" + pattern
// fmt.Printf("key => %s \n", key)
return key
}
// 初始化一个 HandlerBaseOnMap
func NewHandlers() *HandlerBaseOnMap {
return &HandlerBaseOnMap{handlers: make(map[string]func(ctx *Context))}
}
/*
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
该方法为 http.Handle 必须实现的方法
*/
func (h *HandlerBaseOnMap) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
// 获取key
key := h.key(request.Method, request.URL.Path)
// 通过 key 找到 handlers map 中对应要执行的方法
if handler, ok := h.handlers[key]; ok {
// 找到了路由,调用对应的 handler 并自动创建 NewContext 获取 上下文结构体
handler(NewContext(writer, request))
} else {
// 返回404
writer.WriteHeader(http.StatusNotFound)
writer.Write([]byte("Not Found"))
}
}
/*=============================== 路由层 map抽象结束 ==============================*/
// 实现接口方法
// 传入method 为访问方式,pattern 为路由,handlerFunc 参数为方法,handlerFunc 就是对应传入的方法名
func (s *sdkHttpServer) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
// 闭包创建
// 不够好,
/* http.HandleFunc(pattern, func(writer http.ResponseWriter,
request *http.Request) {
ctx := NewContext(writer, request)
// 调用对应的 handlerFunc 方法, 将 ctx 传入
handlerFunc(ctx)
}) */
// 基于 map 创建支持 method 方式的
// 生成key
key := s.handler.key(method, pattern)
// 插入map key 和对应 value, 也就是要执行的方法
s.handler.handlers[key] = handlerFunc
}
func (s *sdkHttpServer) Start(address string) error {
// http.Handle("/", s.handler)
// return http.ListenAndServe(address, nil)
// 启动并注册路由
return http.ListenAndServe(address, s.handler)
}
// 暴露给外部使用,返回一个 Server 结构体(其实就是实例)
func NewSdkHttpServer(name string) Server {
// 由于引用类型,所以实际是返回 指针
return &sdkHttpServer{
Name: name,
handler: NewHandlers(),
}
}
/*=============================== serve层抽象结束 ==============================*/
/*=============================== 业务部分 ==============================*/
// 登录信息结构体
type signUpReq struct {
// 导出json字符串中key名字与结构体变量命名一致,
// 我们可以通过结构体定义时指定导出后key值;并且支持如果结构体中变量为空时不导出
Email string `json:"email"`
Password string `json:"password"`
ConfirmedPassword string `json:"confirmed_password"`
}
// 返回信息结构
type commonResponse struct {
BizCode int `json:"biz_code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// 简单注册
// func SignUp(w http.ResponseWriter, r *http.Request) {
func SignUp(ctx *Context) {
// 初始化
req := &signUpReq{}
// 传入指针
err := ctx.ReadJson(req)
if err != nil {
fmt.Fprintf(ctx.W, "err: %v", err)
// 要返回掉,不然就会继续执行后面的代码
return
}
// 反序列化之后的 req 结构体
fmt.Printf("反序列化之后的 req: %#v \n", req)
// 得到 req 请求参数结构体 ,处理业务逻辑,等等...
// 初始化返回信息结构体
resp := &commonResponse{
BizCode: 200,
Msg: "正常返回",
Data: 10086,
}
// err = ctx.WriteJson(http.StatusOK, resp)
// 不用传入状态码了
err = ctx.OkJson(resp)
if err != nil {
fmt.Fprintf(ctx.W, "写入响应失败: %v", err)
}
}
func main() {
// 自己封装
server := NewSdkHttpServer("my-server")
// 注册路由
server.Route("POST", "/SignUp", SignUp)
// 启动服务
server.Start("127.0.0.1:8080")
}
缺点
这种实现方式是有缺点的:
这样基于Map实现一个负责简单路由查找的hanfler
就基本完成了,也实现了对RESTful API
的简单支持。但从上面代码可以看出:sdkHttpServer
和HandlerBaseOnMap
强耦合;Route
方法依赖于知道 Handlers
的内部细节;当想要扩展到利用路由树来实现时,sdkHttpServer
也要修改。
使用接口实现路由
上面的缺点,我们只需要使用接口实现,就可以将上面的缺点去掉
例:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
// 抽象接口
/*=============================== serve层抽象开始 ==============================*/
type Server interface {
RouteTable
Start(address string) error
}
// 结构体,相当于实现类
type sdkHttpServer struct {
// Name server 的名字
Name string
// 耦合太强
// handler *HandlerBaseOnMap
// 只依赖接口
handler MyHandler
}
// 定义自定的Handler接口
type MyHandler interface {
http.Handler
RouteTable
}
type RouteTable interface {
// route 设定一个路由,命中的会执行 handlerFunc
Route(method string, pattern string, handlerFunc func(ctx *Context))
}
/*=============================== 上下文层抽象开始 ==============================*/
// 上下文结构体
type Context struct {
W http.ResponseWriter
R *http.Request
}
// 读取并反序列化
// req interface{} 接受任何类型
func (c *Context) ReadJson(req interface{}) error {
// 读取body信息
r := c.R
body, err := io.ReadAll(r.Body)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
// 将body参数json信息 解析(反序列化)到req 结构体
err = json.Unmarshal(body, req)
if err != nil {
// 要返回掉,不然就会继续执行后面的代码
return err
}
return nil
}
// 响应
func (c *Context) WriteJson(code int, resp interface{}) error {
c.W.WriteHeader(code)
// 将响应结构体输入到浏览器
respJson, err := json.Marshal(resp)
if err != nil {
return err
}
_, err = c.W.Write(respJson)
return err
}
// 200
func (c *Context) OkJson(resp interface{}) error {
return c.WriteJson(http.StatusOK, resp)
}
// 500
func (c *Context) ErrorJson(resp interface{}) error {
return c.WriteJson(http.StatusInternalServerError, resp)
}
// 对外提供创建的方法
func NewContext(writer http.ResponseWriter, request *http.Request) *Context {
return &Context{
R: request,
W: writer,
}
}
/*=============================== 上下文层抽象结束 ==============================*/
/*=============================== 路由层 map抽象开始 ==============================*/
// 路由结构体
type HandlerBaseOnMap struct {
// key 应该是 method + url
// value 应该是 与 path 对应的方法 如:signUp()
handlers map[string]func(ctx *Context)
}
// 生成key 方法
func (h *HandlerBaseOnMap) key(method string, pattern string) string {
// 将方法和路由拼接起来 最后如:GET#/index
key := method + "#" + pattern
// fmt.Printf("key => %s \n", key)
return key
}
// 一种常用的GO 设计模式,
// 用来确保 HandlerBaseOnMap 肯定实现了 MyHandler 接口
var _ MyHandler = &HandlerBaseOnMap{}
// 初始化一个 HandlerBaseOnMap
func NewHandlers() *HandlerBaseOnMap {
return &HandlerBaseOnMap{handlers: make(map[string]func(ctx *Context))}
}
/*
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
该方法为 http.Handle 必须实现的方法
*/
// func (h *HandlerBaseOnMap) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
func (h *HandlerBaseOnMap) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
// 获取key
key := h.key(request.Method, request.URL.Path)
// 通过 key 找到 handlers map 中对应要执行的方法
if handler, ok := h.handlers[key]; ok {
// 找到了路由,调用对应的 handler 并自动创建 NewContext 获取 上下文结构体
handler(NewContext(writer, request))
} else {
// 返回404
writer.WriteHeader(http.StatusNotFound)
writer.Write([]byte("Not Found"))
}
}
// 实现了 RouteTable
func (h *HandlerBaseOnMap) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
key := h.key(method, pattern)
h.handlers[key] = handlerFunc
}
/*=============================== 路由层 map抽象结束 ==============================*/
// 实现接口方法
// 传入method 为访问方式,pattern 为路由,handlerFunc 参数为方法,handlerFunc 就是对应传入的方法名
func (s *sdkHttpServer) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
// 闭包创建
// 不够好,
/* http.HandleFunc(pattern, func(writer http.ResponseWriter,
request *http.Request) {
ctx := NewContext(writer, request)
// 调用对应的 handlerFunc 方法, 将 ctx 传入
handlerFunc(ctx)
}) */
/* // 基于 map 创建支持 method 方式的
// 生成key
key := s.handler.key(method, pattern)
// 插入map key 和对应 value, 也就是要执行的方法
s.handler.handlers[key] = handlerFunc */
// 基于接口 实际 调用的是 MyHandler , 委托的方式
s.handler.Route(method, pattern, handlerFunc)
}
func (s *sdkHttpServer) Start(address string) error {
// http.Handle("/", s.handler)
// return http.ListenAndServe(address, nil)
// 启动并注册路由
return http.ListenAndServe(address, s.handler)
}
// 暴露给外部使用,返回一个 Server 结构体(其实就是实例)
func NewSdkHttpServer(name string) Server {
// 由于引用类型,所以实际是返回 指针
return &sdkHttpServer{
Name: name,
handler: NewHandlers(),
}
}
/*=============================== serve层抽象结束 ==============================*/
/*=============================== 业务部分 ==============================*/
// 登录信息结构体
type signUpReq struct {
// 导出json字符串中key名字与结构体变量命名一致,
// 我们可以通过结构体定义时指定导出后key值;并且支持如果结构体中变量为空时不导出
Email string `json:"email"`
Password string `json:"password"`
ConfirmedPassword string `json:"confirmed_password"`
}
// 返回信息结构
type commonResponse struct {
BizCode int `json:"biz_code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// 简单注册
// func SignUp(w http.ResponseWriter, r *http.Request) {
func SignUp(ctx *Context) {
// 初始化
req := &signUpReq{}
// 传入指针
err := ctx.ReadJson(req)
if err != nil {
fmt.Fprintf(ctx.W, "err: %v", err)
// 要返回掉,不然就会继续执行后面的代码
return
}
// 反序列化之后的 req 结构体
fmt.Printf("反序列化之后的 req: %#v \n", req)
// 得到 req 请求参数结构体 ,处理业务逻辑,等等...
// 初始化返回信息结构体
resp := &commonResponse{
BizCode: 200,
Msg: "正常返回",
Data: 10086,
}
// err = ctx.WriteJson(http.StatusOK, resp)
// 不用传入状态码了
err = ctx.OkJson(resp)
if err != nil {
fmt.Fprintf(ctx.W, "写入响应失败: %v", err)
}
}
func main() {
// 自己封装
server := NewSdkHttpServer("my-server")
// 注册路由
server.Route("POST", "/SignUp", SignUp)
// 启动服务
server.Start("127.0.0.1:8080")
}
支持filter(中间件)
一般框架都支持,过滤器或者叫中间件,这也是AOP,面向切面,这里为 server 支持一些 AOP逻辑
- AOP:横向关注点,一般用于解决 Log, tracing,metric,熔断,限流等
- filter:我们希望请求在真正被处理之前能够经过一大堆的 filter
如
filter定义和模拟实现
package main
import (
"fmt"
"time"
)
type Filter func(ctx *Context)
type FilerBuilder func(next Filter) Filter
var _ FilerBuilder = MetricsFilterBuilder
func MetricsFilterBuilder(next Filter) Filter {
return func(ctx *Context) {
start := time.Now().Nanosecond()
next(ctx)
end := time.Now().Nanosecond()
fmt.Printf("用了 %d 纳秒", end-start)
}
}
为什么这么定义
- 考虑我们 metric filter
- 在请求执行前,我记录时间戳
- 在请求执行后,我记录时间戳
- 两者相减就是执行时间
- 同时保证线程安全
- 是两个filter 一前一后,那么要考虑线程安全问题
- 要考虑开始时间怎么传递给后一个filter
- 如果是一个filter,不采用这种方式,那么怎么把filter串起来?
github
这里不一一实现了,这里给出
github:https://github.com/makalochen/toy-web/tree/main/pkg
可以自行查看
当然,我也是跟着别人来的,我只是一步步理解稍作修改