GO web开发

news/2024/11/25 3:45:14/

go web开发

简介

go官方提供了http服务,但它的功能很简单。

这里介绍web开发中的一些问题,和web框架(echo)怎么解决这些问题 ,对于具体的echo的使用,可看官网

官网:

https://echo.labstack.com/

在这里插入图片描述

它的特点:

在这里插入图片描述

关于echo和gin的不同可以看下面的内容

  1. https://yuiltripathee.medium.com/go-gin-vs-echo-comparison-edf1536e2e25
  2. https://mattermost.com/blog/choosing-a-go-framework-gin-vs-echo/

对于我来说,有下面几点

  1. echo可以自定义错误处理,并且handler可以直接返回error,这很符合Go的规范。
  2. echo将middleware,handler分开,但gin并没有分开,需要在它的Context中调用next来进行下一步(这也是gin的一个设计特点,但这特点我不太喜欢 )
  3. https://github.com/deepmap/oapi-codegen 默认的服务器是echo(oapi-codegen可以将swagger文档转为对于的服务端代码和stub代码,并且会生成路由注册方法,生成对应的接口,生成model,提供校验。支持的服务器有chi,gin,echo,net/http )

官网提供了详细的例子和使用说明,具体可以看官网。

web开发可分为下面的几个阶段:

  1. 路由
  2. handler
  3. 调用handler的时候在前和后增加hook
  4. 参数查找和验证
  5. 返回值的处理

除此之外,还需要注意未知的错误发生,导致程序发生意外。

在开始之前先列举go原生和echo的两种方式

go原生

package mainimport ("fmt""net/http"
)func main() {// 创建一个路由器router := http.NewServeMux()// 注册路由处理函数router.HandleFunc("/", homeHandler)router.HandleFunc("/about", aboutHandler)// 启动HTTP服务器并指定端口port := ":8080"fmt.Printf("Server listening on port %s\n", port)http.ListenAndServe(port, router)
}// 处理根路径的请求
func homeHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Welcome to the home page!")
}// 处理关于页面的请求
func aboutHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "About page")
}

echo

func main() {e := echo.New()e.GET("/",homeHandlerEcho)e.GET("/about",aboutHandlerEcho)if err := e.Start(":8080"); err != nil {return }
}// 处理根路径的请求
func homeHandlerEcho(ctx echo.Context) error  {return ctx.String(http.StatusOK,"Welcome to the home page!")
}
func aboutHandlerEcho(ctx echo.Context) error  {return ctx.String(http.StatusOK,"About page")
}

路由

路由主要是包括路由注册和路由查找

原生路由有下面的问题

  1. 不能区分方法。
  2. 不支持路径参数

原生的实现很简单,将路由放在map中,map的key就是path,v为handler,路由查找和注册的方法也很简单

type ServeMux struct {mu    sync.RWMutexm     map[string]muxEntryes    []muxEntry // slice of entries sorted from longest to shortest.hosts bool       // whether any patterns contain hostnames
}type muxEntry struct {h       Handlerpattern string
}

所以在使用原生的时候,要实现上面的功能,需要自己在handler中区分Method,提取路径参数。

下面介绍echo的路由实现

Echo的路由器使用了压缩 radix tree(基数树)数据结构来实现高效的路由匹配。基数树是一种用于高效存储和检索字符串的数据结构,它允许根据字符串的前缀进行快速匹配。

radix tree

Radix树(也称为基数树或前缀树)是一种用于高效存储和检索字符串的数据结构。它通过共享公共前缀来(LCP longest common prefix)压缩存储空间,并提供快速的字符串匹配操作。

在Radix树中,每个节点代表一个字符串的前缀。树的根节点代表空字符串,而叶子节点表示完整的字符串。每个节点包含一个字符和指向子节点的指针或链接。

相比于其他树结构,Radix树的一个主要优势是它能够有效地存储具有相同前缀的字符串。这是通过将共同前缀存储在树的节点中来实现的,从而节省了存储空间。此外,Radix树还提供了高效的字符串匹配操作,因为它可以在O(k)的时间复杂度内完成字符串的插入、查找和删除操作,其中k是待操作的字符串的长度。

