go 基于http库撸一个简易架子

http库

实现一个最简单的 http server需要几行代码? 对于python可能只需一行,对于 node 可能也要不了几行,那对于 golang 要几行?同样也要不了几行,这几乎是所有现代化高级语言的特性,提供了官方内置库,大大简化了开发工作量

http库就是做这个的,下面看瞎官方解释

https://pkg.go.dev/net/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 测试

image-20230130104808501

可以看到,已经可以访问了

http 库 —— Request 概览

  • Body 和 GetBody
  • URL
  • Method
  • Header
  • Form
  • ...

image-20230130105216135

可以看到方法很多,具体方法的用法,有的猜方法名也能猜到,当然具体用法示例可以看官方文档

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

image-20230130110937146

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

image-20230130113114710

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

image-20230130114350400

http 库 —— Request URL

包含路径方面的所有信息和一些很有用的 操作

image-20230130150724120

image-20230130150841014

需要注意的是

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

image-20230130151334215

所以实际中记得自己输出来看一下,确认有没有

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

image-20230130152052661

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 方式

image-20230130153142982

x-www-form-urlencoded 方式

image-20230130153429960

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

image-20230131142307828

至此路由创建完毕

抽象出路由注册和服务启动方法

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

image-20230131142307828

到这里我们封装完了,这里你们可能会说,你这不就是在原生的基础上套了一层改了个方法名吗?

别急还没完

基于原生实现简单的逻辑注册

我们首先基于原生的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"
}

响应

如下图正常返回

image-20230131152835136

原生的缺点

如下图

image-20230131153248437

你们有没有发现,这一块儿的代码,但凡你要读取和输出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"
}

响应

image-20230131164509364

image-20230131164617356

上下文对象 Context -> 常用状态码

通过上面代码,我们发现每次都要传入状态码,其实我们最常用状态码就两种

  • 200
  • 500

相关状态码查询文档

https://studygolang.com/pkgdoc

image-20230131170922374

那么我们可以不用传入状态码

实现

例:

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

image-20230131171253587

这样我们就不用传入状态码了

让框架去创建 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")
}

image-20230131181814860

支持 RESTFul API

有没有发现大部分框架都是支持,RESTFul API 呢?

那我们怎么实现?

定义

image-20230131182141114

实现

这里我们简单理解,就是要让我们这个简单的框架支持 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")
}

image-20230201164730679

image-20230201164956733

缺点

这种实现方式是有缺点的:

这样基于Map实现一个负责简单路由查找的hanfler就基本完成了,也实现了对RESTful API的简单支持。但从上面代码可以看出:sdkHttpServerHandlerBaseOnMap强耦合;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")
}

image-20230201191441363

image-20230201191455342

支持filter(中间件)

一般框架都支持,过滤器或者叫中间件,这也是AOP,面向切面,这里为 server 支持一些 AOP逻辑

  • AOP:横向关注点,一般用于解决 Log, tracing,metric,熔断,限流等
  • filter:我们希望请求在真正被处理之前能够经过一大堆的 filter

image-20230202181258335

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

可以自行查看

当然,我也是跟着别人来的,我只是一步步理解稍作修改

posted @ 2023-02-03 10:22  makalo  阅读(118)  评论(0编辑  收藏  举报