7天用Go从零实现Web框架Gee教程

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 7天用Go从零实现Web框架Gee教程

Day0 序言

设计一个框架

设计框架之前,需要知道为什么要使用框架,框架能解决什么问题,只有明白了这一点,才能设计出框架中的功能

net/heep简单的处理请求示例

func main() {
    http.HandleFunc("/", handler)
    http.HandleFunc("/count", counter)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

net/http提供了基础的Web功能,即监听端口,映射静态路由,解析HTTP报文。一些Web开发中简单的需求并不支持,需要手工实现。

  • 动态路由:例如hello/:name,hello/*这类的规则。
  • 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的handler中实现。
  • 模板:没有统一简化的HTML机制。

可以发现,当我们离开框架,使用基础库时,需要频繁手工处理的地方,就是框架的价值所在。

Day1 HTTP基础

标准库启动web服务

package main
import (
  "fmt"
  "log"
  "net/http"
)
func main() {
  http.HandleFunc("/", indexHandler)
  http.HandleFunc("/hello", helloHandler)
  log.Fatal(http.ListenAndServe(":9999", nil))
}
// handler echoes r.URL.Path
func indexHandler(w http.ResponseWriter, req *http.Request) {
  fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}
// handler echoes r.URL.Header
func helloHandler(w http.ResponseWriter, req *http.Request) {
  for k, v := range req.Header {
    fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
  }
}
$ curl http://localhost:9999/
URL.Path = "/"
$ curl http://localhost:9999/hello
Header["Accept"] = ["*/*"]
Header["User-Agent"] = ["curl/7.54.0"]

这里设置了2个路由,/和/hello,分配绑定indexHandler和helloHandler,根据不同的http请求会调用不同的处理函数。

main 函数的最后一行,是用来启动 Web 服务的,第一个参数是地址,:9999表示在 9999 端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。第二个参数,则是我们基于net/http标准库实现Web框架的入口。

实现http.Handler接口

package http
type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error

通过源码可以看到,Handler是一个接口类型,需要实现ServeHTTP方法,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。所以我们要自己写一个实现该接口的实例,让所有的HTTP请求由自己来写

package main
import (
  "fmt"
  "log"
  "net/http"
)
// Engine is the uni handler for all requests
type Engine struct{}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  switch req.URL.Path {
  case "/":
    fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
  case "/hello":
    for k, v := range req.Header {
      fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
    }
  default:
    fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
  }
}
func main() {
  engine := new(Engine)
  log.Fatal(http.ListenAndServe(":9999", engine))
}

我们定义了一个空的结构体Engine,实现了方法ServeHTTP。这个方法有2个参数:

  • 第一个参数是 ResponseWriter ,其实就是个网络套接字,通过w来写输入给客户端(C/S)
  • 第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;

在 main 函数中,我们给 ListenAndServe 方法的第二个参数传入了刚才创建的engine实例。至此,我们走出了实现Web框架的第一步,即,将所有的HTTP请求转向了我们自己的处理逻辑。还记得吗,在实现Engine之前,我们调用 http.HandleFunc 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如/hello。但是在实现Engine之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。

Gee框架的雏形

main.go和gee.go

main.go

package main
import (
  "Gee/gee"
  "fmt"
  "net/http"
)
func main() {
  r := gee.New()
  r.GET("/", func(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
  })
  r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
    for k, v := range req.Header {
      fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
    }
  })
  r.Run(":9999")
}

gee.go

package gee
import (
  "fmt"
  "net/http"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request)
// Engine implement the interface of ServeHTTP
type Engine struct {
  router map[string]HandlerFunc
}
// New is the constructor of gee.Engine
func New() *Engine {
  return &Engine{router: make(map[string]HandlerFunc)}
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
  key := method + "-" + pattern
  engine.router[key] = handler
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
  engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
  engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
  return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  key := req.Method + "-" + req.URL.Path
  if handler, ok := engine.router[key]; ok {
    handler(w, req)
  } else {
    fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
  }
}

解析

在mian.go中,使用New()创建 gee 的实例,使用 GET()方法添加路由,最后使用Run()启动Web服务。这里的路由,只是静态路由,不支持/hello/:name这样的动态路由,动态路由我们将在下一次实现。

gee.go是重头戏

我们先来看Run方法,发现里面实则是调用http.ListenAndServe(addr, engine)。我们会发现所有的http请求的处理首先都会进入ServeHTTP方法,在ServeHTTP方法内,发现通过engine.router这个map(key是请求方法+“-”+请求路径,例如GET-/、GET-/hello、POST-/hello,value是用户映射的处理方法)传入key,来调用不同的函数来处理不同的请求。Engine实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND 。

type Engine struct {
  router map[string]HandlerFunc
}

