自己写个简单易用的 Go Web 框架

先放个源码链接

github.com/dpwgc/easie...

里面有更详细的文档说明,觉得不错的可以Star一下。

为什么写这玩意

有时我要写一些很小的应用,比如爬虫后端、服务器脚本又或者是BFF服务,需要简单的Web服务。用gin感觉有点重,go.mod里会有很多用不到的依赖,直接用httprouter又很麻烦。所以想整个简单易用的Web框架。在实现基础能力的同时,顺便集成一些方便开发的功能(便捷的Websocket连接、SSE推送、自动绑定请求数据、自动序列化返回响应数据),并且尽量不引入第三方依赖,保持极简架构。


大概的架构图

技术选型

  • 底层HTTP路由:选择httprouter。
  • 将请求头/表单/URI参数绑定到结构体上:选择mapstructure。

框架目前的go.mod依赖情况:

go 复制代码
module github.com/dpwgc/easierweb

go 1.21.4

require (
   github.com/julienschmidt/httprouter v1.3.0
   github.com/mitchellh/mapstructure v1.5.0
   golang.org/x/net v0.17.0
   gopkg.in/yaml.v3 v3.0.1
)

基本用法

首先,运行go get,安装一下框架:

arduino 复制代码
go get github.com/dpwgc/easierweb

然后,编写路由代码,写起来和gin差不多,没有学习成本:

go 复制代码
package main

import (
   "github.com/dpwgc/easierweb"
   "github.com/dpwgc/easierweb/middlewares"
   "log"
   "net/http"
)

func main() {

   // 新建一个路由
   router := easierweb.New()
   
   // 使用日志中间件
   router.Use(middlewares.Logger())
   
   // GET接口
   router.GET("/hello/:name", hello)
   
   // 启动服务
   log.Fatal(router.Run(":80"))
}

// 请求处理函数
func hello(ctx *easierweb.Context) {

   // 获取URI Path里的name参数
   name := ctx.Path.Get("name")
   
   // 绑定URI Query参数到指定结构体上
   req := Request{}
   err := ctx.BindQuery(&req)
   if err != nil {
      panic(err)
   }
   
   fmt.Println("request data:", req)
   
   // 响应:hello {name}
   ctx.WriteString(http.StatusOK, "hello "+name)
}

// 请求结构体
type Request struct {
   Name   string `mapstructure:"name"`
   Mobile string `mapstructure:"mobile"`
}

进阶改造(将繁琐的流程自动化)

实际在使用的时候,每次获取请求数据都得调用一下Bind函数,然后响应数据时又得调用下Write函数再return,感觉还是不够简单,因此加了个通过反射自动绑定请求数据到结构体、自动序列化写入响应数据的功能。

灵感来源于 github.com/MikeLINGxZ/... 这是一个基于Gin的自动绑定请求数据+自动写入响应数据的库。

最终成品有点像Spring Boot那种接口方法样式:

go 复制代码
package main

import (
   "fmt"
   "github.com/dpwgc/easierweb"
   "github.com/dpwgc/easierweb/middlewares"
   "log"
)

func main() {
	
   // 还是老样子,先创建一个路由
   router := easierweb.New().Use(middlewares.Logger())
   
   // 使用EasyPOST函数(EasyXXX函数内置了自动绑定+自动写入),整一个POST提交接口
   router.EasyPOST("/submit", submit)
   
   // 启动服务
   log.Fatal(router.Run(":80"))
}

// 请求处理函数(自动绑定请求数据+自动写入响应数据)
// 这个请求处理函数和上文的基础版不大一样,除了上下文入参以外,还有个请求数据入参和响应数据返回体
// 当这个函数被调用时,会通过反射将POST请求的Body数据自动解析并绑定到req参数上(如果是GET请求就绑定Query参数)
// 当这个函数返回时,同样通过反射获取到返回的结构体,将其序列化成Json字符串后,写入响应
func submit(ctx *easierweb.Context, req Request) *Response {
	
   // 打印req的参数
   fmt.Printf("json body -> name: %s, mobile: %s \n", req.Name, req.Mobile)
   
   // 直接return结构体
   return &Response{Code: 1000, Msg:  "hello"}
}