压缩radix tree

压缩型Radix树(Compact Radix Tree)是一种对传统Radix树进行了优化的变种。它通过合并具有相同前缀的节点来进一步减少存储空间的使用。

在传统的Radix树中,每个节点都包含一个字符和指向子节点的指针或链接。而在压缩型Radix树中,当一个节点只有一个子节点时,会将该节点与其子节点合并成一个节点。这样做可以减少节点的数量,从而减少了存储空间的使用。

压缩型Radix树的另一个优化是使用压缩路径(Compressed Path),即将具有相同前缀的节点路径合并成一个路径。这样可以进一步减少存储空间,并提高查找操作的效率。相比于传统的Radix树,压缩型Radix树在存储空间方面具有更好的性能。它可以在相同的功能下使用更少的内存。然而,由于合并节点和路径的操作,压缩型Radix树的插入和删除操作可能会更复杂一些,且可能需要更多的计算资源。

本质上来说是用空间换时间,压缩radix tree 的查找比radix tree 快。

Router结构体

Router struct {tree   *node // compressed redix tree root noteroutes map[string]*Route  // key是method,v是treeecho   *Echo}// 树的节点
node struct {// 节点类型,有三种 staticKind(正常的路径),paramKind(路径有参数),anyKind(通配符)kind           kind // label对应kind类型,也有三种,正常(正常路径的index为0的字符),路径有参数(:),通配符(*)label          byte prefix         string // 前缀parent         *node   // 父节点staticChildren children  // 静态的子节点(切片类型)也就是说,静态节点可以有多个子节点originalPath   string   // methods        *routeMethods   // 方法paramChild     *node   // paramKind 子节点(只有一个)anyChild       *node  // anyKind 子节点(只有一个)paramsCount    int    // 路径参数数量// isLeaf indicates that node does not have child routesisLeaf bool// isHandler indicates that node has at least one handler registered to itisHandler bool  // 是否有handler// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 casesnotFoundHandler *routeMethod
}
kind        uint8
children    []*node    
routeMethod struct {ppath   stringpnames  []stringhandler HandlerFunc
}
routeMethods struct {connect     *routeMethod     // 方法和对应的methoddelete      *routeMethodget         *routeMethodhead        *routeMethodoptions     *routeMethodpatch       *routeMethodpost        *routeMethodpropfind    *routeMethodput         *routeMethodtrace       *routeMethodreport      *routeMethodanyOther    map[string]*routeMethodallowHeader string
}

现在,我们有下面的路径

			e := New()r := e.routerr.Add(http.MethodGet, "/a/:b/c", handlerHelper("case", 1))r.Add(http.MethodGet, "/a/c/d", handlerHelper("case", 2))r.Add(http.MethodGet, "/a/d/c", handlerHelper("case", 2))r.Add(http.MethodGet, "/a/c/df", handlerHelper("case", 3))r.Add(http.MethodGet, "/a/*/f", handlerHelper("case", 4))r.Add(http.MethodGet, "/:e/c/f", handlerHelper("case", 5))r.Add(http.MethodGet, "/*", handlerHelper("case", 6))

构建的压缩形radix tree如下:

在这里插入图片描述

这个图是用graphviz来实现的

graphviz官网:https://www.graphviz.org/docs/attrs/label/

graphviz很好用,在图形化展示的时候很方便。

我用了下面的代码,生成了graphviz的语法,然后渲染了一下

注意:echo中node类型是不可导出的,没办法在包外直接获取它,遍历,所以,我在它的单元测试中做了下面的代码

他的原理是图的深度优先遍历,构建graphviz的语法

var kmap = make(map[*node]string) //key是节点的名字,v是节点
var kindMap  = map[kind]string{staticKind:"staticKind",paramKind:"paramKind",anyKind:"anyKind",
}var index int
var edgesStr []string
func deepInfo(note *node)  {if note == nil{return}index++var builder strings.Buildername := fmt.Sprintf("node%d", index)builder.WriteString(name)builder.WriteString("[")builder.WriteString("id=")builder.WriteString(strconv.Itoa(index))builder.WriteString(" ")var a = " \\n "var labelBuilder strings.BuilderlabelBuilder.WriteString("kind=")labelBuilder.WriteString(kindMap[note.kind])labelBuilder.WriteString(a)labelBuilder.WriteString("label=")labelBuilder.WriteByte(note.label)labelBuilder.WriteString(a)labelBuilder.WriteString("prefix=")labelBuilder.WriteString(note.prefix)labelBuilder.WriteString(a)labelBuilder.WriteString("paramsCount=")labelBuilder.WriteString(strconv.Itoa(note.paramsCount))labelBuilder.WriteString(a)labelBuilder.WriteString("isHandler=")labelBuilder.WriteString(strconv.FormatBool(note.isHandler))labelBuilder.WriteString(a)labelBuilder.WriteString("isLeaf=")labelBuilder.WriteString(strconv.FormatBool(note.isLeaf))labelBuilder.WriteString(a)builder.WriteString("label=")builder.WriteString("\"")builder.WriteString(labelBuilder.String())builder.WriteString("\"")builder.WriteString("]")fmt.Printf("%s \n",builder.String())kmap[note] = names,ok := kmap[note.parent]if ok{var edgeBuilder strings.BuilderedgeBuilder.WriteString(s)edgeBuilder.WriteString(" -> ")edgeBuilder.WriteString(name)edgeBuilder.WriteString("[")edgeBuilder.WriteString("label=")edgeBuilder.WriteString(edgeLabel(note,note.parent))edgeBuilder.WriteString("]")edgesStr = append(edgesStr,edgeBuilder.String() )}for _, child := range note.staticChildren {deepInfo(child)}deepInfo(note.anyChild)deepInfo(note.paramChild)}
func edgeLabel(cur *node,parent *node) string {for _, child := range parent.staticChildren {if cur == child{return "staticChildren"}}if cur == parent.paramChild{return "paramChild"}if cur == parent.anyChild{return "anyChild"}return ""
}// 使用如下:
func TestRouteMultiLevelBacktracking(t *testing.T) {e := New()r := e.routerr.Add(http.MethodGet, "/a/:b/c", handlerHelper("case", 1))r.Add(http.MethodGet, "/a/c/d", handlerHelper("case", 2))r.Add(http.MethodGet, "/a/d/c", handlerHelper("case", 2))r.Add(http.MethodGet, "/a/c/df", handlerHelper("case", 3))r.Add(http.MethodGet, "/a/*/f", handlerHelper("case", 4))r.Add(http.MethodGet, "/:e/c/f", handlerHelper("case", 5))r.Add(http.MethodGet, "/*", handlerHelper("case", 6))deepInfo(r.tree)println(strings.Join(edgesStr, "\n"))}

digraph的文件内容如下:

digraph {node [shape=box]layout=dot
node1[id=1 label="kind=staticKind \n label=/ \n prefix=/ \n paramsCount=0 \n isHandler=false \n isLeaf=false \n "]
node2[id=2 label="kind=staticKind \n label=a \n prefix=a/ \n paramsCount=0 \n isHandler=false \n isLeaf=false \n "]
node3[id=3 label="kind=staticKind \n label=c \n prefix=c/d \n paramsCount=0 \n isHandler=true \n isLeaf=false \n "]
node4[id=4 label="kind=staticKind \n label=f \n prefix=f \n paramsCount=0 \n isHandler=true \n isLeaf=true \n "]
node5[id=5 label="kind=staticKind \n label=d \n prefix=d/c \n paramsCount=0 \n isHandler=true \n isLeaf=true \n "]
node6[id=6 label="kind=anyKind \n label=* \n prefix=* \n paramsCount=1 \n isHandler=true \n isLeaf=false \n "]
node7[id=7 label="kind=staticKind \n label=/ \n prefix=/f \n paramsCount=1 \n isHandler=true \n isLeaf=true \n "]
node8[id=8 label="kind=paramKind \n label=: \n prefix=: \n paramsCount=0 \n isHandler=false \n isLeaf=false \n "]
node9[id=9 label="kind=staticKind \n label=/ \n prefix=/c \n paramsCount=1 \n isHandler=true \n isLeaf=true \n "]
node10[id=10 label="kind=anyKind \n label=* \n prefix=* \n paramsCount=1 \n isHandler=true \n isLeaf=true \n "]
node11[id=11 label="kind=paramKind \n label=: \n prefix=: \n paramsCount=0 \n isHandler=false \n isLeaf=false \n "]
node12[id=12 label="kind=staticKind \n label=/ \n prefix=/c/f \n paramsCount=1 \n isHandler=true \n isLeaf=true \n "]
node1 -> node2[label=staticChildren]
node2 -> node3[label=staticChildren]
node3 -> node4[label=staticChildren]
node2 -> node5[label=staticChildren]
node2 -> node6[label=anyChild]
node6 -> node7[label=staticChildren]
node2 -> node8[label=paramChild]
node8 -> node9[label=staticChildren]
node1 -> node10[label=anyChild]
node1 -> node11[label=paramChild]
node11 -> node12[label=staticChildren]
}

中间件

有这样的一个需求,现在要打印每个请求的耗费的时间。

显然需要一个地方来集中处理这种问题, 写在业务代码中不合适,我们用原生来讲一下middleware的原理

middleware就是将handler包了一层。比如下面的代码

// 它的函数签名和是 HandlerFunc
func homeHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Welcome to the home page!")
}
// 我们只需要在写一个方法,想办法在这个方法里面调用上面的handler方法就好了,并且返回值还是一个新的handler,这里用type就可以实现func timeMiddleware(handler http.HandlerFunc)  http.HandlerFunc {// 返回了一个新的handlerreturn func(writer http.ResponseWriter, request *http.Request) {// 调用原生的handlerhandler(writer,request)}
}

