go官方提供了http服务,但它的功能很简单。
这里介绍web开发中的一些问题,和web框架(echo)怎么解决这些问题 ,对于具体的echo的使用,可看官网
官网:
https://echo.labstack.com/
它的特点:
关于echo和gin的不同可以看下面的内容
对于我来说,有下面几点
官网提供了详细的例子和使用说明,具体可以看官网。
web开发可分为下面的几个阶段:
除此之外,还需要注意未知的错误发生,导致程序发生意外。
在开始之前先列举go原生和echo的两种方式
package main
import (
"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")
}
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")
}
路由主要是包括路由注册和路由查找
原生路由有下面的问题
原生的实现很简单,将路由放在map中,map的key就是path,v为handler,路由查找和注册的方法也很简单
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
h Handler
pattern string
}
所以在使用原生的时候,要实现上面的功能,需要自己在handler中区分Method,提取路径参数。
下面介绍echo的路由实现
Echo的路由器使用了压缩 radix tree
(基数树)数据结构来实现高效的路由匹配。基数树是一种用于高效存储和检索字符串的数据结构,它允许根据字符串的前缀进行快速匹配。
Radix树(也称为基数树或前缀树)是一种用于高效存储和检索字符串的数据结构。它通过共享公共前缀来(LCP longest common prefix)压缩存储空间,并提供快速的字符串匹配操作。
在Radix树中,每个节点代表一个字符串的前缀。树的根节点代表空字符串,而叶子节点表示完整的字符串。每个节点包含一个字符和指向子节点的指针或链接。
相比于其他树结构,Radix树的一个主要优势是它能够有效地存储具有相同前缀的字符串。这是通过将共同前缀存储在树的节点中来实现的,从而节省了存储空间。此外,Radix树还提供了高效的字符串匹配操作,因为它可以在O(k)的时间复杂度内完成字符串的插入、查找和删除操作,其中k是待操作的字符串的长度。
压缩型Radix树(Compact Radix Tree)是一种对传统Radix树进行了优化的变种。它通过合并具有相同前缀的节点来进一步减少存储空间的使用。
在传统的Radix树中,每个节点都包含一个字符和指向子节点的指针或链接。而在压缩型Radix树中,当一个节点只有一个子节点时,会将该节点与其子节点合并成一个节点。这样做可以减少节点的数量,从而减少了存储空间的使用。
压缩型Radix树的另一个优化是使用压缩路径(Compressed Path),即将具有相同前缀的节点路径合并成一个路径。这样可以进一步减少存储空间,并提高查找操作的效率。相比于传统的Radix树,压缩型Radix树在存储空间方面具有更好的性能。它可以在相同的功能下使用更少的内存。然而,由于合并节点和路径的操作,压缩型Radix树的插入和删除操作可能会更复杂一些,且可能需要更多的计算资源。
本质上来说是用空间换时间,压缩radix tree 的查找比radix tree 快。
Router struct {
tree *node // compressed redix tree root note
routes map[string]*Route // key是method,v是tree
echo *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 routes
isLeaf bool
// isHandler indicates that node has at least one handler registered to it
isHandler bool // 是否有handler
// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
notFoundHandler *routeMethod
}
kind uint8
children []*node
routeMethod struct {
ppath string
pnames []string
handler HandlerFunc
}
routeMethods struct {
connect *routeMethod // 方法和对应的method
delete *routeMethod
get *routeMethod
head *routeMethod
options *routeMethod
patch *routeMethod
post *routeMethod
propfind *routeMethod
put *routeMethod
trace *routeMethod
report *routeMethod
anyOther map[string]*routeMethod
allowHeader string
}
现在,我们有下面的路径
e := New()
r := e.router
r.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.Builder
name := 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.Builder
labelBuilder.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] = name
s,ok := kmap[note.parent]
if ok{
var edgeBuilder strings.Builder
edgeBuilder.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.router
r.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 {
// 返回了一个新的handler
return func(writer http.ResponseWriter, request *http.Request) {
// 调用原生的handler
handler(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 {
// 返回了一个新的handler
return func(writer http.ResponseWriter, request *http.Request) {
// 调用原生的handler
start := 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 {
// 返回了一个新的handler
return func(writer http.ResponseWriter, request *http.Request) {
// 调用原生的handler
start := 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中间件的相关代码,和上面的示例差不多。
源码: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的绑定源码。
这种绑定和校验的有两种方式
反射虽然说性能不太好,但web开发中是需要存在大量的参数校验的,性能瓶颈不一定是反射出现的,具体的情况还需要通过pprof来发现。
如果反射真的是问题,就可以采用第二种方法,对源代码做解析,生成校验代码,然后在代码中调用这些校验操作。比如利用下面的库
原生的操作比较粗暴,直接对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
到这里,结束了。