type Request struct {
   Name   string `json:"name"`
   Mobile string `json:"mobile"`
}

type Response struct {
   Code int    `json:"code"`
   Msg  string `json:"msg"`
}

请求入参和响应返回值在反射赋值/取值时都做了动态化识别,可传可不传,不传req入参时就不会自动绑定请求数据,不返回Response且没有报错时就响应204,返回了error或者函数抛出异常了就返回400/500,Response可以是对象也可以是切片。

go 复制代码
func TestAPI(ctx *easierweb.Context, req Request) (*Response, error)
func TestAPI(ctx *easierweb.Context, req Request) *Response
func TestAPI(ctx *easierweb.Context, req Request) error
func TestAPI(ctx *easierweb.Context, req Request)
func TestAPI(ctx *easierweb.Context) (*Response, error)
func TestAPI(ctx *easierweb.Context) *Response
func TestAPI(ctx *easierweb.Context) error
func TestAPI(ctx *easierweb.Context)

实际开发中,不一定以Json格式来接收/呈现数据,所以留了个插件口子,可以让用户自定义自动绑定/写入数据的序列化与反序列化方式。

go 复制代码
// 使用XML格式来处理自动绑定和自动写入的数据
router := easierweb.New(easierweb.RouterOptions{
   RequestHandle: plugins.XMLRequestHandle(),
   ResponseHandle: plugins.XMLResponseHandle(),
})

追加功能(常用方法封装)

我写浏览器Js爬虫经常要用Websocket来连接后端并持续传输数据,还有些表单动态赋值的需求经常要用到SSE,那就顺便给框架整个websocket和SSE的快捷使用方式,同时把连接升级、跨域、SSE请求头设置、连接关闭之类的操作全部都封装到底层,不需要使用者操心这么多事情。

go 复制代码
func main() {

   // 新建路由
   router := easierweb.New()
   
   // 快速使用websocket
   router.WS("/demoWS/:id", DemoWS)

   // 快速使用SSE
   router.SSE("/demoSSE/:id", DemoSSE)

   // 启动
   log.Fatal(router.Run(":80"))
}
go 复制代码
// 快速使用websocket
func DemoWS(ctx *easierweb.Context) {

   // 处理websocket连接
   for {
   
      // 接收客户端消息
      msg, err := ctx.ReceiveString()
      if err != nil {
         panic(err)
      }

      fmt.Println("read websocket msg:", msg)

      // 发送消息给客户端
      err = ctx.SendString("hello")
      if err != nil {
         panic(err)
      }

      time.Sleep(1 * time.Second)

      // 函数返回时,websocket连接会自动关闭,不劳烦用户手动调close了
      return
   }
}
go 复制代码
// 快速使用SSE
func DemoSSE(ctx *easierweb.Context) {

   // 循环推送数据
   for i := 0; i < 5; i++ {

      // SSE推送数据, 用'\n\n'做间隔符
      err := ctx.Push(fmt.Sprintf("sse push %v", i), "\n\n")
      if err != nil {
         panic(err)
      }

      time.Sleep(1 * time.Second)
   }
}

结束

相关推荐
一个热爱生活的普通人23 分钟前
浅谈池化思想:以 database/sql 连接池为例
后端·go
快乐源泉28 分钟前
【设计模式】桥接,是设计模式?对,其实你用过
后端·设计模式·go
武斌41 分钟前
go-doudou CLI命令行工具详解
后端·微服务·go
Piper蛋窝2 小时前
Go 1.2 相比 Go1.1 有哪些值得注意的改动?
后端·go
ling__wx3 小时前
go学习记录(第一天)
学习·go
chxii10 小时前
2.2goweb解析http请求信息
go
小刀飘逸18 小时前
部署go项目到linux服务器(简易版)
后端·go
我是前端小学生21 小时前
Go 语言中的 Channel 全面解析
go
IT杨秀才21 小时前
Go语言单元测试指南
后端·单元测试·go
Piper蛋窝1 天前
Go 1.1 相比 Go1.0 有哪些值得注意的改动?
go