按照这个,我们开始改动代码

func main() {// 创建一个路由器router := http.NewServeMux()// 注册路由处理函数router.HandleFunc("/", timeMiddleware(homeHandler))router.HandleFunc("/about", timeMiddleware(aboutHandler))// 启动HTTP服务器并指定端口port := ":8080"fmt.Printf("Server listening on port %s\n", port)http.ListenAndServe(port, router)
}func timeMiddleware(handler http.HandlerFunc)  http.HandlerFunc {// 返回了一个新的handlerreturn func(writer http.ResponseWriter, request *http.Request) {// 调用原生的handlerstart := time.Now()handler(writer,request)println(time.Now().Sub(start).Seconds())}
}// 处理根路径的请求
func homeHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Welcome to the home page!")
}// 处理关于页面的请求
func aboutHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "About page")
}

按照这个原理我们还可以增加更多的middleware,如下

func requestLogTimeMiddleware(handler http.HandlerFunc)  http.HandlerFunc  {return func(writer http.ResponseWriter, request *http.Request) {s := make([]string, 0, 10)for k, valus := range request.Header {s = append(s, k,strings.Join(valus,","))}fmt.Printf("%v",s)handler(writer,request)}
}func main() {// 创建一个路由器router := http.NewServeMux()// 注册路由处理函数// 再次包了一层router.HandleFunc("/", requestLogTimeMiddleware(timeMiddleware(homeHandler)))router.HandleFunc("/about", timeMiddleware(aboutHandler))// 启动HTTP服务器并指定端口port := ":8080"fmt.Printf("Server listening on port %s\n", port)http.ListenAndServe(port, router)
}