结构体内存了一张路由表,由addRoute方法添加kv,GET和POST这两个方法只是对addRoute的包装而已。

type HandlerFunc func(http.ResponseWriter, *http.Request)

HandlerFunc的具体实现由用户自己实现

r.GET("/", func(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
  })

运行测试

68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl http://localhost:9999/
URL.Path = "/"
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl http://localhost:9999/hello
Header["User-Agent"] = ["curl/7.75.0"]
Header["Accept"] = ["*/*"]
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl http://localhost:9999/world
404 NOT FOUND: /world

Day2 上下文

设计策略

  • 将路由(router)独立出来,方便之后增强。
  • 设计上下文(Context),封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持。
  • 动手写 Gee 框架的第二天,框架代码140行,新增代码约90行
  • 解耦!

使用效果main.go

package main
import (
  "Gee/gee"
  "net/http"
)
func main() {
  r := gee.New()
  r.GET("/", func(c *gee.Context) {
    c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
  })
  r.GET("/hello", func(c *gee.Context) {
    // expect /hello?name=geektutu
    c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
  })
  r.POST("/login", func(c *gee.Context) {
    c.JSON(http.StatusOK, gee.H{
      "username": c.PostForm("username"),
      "password": c.PostForm("password"),
    })
  })
  r.Run(":9999")
}
  • 从Handler的参数变成成了gee.Context,提供了查询Query/PostForm参数的功能。
  • gee.Context封装了HTML/String/JSON函数,能够快速构造HTTP响应。

router.go context.go gee.go

router.go

package gee
import (
  "log"
  "net/http"
)
type router struct {
  handlers map[string]HandlerFunc
}
func newRouter() *router {
  return &router{handlers: make(map[string]HandlerFunc)}
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
  log.Printf("Route %4s - %s", method, pattern)
  key := method + "-" + pattern
  r.handlers[key] = handler
}
func (r *router) handle(c *Context) {
  key := c.Method + "-" + c.Path
  if handler, ok := r.handlers[key]; ok {
    handler(c)
  } else {
    c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
  }
}

context.go

package gee
import (
  "encoding/json"
  "fmt"
  "net/http"
)
type H map[string]interface{}
type Context struct {
  // origin objects
  Writer http.ResponseWriter
  Req    *http.Request
  // request info
  Path   string
  Method string
  // response info
  StatusCode int
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
  return &Context{
    Writer: w,
    Req:    req,
    Path:   req.URL.Path,
    Method: req.Method,
  }
}
func (c *Context) PostForm(key string) string {
  return c.Req.FormValue(key)
}
func (c *Context) Query(key string) string {
  return c.Req.URL.Query().Get(key)
}
func (c *Context) Status(code int) {
  c.StatusCode = code
  c.Writer.WriteHeader(code)
}
func (c *Context) SetHeader(key string, value string) {
  c.Writer.Header().Set(key, value)
}
func (c *Context) String(code int, format string, values ...interface{}) {
  c.SetHeader("Content-Type", "text/plain")
  c.Status(code)
  c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Context) JSON(code int, obj interface{}) {
  c.SetHeader("Content-Type", "application/json")
  c.Status(code)
  encoder := json.NewEncoder(c.Writer)
  if err := encoder.Encode(obj); err != nil {
    http.Error(c.Writer, err.Error(), 500)
  }
}
func (c *Context) Data(code int, data []byte) {
  c.Status(code)
  c.Writer.Write(data)
}
func (c *Context) HTML(code int, html string) {
  c.SetHeader("Content-Type", "text/html")
  c.Status(code)
  c.Writer.Write([]byte(html))
}

gee.go

package gee
import "net/http"
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)
// Engine implement the interface of ServeHTTP
type Engine struct {
  router *router
}
// New is the constructor of gee.Engine
func New() *Engine {
  return &Engine{router: newRouter()}
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
  engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
  engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
  engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
  return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  c := newContext(w, req)
  engine.router.handle(c)
}

设计思想

context

对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

针对使用场景,封装*http.Request和http.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

router路由

将路由抽离出来,放到了一个新的文件中router.go,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context。

gee框架入口

router相关的代码独立后,gee.go简单了不少。最重要的还是通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求。相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。这个对象目前还非常简单,仅仅是包装了原来的两个参数,之后我们会慢慢地给Context插上翅膀。

总结

由gee的实例Engine开始,里面存着一个router,router里面存着一个map,k的Method + “-” + Path,而v则是对应的回调函数,那么这个回调的参数全部由context存储,在ServeHTTP中,每有一个新的请求过来,都会创建一个context,然后调用engine.router.handle,并把context传进去,给请求发送响应,由main中用户自己定义,但是都是调用context中api来实现的,所以context承载了整个会话的生命周期