但这样使用起来不是很方便,包裹起来看起来很丑陋,这样我们将middleware集中放在一个地方,然后统一应用,来减少注册时候的冗余。

// 定义类型
type middleware func(handlerFunc http.HandlerFunc) http.HandlerFunc// 中间件集中管理
var middlewareList = make([]middleware,0,10)// 添加中间件
func use(m middleware)  {middlewareList = append(middlewareList, m)
}// 包装handler
func applyMiddleware(handle http.HandlerFunc) http.HandlerFunc {for i := len(middlewareList)-1; i <=0 ; i-- {m := middlewareList[i]handle = m(handle)}return handle
}func main() {// 创建一个路由器router := http.NewServeMux()// 注册中间件use(requestLogTimeMiddleware)use(timeMiddleware)router.HandleFunc("/",applyMiddleware(homeHandler))router.HandleFunc("/about", applyMiddleware(aboutHandler))// 启动HTTP服务器并指定端口port := ":8080"fmt.Printf("Server listening on port %s\n", port)http.ListenAndServe(port, router)
}func requestLogTimeMiddleware(handler http.HandlerFunc)  http.HandlerFunc  {return func(writer http.ResponseWriter, request *http.Request) {s := make([]string, 0, 10)for k, valus := range request.Header {s = append(s, k,strings.Join(valus,","))}fmt.Printf("%v",s)handler(writer,request)}
}func timeMiddleware(handler http.HandlerFunc)  http.HandlerFunc {// 返回了一个新的handlerreturn func(writer http.ResponseWriter, request *http.Request) {// 调用原生的handlerstart := time.Now()handler(writer,request)fmt.Printf("cost: %v",time.Now().Sub(start).Seconds())}
}// 处理根路径的请求
func homeHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Welcome to the home page!")
}// 处理关于页面的请求
func aboutHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "About page")
}

applyMiddleware的解释如下:

倒序是因为要保持后添加的middleware先执行。可以理解为持续压栈,然后出栈执行

先添加的requestLogTimeMiddleware,之后是timeMiddleware,为了保证在真正执行的时候也可以按照这个顺序,我们需要先将timeMiddleware压栈,然后是requestLogTimeMiddleware,这样保证的handler第一个执行的就是requestLogTimeMiddleware

echo

回头看echo中间件的相关代码,和上面的示例差不多。

源码:https://github.com/labstack/echo/blob/master/echo.go#L473

echo提供了很多的中间件:https://github.com/labstack/echo/tree/master/middleware

参数查找和验证

查找

http.Request提供了读取各种content/type

echo对它们进行了封装,提供了bind操作来绑定数据

官网:https://echo.labstack.com/guide/binding/

这种原理就是反射操作,在结构体中提供了标签tag,通过反射来操作,获取对应的值

验证

echo并不提供验证的能力,需要引用第三方的库来操作

官网:https://echo.labstack.com/guide/request/#validate-data

和上面一样,在结构体中提供tag标签,通过反射来操作。

代码

func main() {// 定义绑定参数的名字和校验规则type Vocab struct {Id uint64 `param:"id" min:"1" max:"124"`}// query中的数据data := map[string]string{"id":"31232",   // 会触发校验}v := new(Vocab)// 绑定并且校验err := bindAndValidate(data, v)if err != nil {panic(err)}fmt.Printf("%+v",v)
}
func bindAndValidate(data map[string]string,v interface{}) error  {// 反射获取类型                                                           typ := reflect.TypeOf(v)if typ.Kind()!= reflect.Ptr{return errors.New("not ptr")}typ = typ.Elem()val := reflect.ValueOf(v).Elem()// 拿到字段for i := 0; i < typ.NumField(); i++ {typeField := typ.Field(i)structField := val.Field(i)if !structField.CanSet() {continue}// 获取tag,从map中获取数据,执行校验inputFieldName := typeField.Tag.Get("param")validateMin,_ := strconv.ParseUint(typeField.Tag.Get("min"),10,64)validateMax,_ := strconv.ParseUint(typeField.Tag.Get("max"),10,64)inputValue,err := strconv.ParseUint(data[inputFieldName],10,64)if err != nil {return errors.New("convert error")}if inputValue < validateMin || inputValue > validateMax{return errors.New("validate error")}structField.SetUint(inputValue)}return nil
}

为了说明原理,我这里写的很简单,如果给生产环境编写校验库的话,请务必做好功能的完善和容错

可以用在生成中的校验和绑定的功能肯定比我们上面复杂的多,可以看echo的绑定源码。

这种绑定和校验的有两种方式

  1. 反射操作
  2. 解析源码,预先生成好校验操作的代码,之后直接调用。

反射虽然说性能不太好,但web开发中是需要存在大量的参数校验的,性能瓶颈不一定是反射出现的,具体的情况还需要通过pprof来发现。