测试

-i 参数可以显示 http response 的头信息,连同网页代码一起

68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl -i http://localhost:9999/
HTTP/1.1 200 OK
Content-Type: text/html
Date: Mon, 22 Nov 2021 08:10:31 GMT
Content-Length: 18
<h1>Hello Gee</h1>
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/hello?name=geektutu" -i
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Mon, 22 Nov 2021 08:10:41 GMT
Content-Length: 33
hello geektutu, you're at /hello
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234' -i
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 22 Nov 2021 08:10:48 GMT
Content-Length: 42
{"password":"1234","username":"geektutu"}
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/xxx" -i
HTTP/1.1 404 Not Found
Content-Type: text/plain
Date: Mon, 22 Nov 2021 08:10:56 GMT
Content-Length: 20
404 NOT FOUND: /xxx

Day3 前缀树路由

简介

使用 Trie 树实现动态路由(dynamic route)解析。

支持两种模式:name和*filepath,代码约150行。

接下来我们实现的动态路由具备以下两个功能。

  • 参数匹配:。例如 /p/:lang/doc,可以匹配 /p/c/doc 和 /p/go/doc。
  • 通配*。例如 /static/*filepath,可以匹配/static/fav.ico,也可以匹配/static/js/jQuery.js,这种模式常用于静态服务器,能够递归地匹配子路径。

简单来说,定义了 /p/:lang/doc 这个规则,当请求是/p/xxx/doc, xxx可以任意写,都会匹配到/p/:lang/doc的回调函数

而 /static/*filepath 就比较宽松了,请求前面只要是static/ ,无论是/static/xx还是/static/a/b/c/xx,都会匹配 /static/*filepath的回调函数

代码贴在下面,有注释,慢慢看

刚开始学习的时候不要死扣细节,想一下这样做的原因是什么,能做出什么效果来,就比如我上面写的简介

需要注意的是

n, params := r.getRoute(c.Method, c.Path)
c.Params = params

params是一个map,里面存这匹配或者通配对应的名字,例如

map[name:hahah]
map[filepath:css/geektutu.css]

代码阅读顺序建议

r.GET("/hello", func(c *gee.Context) {
  // expect /hello?name=geektutu
  c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) 
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) 
func (r *router) handle(c *Context)
n, params := r.getRoute(c.Method, c.Path)
func parsePattern(pattern string) []string

代码

main.go

package main
import (
  "Gee/gee"
  "net/http"
)
func main() {
  r := gee.New()
  r.GET("/", func(c *gee.Context) {
    c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
  })
  r.GET("/hello", func(c *gee.Context) {
    // expect /hello?name=geektutu
    c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
  })
  r.GET("/hello/:name/doc", func(c *gee.Context) {
    // expect /hello/geektutu
    c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
  })
  r.GET("/assets/*filepath", func(c *gee.Context) {
    c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")})
  })
  r.Run(":9999")
}


gee.go

package gee
import (
  "net/http"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)
// Engine implement the interface of ServeHTTP
type Engine struct {
  router *router
}
// New is the constructor of gee.Engine
func New() *Engine {
  return &Engine{router: newRouter()}
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
  engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
  engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
  engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
  return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  c := newContext(w, req)
  engine.router.handle(c)
}

trie.go

package gee
import (
  "fmt"
  "strings"
)
type node struct {
  pattern   string  // 是否一个完整的url,不是则为空字符串
  part      string  // URL块值,用/分割的部分,比如/abc/123,abc和123就是2个part
  children  []*node // 该节点下的子节点
  isWild    bool  // 是否模糊匹配,比如:filename或*filename这样的node就为true
}
func (n *node) String() string {
  return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}
// 找到匹配的子节点,场景是用在插入时使用,找到1个匹配的就立即返回
func (n *node) matchChild(part string) *node {
  // 遍历n节点的所有子节点,看是否能找到匹配的子节点,将其返回
  for _, child := range n.children {
    // 如果有模糊匹配的也会成功匹配上
    if child.part == part || child.isWild {
      return child
    }
  }
  return nil
}
// 一边匹配一边插入的方法
//r.roots[method].insert(pattern, parts, 0)
//parts = [] parts = [hello] parts = [hello :name]  parts = [assets *filepath]
//pattren= / ```/hello ```/hello/:name ```/assets/*filepath
func (n *node) insert(pattern string, parts []string, height int) {
  if len(parts) == height {
    // 如果已经匹配完了,那么将pattern赋值给该node,表示它是一个完整的url
    // 这是递归的终止条件
    n.pattern = pattern
    return
  }
  part := parts[height]
  child := n.matchChild(part)
  if child == nil {
    // 没有匹配上,那么进行生成,放到n节点的子列表中
    child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
    n.children = append(n.children, child)
  }
  // 接着插入下一个part节点
  child.insert(pattern, parts, height+1)
}
// 这个函数跟matchChild有点像,但它是返回所有匹配的子节点,原因是它的场景是用以查找
// 它必须返回所有可能的子节点来进行遍历查找
func (n *node) matchChildren(part string) []*node {
  nodes := make([]*node, 0)
  for _, child := range n.children {
    if child.part == part || child.isWild {
      nodes = append(nodes, child)
    }
  }
  return nodes
}
//n := root.search(searchParts, 0)
[]   [hello] [hello :name] [assets *filepath]
func (n *node) search(parts []string, height int) *node {
  if len(parts) == height || strings.HasPrefix(n.part, "*") {
    // 递归终止条件,找到末尾了或者通配符
    if n.pattern == "" {
      // pattern为空字符串表示它不是一个完整的url,匹配失败
      return nil
    }
    return n
  }
  part := parts[height]
  // 获取所有可能的子路径
  children := n.matchChildren(part)
  for _, child := range children {
    // 对于每条路径接着用下一part去查找
    result := child.search(parts, height+1)
    if result != nil {
      // 找到了即返回
      return result
    }
  }
  return nil
}
// 查找所有完整的url,保存到列表中
func (n *node) travel(list *([]*node)) {
  if n.pattern != "" {
    // 递归终止条件
    *list = append(*list, n)
  }
  for _, child := range n.children {
    // 一层一层的递归找pattern是非空的节点
    child.travel(list)
  }
}

router.go

package gee
import (
  "net/http"
  "strings"
)
type router struct {
  roots    map[string]*node
  handlers map[string]HandlerFunc
}
func newRouter() *router {
  return &router{
    roots:    make(map[string]*node),
    handlers: make(map[string]HandlerFunc),
  }
}
// Only one * is allowed
func parsePattern(pattern string) []string {
  vs := strings.Split(pattern, "/")
  parts := make([]string, 0)
  for _, item := range vs {
    if item != "" {
      parts = append(parts, item)
      if item[0] == '*' {
        break
      }
    }
  }
  return parts
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
  //parts = [] parts = [hello] parts = [hello :name]  parts = [assets *filepath]
  parts := parsePattern(pattern)
  //key= GET-/ key= GET-/hello key= GET-/hello/:name key= GET-/assets/*filepath
  key := method + "-" + pattern
  //method=/  以/为root节点
  _, ok := r.roots[method]
  if !ok {
    r.roots[method] = &node{}
  }
  //pattren= / ```/hello ```/hello/:name ```/assets/*filepath
  r.roots[method].insert(pattern, parts, 0)
  //把key= GET-/ key= GET-/hello key= GET-/hello/:name key= GET-/assets/*filepath 与回调绑定
  r.handlers[key] = handler
}
func (r *router) getRoute(method string, path string) (*node, map[string]string) {
  //[]   [hello] [hello :name] [assets *filepath]
  searchParts := parsePattern(path)
  params := make(map[string]string)
  root, ok := r.roots[method]
  if !ok {
    return nil, nil
  }
  n := root.search(searchParts, 0)
  //找到匹配的节点了
  if n != nil {
    //path和parts已经不一样了,/hello/hahah 变成  [hello hahah]
    parts := parsePattern(n.pattern)
    for index, part := range parts {
      if part[0] == ':' {
        params[part[1:]] = searchParts[index]
      }
      if part[0] == '*' && len(part) > 1 {
        params[part[1:]] = strings.Join(searchParts[index:], "/")
        break
      }
    }
    return n, params
  }
  return nil, nil
}
func (r *router) getRoutes(method string) []*node {
  root, ok := r.roots[method]
  if !ok {
    return nil
  }
  nodes := make([]*node, 0)
  root.travel(&nodes)
  return nodes
}
func (r *router) handle(c *Context) {
  n, params := r.getRoute(c.Method, c.Path)
  if n != nil {
    c.Params = params
    key := c.Method + "-" + n.pattern
    r.handlers[key](c)
  } else {
    c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
  }
}

context.go

package gee
import (
  "encoding/json"
  "fmt"
  "net/http"
)
type H map[string]interface{}
type Context struct {
  // origin objects
  Writer http.ResponseWriter
  Req    *http.Request
  // request info
  Path   string
  Method string
  Params map[string]string
  // response info
  StatusCode int
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
  return &Context{
    Writer: w,
    Req:    req,
    Path:   req.URL.Path,
    Method: req.Method,
  }
}
func (c *Context) Param(key string) string {
  value, _ := c.Params[key]
  return value
}
func (c *Context) PostForm(key string) string {
  return c.Req.FormValue(key)
}
func (c *Context) Query(key string) string {
  return c.Req.URL.Query().Get(key)
}
func (c *Context) Status(code int) {
  c.StatusCode = code
  c.Writer.WriteHeader(code)
}
func (c *Context) SetHeader(key string, value string) {
  c.Writer.Header().Set(key, value)
}
func (c *Context) String(code int, format string, values ...interface{}) {
  c.SetHeader("Content-Type", "text/plain")
  c.Status(code)
  c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Context) JSON(code int, obj interface{}) {
  c.SetHeader("Content-Type", "application/json")
  c.Status(code)
  encoder := json.NewEncoder(c.Writer)
  if err := encoder.Encode(obj); err != nil {
    http.Error(c.Writer, err.Error(), 500)
  }
}
func (c *Context) Data(code int, data []byte) {
  c.Status(code)
  c.Writer.Write(data)
}
func (c *Context) HTML(code int, html string) {
  c.SetHeader("Content-Type", "text/html")
  c.Status(code)
  c.Writer.Write([]byte(html))
}

测试

68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/"
<h1>Hello Gee</h1>
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/hello"
hello , you're at /hello
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/hello/wxf/doc"
hello wxf, you're at /hello/wxf/doc
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/hello/wxf/err"
404 NOT FOUND: /hello/wxf/err
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/assets/nihao"
{"filepath":"nihao"}
68725@wxf MINGW64 ~/Desktop/7day/Gee
$ curl "http://localhost:9999/assets/nihao/niyehao"
{"filepath":"nihao/niyehao"}

Day4 分组控制

何为分组,分组的意义

分组控制(Group Control)是 Web 框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:

  • 以/post开头的路由匿名可访问。
  • 以/admin开头的路由需要鉴权。
  • 以/api开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。

大部分情况下的路由分组,是以相同的前缀来区分的。因此,我们今天实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如/post是一个分组,/post/a和/post/b可以是该分组下的子分组。作用在/post分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。

中间件可以给框架提供无限的扩展能力,应用在分组上,可以使得分组控制的收益更为明显,而不是共享相同的路由前缀这么简单。例如/admin的分组,可以应用鉴权中间件;/分组应用日志中间件,/是默认的最顶层的分组,也就意味着给所有的路由,即整个框架增加了记录日志的能力。

简单点来说,就是设计不同的前缀,再用不同的前缀写下面的代码,这样管理相同前缀的不同路由很方便

v1 := r.Group("/v1")
  {
    v1.GET("/", func(c *gee.Context) {
      c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
    })
    v1.GET("/hello", func(c *gee.Context) {
      // expect /hello?name=geektutu
      c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
    })
  }
  v2 := r.Group("/v2")
  {
    v2.GET("/hello/:name", func(c *gee.Context) {
      // expect /hello/geektutu
      c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
    })
    v2.POST("/login", func(c *gee.Context) {
      c.JSON(http.StatusOK, gee.H{
        "username": c.PostForm("username"),
        "password": c.PostForm("password"),
      })
    })
  }


2021/11/23 21:28:14 Route  GET - /v1/
2021/11/23 21:28:14 Route  GET - /v1/hello
2021/11/23 21:28:14 Route  GET - /v2/hello/:name
2021/11/23 21:28:14 Route POST - /v2/login

设计分组

一个 Group 对象需要具备哪些属性呢?首先是前缀(prefix),比如/,或者/api;

要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;

当然了,按照我们一开始的分析,中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。

如果Group对象需要直接映射路由规则的话,比如我们想在使用框架时,这么调用v2.GET,那么Group对象,还需要有访问Router的能力。


为了方便,我们可以在Group中,保存一个指针,指向Engine,整个框架的所有资源都是由Engine统一协调的,那么就可以通过Engine间接地访问各种接口了。所以,最后的 Group 的定义是这样的:

RouterGroup struct {
  prefix      string
  middlewares []HandlerFunc // support middleware
  parent      *RouterGroup  // support nesting
  engine      *Engine       // all groups share a Engine instance
}

进一步地抽象,将Engine作为最顶层的分组,也就是说Engine拥有RouterGroup所有的能力(组合,RouterGroup有的属性和方法,Engine全都有)。

Engine struct {
  *RouterGroup
  router *router
  groups []*RouterGroup // store all groups
}

进一步修改gee.go和main.go的代码

代码

main.go

package main
import (
  "Gee/gee"
  "net/http"
)
func main() {
  r := gee.New()
  r.GET("/index", func(c *gee.Context) {
    c.HTML(http.StatusOK, "<h1>Index Page</h1>")
  })
  v1 := r.Group("/v1")
  {
    v1.GET("/", func(c *gee.Context) {
      c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
    })
    v1.GET("/hello", func(c *gee.Context) {
      // expect /hello?name=geektutu
      c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
    })
  }
  v2 := r.Group("/v2")
  {
    v2.GET("/hello/:name", func(c *gee.Context) {
      // expect /hello/geektutu
      c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
    })
    v2.POST("/login", func(c *gee.Context) {
      c.JSON(http.StatusOK, gee.H{
        "username": c.PostForm("username"),
        "password": c.PostForm("password"),
      })
    })
  }
  r.Run(":9999")
}

gee.go

package gee
import (
  "log"
  "net/http"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)
// Engine implement the interface of ServeHTTP
type (
  RouterGroup struct {
    prefix      string
    middlewares []HandlerFunc // support middleware
    parent      *RouterGroup  // support nesting
    engine      *Engine       // all groups share a Engine instance
  }
  Engine struct {
    *RouterGroup
    router *router
    groups []*RouterGroup // store all groups
  }
)
// New is the constructor of gee.Engine
func New() *Engine {
  engine := &Engine{router: newRouter()}
  engine.RouterGroup = &RouterGroup{engine: engine}
  engine.groups = []*RouterGroup{engine.RouterGroup}
  return engine
}
// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
func (group *RouterGroup) Group(prefix string) *RouterGroup {
  engine := group.engine
  newGroup := &RouterGroup{
    prefix: group.prefix + prefix,
    parent: group,
    engine: engine,
  }
  engine.groups = append(engine.groups, newGroup)
  return newGroup
}
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
  pattern := group.prefix + comp
  log.Printf("Route %4s - %s", method, pattern)
  group.engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
  group.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
  group.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
  return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  c := newContext(w, req)
  engine.router.handle(c)
}

其他的代码没有改变,参考Day3即可

测试

68725@wxf MINGW64 ~/Desktop/7day/Gee
$go run main.go
2021/11/23 21:28:14 Route  GET - /index
2021/11/23 21:28:14 Route  GET - /v1/
2021/11/23 21:28:14 Route  GET - /v1/hello
2021/11/23 21:28:14 Route  GET - /v2/hello/:name
2021/11/23 21:28:14 Route POST - /v2/login
68725@wxf MINGW64 ~/Desktop/7day/Gee
$  curl "http://localhost:9999/"
404 NOT FOUND: /
68725@wxf MINGW64 ~/Desktop/7day/Gee
$  curl "http://localhost:9999/index"
<h1>Index Page</h1>
68725@wxf MINGW64 ~/Desktop/7day/Gee
$  curl "http://localhost:9999/v1"
<h1>Hello Gee</h1>
68725@wxf MINGW64 ~/Desktop/7day/Gee
$  curl "http://localhost:9999/v1/hello"
hello , you're at /v1/hello
68725@wxf MINGW64 ~/Desktop/7day/Gee
$  curl "http://localhost:9999/v1/hello/errTest"
404 NOT FOUND: /v1/hello/errTest
68725@wxf MINGW64 ~/Desktop/7day/Gee
$  curl "http://localhost:9999/v2/hello/acTest"
hello acTest, you're at /v2/hello/acTest
68725@wxf MINGW64 ~/Desktop/7day/Gee
$  curl "http://localhost:9999/v2/login" -X POST -d 'username=geektutu&password=1234'
{"password":"1234","username":"geektutu"}

Day5 中间件

中间件是什么

中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。

中间件设计

Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是Context对象。插入点是框架接收到请求初始化Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context进行二次加工。另外通过调用(*Context).Next()函数,中间件可等待用户自己定义的 Handler处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。

该图是中间件执行流程的重点

代码

main.go

package gee
import (
  "log"
  "net/http"
  "strings"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)
// Engine implement the interface of ServeHTTP
type (
  RouterGroup struct {
    prefix      string
    middlewares []HandlerFunc // support middleware
    parent      *RouterGroup  // support nesting
    engine      *Engine       // all groups share a Engine instance
  }
  Engine struct {
    *RouterGroup
    router *router
    groups []*RouterGroup // store all groups
  }
)
// New is the constructor of gee.Engine
func New() *Engine {
  engine := &Engine{router: newRouter()}
  engine.RouterGroup = &RouterGroup{engine: engine}
  engine.groups = []*RouterGroup{engine.RouterGroup}
  return engine
}
// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
func (group *RouterGroup) Group(prefix string) *RouterGroup {
  engine := group.engine
  newGroup := &RouterGroup{
    prefix: group.prefix + prefix,
    parent: group,
    engine: engine,
  }
  engine.groups = append(engine.groups, newGroup)
  return newGroup
}
// Use is defined to add middleware to the group
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
  group.middlewares = append(group.middlewares, middlewares...)
}
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
  pattern := group.prefix + comp
  log.Printf("Route %4s - %s", method, pattern)
  group.engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
  group.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
  group.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
  return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  var middlewares []HandlerFunc
  for _, group := range engine.groups {
    if strings.HasPrefix(req.URL.Path, group.prefix) {
      middlewares = append(middlewares, group.middlewares...)
    }
  }
  c := newContext(w, req)
  c.handlers = middlewares
  engine.router.handle(c)
}

logger.go

package gee
import (
  "log"
  "time"
)
func Logger() HandlerFunc {
  return func(c *Context) {
    // Start timer
    t := time.Now()
    // Process request
    c.Next()
    // Calculate resolution time
    log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
  }
}

gee.go

package gee
import (
  "log"
  "net/http"
  "strings"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)
// Engine implement the interface of ServeHTTP
type (
  RouterGroup struct {
    prefix      string
    middlewares []HandlerFunc // support middleware
    parent      *RouterGroup  // support nesting
    engine      *Engine       // all groups share a Engine instance
  }
  Engine struct {
    *RouterGroup
    router *router
    groups []*RouterGroup // store all groups
  }
)
// New is the constructor of gee.Engine
func New() *Engine {
  engine := &Engine{router: newRouter()}
  engine.RouterGroup = &RouterGroup{engine: engine}
  engine.groups = []*RouterGroup{engine.RouterGroup}
  return engine
}
// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
func (group *RouterGroup) Group(prefix string) *RouterGroup {
  engine := group.engine
  newGroup := &RouterGroup{
    prefix: group.prefix + prefix,
    parent: group,
    engine: engine,
  }
  engine.groups = append(engine.groups, newGroup)
  return newGroup
}
// Use is defined to add middleware to the group
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
  group.middlewares = append(group.middlewares, middlewares...)
}
func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
  pattern := group.prefix + comp
  log.Printf("Route %4s - %s", method, pattern)
  group.engine.router.addRoute(method, pattern, handler)
}
// GET defines the method to add GET request
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
  group.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
  group.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
  return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  var middlewares []HandlerFunc
  for _, group := range engine.groups {
    if strings.HasPrefix(req.URL.Path, group.prefix) {
      middlewares = append(middlewares, group.middlewares...)
    }
  }
  c := newContext(w, req)
  c.handlers = middlewares
  engine.router.handle(c)
}

router.go

package gee
import (
  "net/http"
  "strings"
)
type router struct {
  roots    map[string]*node
  handlers map[string]HandlerFunc
}
func newRouter() *router {
  return &router{
    roots:    make(map[string]*node),
    handlers: make(map[string]HandlerFunc),
  }
}
// Only one * is allowed
func parsePattern(pattern string) []string {
  vs := strings.Split(pattern, "/")
  parts := make([]string, 0)
  for _, item := range vs {
    if item != "" {
      parts = append(parts, item)
      if item[0] == '*' {
        break
      }
    }
  }
  return parts
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
  parts := parsePattern(pattern)
  key := method + "-" + pattern
  _, ok := r.roots[method]
  if !ok {
    r.roots[method] = &node{}
  }
  r.roots[method].insert(pattern, parts, 0)
  r.handlers[key] = handler
}
func (r *router) getRoute(method string, path string) (*node, map[string]string) {
  searchParts := parsePattern(path)
  params := make(map[string]string)
  root, ok := r.roots[method]
  if !ok {
    return nil, nil
  }
  n := root.search(searchParts, 0)
  if n != nil {
    parts := parsePattern(n.pattern)
    for index, part := range parts {
      if part[0] == ':' {
        params[part[1:]] = searchParts[index]
      }
      if part[0] == '*' && len(part) > 1 {
        params[part[1:]] = strings.Join(searchParts[index:], "/")
        break
      }
    }
    return n, params
  }
  return nil, nil
}
func (r *router) getRoutes(method string) []*node {
  root, ok := r.roots[method]
  if !ok {
    return nil
  }
  nodes := make([]*node, 0)
  root.travel(&nodes)
  return nodes
}
func (r *router) handle(c *Context) {
  n, params := r.getRoute(c.Method, c.Path)
  if n != nil {
    key := c.Method + "-" + n.pattern
    c.Params = params
    c.handlers = append(c.handlers, r.handlers[key])
  } else {
    c.handlers = append(c.handlers, func(c *Context) {
      c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
    })
  }
  c.Next()
}

context.go

package gee
import (
  "encoding/json"
  "fmt"
  "net/http"
)
type H map[string]interface{}
type Context struct {
  // origin objects
  Writer http.ResponseWriter
  Req    *http.Request
  // request info
  Path   string
  Method string
  Params map[string]string
  // response info
  StatusCode int
  // middleware
  handlers []HandlerFunc
  index    int
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
  return &Context{
    Path:   req.URL.Path,
    Method: req.Method,
    Req:    req,
    Writer: w,
    index:  -1,
  }
}
func (c *Context) Next() {
  c.index++
  s := len(c.handlers)
  for ; c.index < s; c.index++ {
    c.handlers[c.index](c)
  }
}
func (c *Context) Fail(code int, err string) {
  c.index = len(c.handlers)
  c.JSON(code, H{"message": err})
}
func (c *Context) Param(key string) string {
  value, _ := c.Params[key]
  return value
}
func (c *Context) PostForm(key string) string {
  return c.Req.FormValue(key)
}
func (c *Context) Query(key string) string {
  return c.Req.URL.Query().Get(key)
}
func (c *Context) Status(code int) {
  c.StatusCode = code
  c.Writer.WriteHeader(code)
}
func (c *Context) SetHeader(key string, value string) {
  c.Writer.Header().Set(key, value)
}
func (c *Context) String(code int, format string, values ...interface{}) {
  c.SetHeader("Content-Type", "text/plain")
  c.Status(code)
  c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
func (c *Context) JSON(code int, obj interface{}) {
  c.SetHeader("Content-Type", "application/json")
  c.Status(code)
  encoder := json.NewEncoder(c.Writer)
  if err := encoder.Encode(obj); err != nil {
    http.Error(c.Writer, err.Error(), 500)
  }
}
func (c *Context) Data(code int, data []byte) {
  c.Status(code)
  c.Writer.Write(data)
}
func (c *Context) HTML(code int, html string) {
  c.SetHeader("Content-Type", "text/html")
  c.Status(code)
  c.Writer.Write([]byte(html))
}


理解

中间件由分组实现了嵌套,目前理解为多层hook

测试

gee.Logger()即我们一开始就介绍的中间件,我们将这个中间件和框架代码放在了一起,作为框架默认提供的中间件。在这个例子中,我们将gee.Logger()应用在了全局,所有的路由都会应用该中间件。onlyForV2()是用来测试功能的,仅在v2对应的 Group 中应用了。

$ curl http://localhost:9999/
<h1>Hello Gee</h1>
$ go run main.go
2021/11/24 23:11:28 Route  GET - /
2021/11/24 23:11:28 Route  GET - /v2/hello/:name
2021/11/24 23:11:37 [200] / in 0s
$ curl http://localhost:9999/v2/hello/nihao
{"message":"Internal Server Error"}
$ go run main.go
2021/11/24 23:11:28 Route  GET - /
2021/11/24 23:11:28 Route  GET - /v2/hello/:name
2021/11/24 23:12:30 [500] /v2/hello/nihao in 0s for group v2
2021/11/24 23:12:30 [500] /v2/hello/nihao in 0s

web框架太难,优先级太低,以后有机会再学习

目录
相关文章
|
24天前
|
开发框架 前端开发 JavaScript
ASP.NET Web Pages - 教程
ASP.NET Web Pages 是一种用于创建动态网页的开发模式,采用HTML、CSS、JavaScript 和服务器脚本。本教程聚焦于Web Pages,介绍如何使用Razor语法结合服务器端代码与前端技术,以及利用WebMatrix工具进行开发。适合初学者入门ASP.NET。
|
2月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
60 4
|
1月前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
182 45
|
24天前
|
开发框架 Go 计算机视觉
纯Go语言开发人脸检测、瞳孔/眼睛定位与面部特征检测插件-助力GoFly快速开发框架
开发纯go插件的原因是因为目前 Go 生态系统中几乎所有现有的人脸检测解决方案都是纯粹绑定到一些 C/C++ 库,如 OpenCV 或 dlib,但通过 cgo 调用 C 程序会引入巨大的延迟,并在性能方面产生显著的权衡。此外,在许多情况下,在各种平台上安装 OpenCV 是很麻烦的。使用纯Go开发的插件不仅在开发时方便,在项目部署和项目维护也能省很多时间精力。
|
1月前
|
开发框架 搜索推荐 数据可视化
Django框架适合开发哪种类型的Web应用程序?
Django 框架凭借其强大的功能、稳定性和可扩展性,几乎可以适应各种类型的 Web 应用程序开发需求。无论是简单的网站还是复杂的企业级系统,Django 都能提供可靠的支持,帮助开发者快速构建高质量的应用。同时,其活跃的社区和丰富的资源也为开发者在项目实施过程中提供了有力的保障。
|
1月前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
38 2
|
1月前
|
Go API 数据库
Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
本文介绍了 Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
101 4
|
1月前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
84 1
|
2月前
|
网络安全 开发工具 数据安全/隐私保护
|
1月前
|
缓存 前端开发 中间件
go语言中Web框架
【10月更文挑战第22天】
48 4