如果反射真的是问题,就可以采用第二种方法,对源代码做解析,生成校验代码,然后在代码中调用这些校验操作。比如利用下面的库

  • https://github.com/dave/dst
  • https://pkg.go.dev/go/parser

返回值的处理

原生的操作比较粗暴,直接对response做操作,echo提供了一些封装的方法

具体的可看官方

https://echo.labstack.com/guide/response/

这一点我是觉得gin做的比较好,提供了统一的render接口,通过不同的实现类来做渲染,echo在代码中一把梭哈。

gin:https://github.com/gin-gonic/gin/tree/master/render

在这里插入图片描述

echo:https://github.com/labstack/echo/blob/master/context.go

在这里插入图片描述


到这里,结束了。


http://www.ppmy.cn/news/694531.html

相关文章

人工智能产业链,是时候梳理一下了!

针对人工智能产业链&#xff0c;主要有三个核心&#xff1a;基础技术、人工智能技术及人工智能应用&#xff0c;本文将从主要从这三个方面进行梳理。 在基础技术方面&#xff0c;大数据管理和云计算技术得到广泛的运用&#xff0c;为人工智能技术的实现和人工智能应用的落地提供…

美图即将上市,是时候重新评估其用户价值了

众所周知&#xff0c;互联网公司最核心的资产是用户。在不同时期&#xff0c;大家对用户的关注点还是不一样&#xff0c;PC时期我们关注用户更关注流量&#xff1b;移动互联网时期&#xff0c;我们关注用户更关注活跃度&#xff0c;打开率、使用次数和使用时长&#xff0c;核心…

人工智能(8)---一文读懂人工智能产业链:基础技术、人工智能技术及人工智能应用

一文读懂人工智能产业链&#xff1a;基础技术、人工智能技术及人工智能应用 概要&#xff1a;针对人工智能产业链&#xff0c;主要有三个核心&#xff1a;基础技术、人工智能技术及人工智能应用&#xff0c;本文将从主要从这三个方面进行梳理 人工智能&#xff08;Artificial …

一文读懂人工智能产业链:基础技术、人工智能技术及人工智能应用

原文&#xff1a;https://blog.csdn.net/zhangbijun1230/article/details/82183281 概要&#xff1a;针对人工智能产业链&#xff0c;主要有三个核心&#xff1a;基础技术、人工智能技术及人工智能应用&#xff0c;本文将从主要从这三个方面进行梳理 人工智能&#xff08;Art…

手机线下渠道逆袭,这是趋势还是偶然现象?

每年这个时候&#xff0c;我总会为每个行业做一些年度的总结。而手机作为我最关注的行业之一&#xff0c;这一年所发生的几个事件节点让我记忆犹新。 其一是去年年底业界对手机行业的悲观预测并没有发生&#xff0c;根据赛诺的数据统计上半年国内手机销量达到2.5亿&#xff0c;…

机器学习的意义(转载)

下面是Geoff Hinton的《神经网络机器学习》课程视频的截图。 先别管这张图。为什么我要关注机器学习&#xff1f;在众多被冠以“机器学习”这样一个宣传噱头一样的名字的领域中&#xff0c;我最关注的是计算机视觉。对我而言&#xff0c;这一切的渊源都要追溯到2008年。 那时我…

瑞星的逆风局

文 | 谢幺 From 浅黑科技 那年&#xff0c;马化腾毕业后在一家小公司撸代码&#xff0c;业余炒股&#xff1b;马云为翻译社四处奔波揽业务&#xff0c;李彦宏在美国读硕士&#xff0c;刘强东在人大上大三&#xff0c;做兼职&#xff1b;周鸿祎创业未果&#xff0c;跑去西安读研…

云就是物联网时代的操作系统

最近我有一些关于物联网的思考&#xff0c;结合之前关于语音互联网操作系统的思考&#xff0c;我得出了以下几个结论。 第一、云平台就是物联网时代的操作系统&#xff0c;没有云的语音操作系统没有价值。 第二、在云平台这个操作系统之上&#xff0c;有着语音交互界面和图形交…