go语言之认识web框架
这几天学习了一下geektutu大佬的7days-golang项目(只简单学习了一下web框架-Gee部分),在这里进行一些简单的总结。
先在这里放一张大致的流程图
day1-handler
base1
第一部分简单介绍了一下net/http库和http.Handler接口,同时搭建Gee框架的雏形。
首先go里面是已经内置了net/http库的,里面封装了http网络编程的基础的接口。我们实现Gee这个web框架就是基于net/http实现的,
代码语言:javascript复制 func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/hello", helloHandler)
log.Fatal(http.ListenAndServe(":9999", nil))
//用来启动web服务 :9999是端口 nil是一个预先声明的标识符,是一个变量,表示指针、通道、函数、接口、映射或切片类型的零值,并不是GO 的关键字。在这里表示使用标准库中的实例处理。这个参数是我们基于net/http标准库实现web框架的入口。
}
// handler echoes r.URL.Path
func indexHandler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path = %qn", req.URL.Path)
}
//这个函数,会将请求的url路径写入响应中,w http.ResponseWriter这个参数用来构建http响应, req *http.Request这个参数包含http请求的详细信息。客户端发起请求时,服务器在响应中返回请求的url路径
// handler echoes r.URL.Header
func helloHandler(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %qn", k, v)
}
}
//for循环遍历req.Header所有头部信息,对于每个头部键值对,使用fmt.Fprintf将其格式化并写入响应。
访问/
,响应是URL.Path = /
,而/hello
的响应则是请求头(header)中的键值对信息。
base2
先举个栗子
代码语言:javascript复制 package main
import (
"fmt"
"net/http"
)
// 定义一个类型 MyHandler 实现 Handler 接口
type MyHandler struct{}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
// 实例化 MyHandler
handler := &MyHandler{}
// 启动 HTTP 服务器
err := http.ListenAndServe(":8080", handler)
if err != nil {
fmt.Println("Error starting server:", err)
}
}
代码语言:javascript复制这个代码展示了如何定义一个实现了
http.Handler
接口的处理器,并使用http.ListenAndServe
启动一个 HTTP 服务器。在ServeHTTP
方法中,处理器简单地响应 "Hello, World!"。 ps:ServeHTTP
方法是http.Handler
接口的唯一方法只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了
type Engine struct{}
//定义了一个空的结构体,用来实现ServeHttp,
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/":
fmt.Fprintf(w, "URL.Path = %qn", req.URL.Path)
case "/hello":
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %qn", k, v)
}
default:
fmt.Fprintf(w, "404 NOT FOUND: %sn", req.URL)
}
}
func main() {
engine := new(Engine)
log.Fatal(http.ListenAndServe(":9999", engine))
}
和上面的栗子相近。
在实现
Engine
之前,我们调用 http.HandleFunc 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑(一个具体的路由对应一个规则)。比如/hello
。但是在实现Engine
之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。
base3
base3/go.mod
代码语言:javascript复制 module example
go 1.13
require gee v0.0.0
replace gee => ./gee
从 go 1.11 版本开始,引用相对路径的 package 需要使用上述方式。
base3/main.go
代码语言:javascript复制 package main
// $ curl http://localhost:9999/
// URL.Path = "/"
// $ curl http://localhost:9999/hello
// Header["Accept"] = ["*/*"]
// Header["User-Agent"] = ["curl/7.54.0"]
// curl http://localhost:9999/world
// 404 NOT FOUND: /world
import (
"fmt"
"net/http"
"gee"
)
func main() {
r := gee.New()
r.GET("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path = %qn", req.URL.Path)
})
r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %qn", k, v)
}
})
r.Run(":9999")
}
base3/gee/gee.go
代码语言:javascript复制 package gee
import (
"fmt"
"log"
"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
log.Printf("Route %4s - %s", 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
//handler, ok := engine.router[key]:在路由表中查找对应键的处理器函数。
//engine.router 是一个 map[string]HandlerFunc,存储了所有的路由键和处理器函数。
//handler:如果键存在,handler 将是对应的处理器函数。
//ok:一个布尔值,表示键是否存在于路由表中。
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %sn", req.URL)
}
}
首先定义了类型
HandlerFunc
,这是提供给框架用户的,用来定义路由映射的处理方法。我们在Engine
中,添加了一张路由映射表router
,key 由请求方法和静态路由地址构成,例如GET-/
、GET-/hello
、POST-/hello
,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法。 当用户调用(*Engine).GET()
方法时,会将路由和处理方法注册到映射表 router 中,(*Engine).Run()
方法,是 ListenAndServe 的包装。Engine
实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND 。
至此,整个Gee
框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。
day2-context
对Web服务来说,无非是根据请求*http.Request
,构造响应http.ResponseWriter
。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。
针对使用场景,封装*http.Request
和http.ResponseWriter
的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name
,参数:name
的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。
gee/context.go
代码语言:javascript复制 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))
}
- 代码最开头,给
map[string]interface{}
起了一个别名gee.H
,构建JSON数据时,显得更简洁。 Context
目前只包含了http.ResponseWriter
和*http.Request
,另外提供了对 Method 和 Path 这两个常用属性的直接访问。- 提供了访问Query和PostForm参数的方法。
- 提供了快速构造String/Data/JSON/HTML响应的方法。
这部分代码整体不难理解和day1代码难度基本一样,只是大致修改了一下参数。
gee/router.go
代码语言:javascript复制 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) {
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: %sn", c.Path)
}
}
代码基本没动,只是把与路由有关的方法和结构提取了出来,放到了新的文件中
gee/gee.go
代码语言:javascript复制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 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) {
log.Printf("Route %4s - %s", method, pattern)
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)
/*创建上下文对象:newContext(w, req) 创建并初始化一个 Context 对象,封装了 HTTP 请求和响应相关的信息。
调用路由处理方法:engine.router.handle(c) 将上下文对象传递给路由器的处理方法,根据请求信息找到对应的处理函数并执行。*/
}
相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。
day3
前面使用了map结构来存储了路由表,索引很高效,但是这个方式只能用来索引静态路由,但是无法支持动态路由
动态路由就是一条路由规则可以匹配某一类型而非某一条固定的路由
实现动态路由有很多方法:开源的gorouter支持在路由规则嵌入正则,另一个开源的httprouter就不支持正则。
实现动态路由最常用的数据结构是前缀树(trie树):每一个节点的所有子节点都拥有相同的前缀,这种结构很适用于路由匹配。举个栗子:
- /:lang/doc
- /:lang/tutorial
- /:lang/intro
- /about
- /p/blog
- /p/related
http请求的路径恰好是由/分隔的多段内容构成的,因此,每一段可以作为前缀树的一个节点。我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。
接下来需要实现的动态路由具有以下两个功能。
- 参数匹配
:
。例如/p/:lang/doc
,可以匹配/p/c/doc
和/p/go/doc
。 - 通配
*
。例如/static/*filepath
,可以匹配/static/fav.ico
,也可以匹配/static/js/jQuery.js
,这种模式常用于静态服务器,能够递归地匹配子路径。
具体实现
gee/grie.go
代码语言:javascript复制type node struct {
pattern string
part string
children []*node
isWild bool
}
func (n *node) String() string {
return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}
func (n *node) insert(pattern string, parts []string, height int) {
if len(parts) == height {
n.pattern = pattern
return
}
part := parts[height]
child := n.matchChild(part)
if child == nil {
child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
n.children = append(n.children, child)
}
child.insert(pattern, parts, height 1)
}
func (n *node) search(parts []string, height int) *node {
if len(parts) == height || strings.HasPrefix(n.part, "*") {
if n.pattern == "" {
return nil
}
return n
}
part := parts[height]
children := n.matchChildren(part)
for _, child := range children {
result := child.search(parts, height 1)
if result != nil {
return result
}
}
return nil
}
/*insert 方法通过递归方式,将路由模式的各个部分依次插入前缀树中,构建路由树。
search 方法通过递归方式,在前缀树中查找匹配的路由模式,支持静态路由和通配符路由的匹配。*/
func (n *node) travel(list *([]*node)) {
if n.pattern != "" {
*list = append(*list, n)
}
for _, child := range n.children {
child.travel(list)
}
}
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || child.isWild {
return child
}
}
return nil
}
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
}
//这两个方法整体逻辑差别不大,一个是返回单个子节点,另一个是返回所有匹配的子节点
//matchChild: 在路由树插入新节点时,找到合适的子节点进行插入。
//matchChildren: 在路由树查找节点时,找到所有可能的匹配路径。
gee/router.go
代码语言:javascript复制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
}
/*
r.roots 是一个 map[string]*node,其中键是 HTTP 方法(如 GET、POST),值是对应的路由树的根节点。
root 是根据 method 从 r.roots 中获取的值,即对应的根节点。
ok 是一个布尔值,表示键 method 是否存在于 r.roots 中。
*/
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: %sn", c.Path)
}
}
这里使用 roots 来存储每种请求方式的Trie 树根节点。使用 handlers 存储每种请求方式的 HandlerFunc 。getRoute 函数中,还解析了
:
和*
两种匹配符的参数,返回一个 map 。例如/p/go/doc
匹配到/p/:lang/doc
,解析结果为:{lang: "go"}
,/static/css/geektutu.css
匹配到/static/*filepath
,解析结果为{filepath: "css/geektutu.css"}
。
day4-Group
所谓分组是指路由的分组,如果没有路由的分组,我们就需要对每一个路由进行控制,但是在具体的业务中,一部分路由总是需要进行相似的处理。举个栗子
- 以
/post
开头的路由匿名可访问。 - 以
/admin
开头的路由需要鉴权。 - 以
/api
开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。
大部分情况的路由分组,是用相同的前缀进行区分的,所以这里实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如/post
是一个分组,/post/a
和/post/b
可以是该分组下的子分组。作用在/post
分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。
一个 Group 对象需要具备哪些属性呢?首先是前缀(prefix),比如/
,或者/api
;要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;当然了,按照我们一开始的分析,中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。还记得,我们之前调用函数(*Engine).addRoute()
来映射所有的路由规则和 Handler 。如果Group对象需要直接映射路由规则的话,比如我们想在使用框架时,这时调用:
r := gee.New()
v1 := r.Group("/v1")
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
因此Group对象,还需要有访问
Router
的能力,为了方便,我们可以在Group中,保存一个指针,指向Engine
,整个框架的所有资源都是由Engine
统一协调的,那么就可以通过Engine
间接地访问各种接口了。
gee/gee.go
代码语言:javascript复制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
}
//Group 的定义
Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all groups
}
)
//进一步地抽象,将Engine作为最顶层的分组,也就是说Engine拥有RouterGroup所有的能力。
// 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)
}
//调用了group.engine.router.addRoute来实现了路由的映射。由于Engine从某种意义上继承了RouterGroup的所有属性和方法,因为 (*Engine).engine 是指向自己的。这样实现,我们既可以像原来一样添加路由,也可以通过分组添加路由。
// 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)
}
day5-middlewares
中间件(middlewares),简单说,就是非业务的技术类组件。
有点像接口那种感觉
对中间件而言,需要考虑2个比较关键的点:
- 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
- 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限
geektutu大佬这里参考了gin
具体设计
gee/logger.go
代码语言:javascript复制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 的中间件的定义与路由映射的 Handler 一致,处理的输入是
Context
对象。插入点是框架接收到请求初始化Context
对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context
进行二次加工。另外通过调用(*Context).Next()
函数,中间件可等待用户自己定义的Handler
处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,c.Next()
表示等待执行其他的中间件或用户的Handler
: ps:支持多个中间件,依次进行调用
中间件是应用在RouterGroup
上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。只作用在某条路由规则的功能通用性太差,不适合定义为中间件。
当接收到请求后,匹配路由,该请求的所有信息都保存在Context
中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在Context
中,依次进行调用。为什么依次调用后,还需要在Context
中保存呢?因为在设计中,中间件不仅作用在处理流程前,也可以作用在处理流程后,即在用户定义的 Handler 处理完毕后,还可以执行剩下的操作。
为此,我们给Context
添加了2个参数,定义了Next
方法:
gee/context.go
代码语言:javascript复制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))
}
代码语言:javascript复制
index
是记录当前执行到第几个中间件,当在中间件中调用Next
方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在Next
方法之后定义的部分。如果我们将用户在映射路由时定义的Handler
添加到c.handlers
列表中,结果会怎么样呢?想必你已经猜到了。
func A(c *Context) {
part1
c.Next()
part2
}
func B(c *Context) {
part3
c.Next()
part4
}
设我们应用了中间件 A 和 B,和路由映射的 Handler。
c.handlers
是这样的[A, B, Handler],c.index
初始化为-1。调用c.Next()
,最终的顺序是part1 -> part3 -> Handler -> part 4 -> part2
。
在gee.go中定义Use
函数,将中间件应用到某个 Group 。
在router.go中将从路由匹配得到的 Handler 添加到 c.handlers
列表中,执行c.Next()
。
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: %sn", c.Path)
})
}
c.Next()
}
day6-template
现在越来越流行前后端分离的开发模式,即 Web 后端提供 RESTful 接口,返回结构化的数据(通常为 JSON 或者 XML)。前端使用 AJAX 技术请求到所需的数据,利用 JavaScript 进行渲染。
但前后分离的一大问题在于,页面是在客户端渲染的,比如浏览器,这对于爬虫并不友好。Google 爬虫已经能够爬取渲染后的网页,但是短期内爬取服务端直接渲染的 HTML 页面仍是主流。
要做到服务端渲染,第一步便是要支持 JS、CSS 等静态文件。之前设计动态路由的时候,支持通配符*
匹配多级子路径。比如路由规则/assets/*filepath
,可以匹配/assets/
开头的所有的地址。例如/assets/js/geektutu.js
,匹配后,参数filepath
就赋值为js/geektutu.js
。
如果将所有的静态文件放在/usr/web
目录下,那么filepath
的值即是该目录下文件的相对地址。映射到真实的文件后,将文件返回,静态服务器就实现了。
找到文件后,如何返回这一步,net/http
库已经实现了。因此,gee 框架要做的,仅仅是解析请求的地址,映射到服务器上文件的真实地址,交给http.FileServer
处理就好了。
gee/gee.go
代码语言:javascript复制// create static handler
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
absolutePath := path.Join(group.prefix, relativePath)
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) {
file := c.Param("filepath")
// Check if file exists and/or if we have permission to access it
if _, err := fs.Open(file); err != nil {
c.Status(http.StatusNotFound)
return
}
fileServer.ServeHTTP(c.Writer, c.Req)
}
}
// serve static files
func (group *RouterGroup) Static(relativePath string, root string) {
handler := group.createStaticHandler(relativePath, http.Dir(root))
urlPattern := path.Join(relativePath, "/*filepath")
// Register GET handlers
group.GET(urlPattern, handler)
}
我们给RouterGroup
添加了2个方法,Static
这个方法是暴露给用户的。用户可以将磁盘上的某个文件夹root
映射到路由relativePath
。例如:
r := gee.New()
r.Static("/assets", "/usr/geektutu/blog/static")
// 或相对路径 r.Static("/assets", "./static")
r.Run(":9999")
用户访问localhost:9999/assets/js/geektutu.js
,最终返回/usr/geektutu/blog/static/js/geektutu.js
。
Go语言内置了text/template
和html/template
2个模板标准库,其中html/template为 HTML 提供了较为完整的支持。包括普通变量渲染、列表渲染、对象渲染等。gee 框架的模板渲染直接使用了html/template
提供的能力。
Engine struct {
*RouterGroup
router *router
groups []*RouterGroup // store all groups
htmlTemplates *template.Template // for html render
funcMap template.FuncMap // for html render
}
func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.funcMap = funcMap
}
func (engine *Engine) LoadHTMLGlob(pattern string) {
engine.htmlTemplates = template.Must(template.New("").Funcs(engine.funcMap).ParseGlob(pattern))
}
首先为 Engine 示例添加了 *template.Template
和 template.FuncMap
对象,前者将所有的模板加载进内存,后者是所有的自定义模板渲染函数。
另外,给用户分别提供了设置自定义渲染函数funcMap
和加载模板的方法。
接下来,对原来的 (*Context).HTML()
方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。
gee/context.go
代码语言:javascript复制type Context struct {
// ...
// engine pointer
engine *Engine
}
func (c *Context) HTML(code int, name string, data interface{}) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {
c.Fail(500, err.Error())
}
}
我们在 Context
中添加了成员变量 engine *Engine
,这样就能够通过 Context 访问 Engine 中的 HTML 模板。实例化 Context 时,还需要给 c.engine
赋值。
gee/gee.go
代码语言:javascript复制func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// ...
c := newContext(w, req)
c.handlers = middlewares
c.engine = engine
engine.router.handle(c)
}
最终的目录结构
代码语言:javascript复制---gee/
---static/
|---css/
|---geektutu.css
|---file1.txt
---templates/
|---arr.tmpl
|---css.tmpl
|---custom_func.tmpl
---main.go
day7-error
先简单了解一下什么是panic defer recover
在Go语言中,panic
、defer
和 recover
是处理异常的一套机制。下面是它们的具体作用和使用方法:
panic
panic
用于在程序运行过程中发生严重错误时中止函数的正常执行。panic
可以携带一个错误信息,用于描述发生的错误。调用 panic
后,当前函数的执行将被立即终止,之后会逐层向上传递,直到程序退出或某个上层函数通过 recover
处理了这个 panic
。
func mightGoWrong() {
panic("something went wrong")
}
defer
defer
语句用于延迟函数的执行,直到包含 defer
的函数执行完毕。这通常用于确保一些必要的清理工作能够在函数结束前完成,例如关闭文件、释放资源等。无论函数是否发生 panic
,defer
语句都会执行。
func exampleDefer() {
defer fmt.Println("This will be printed last")
fmt.Println("This will be printed first")
}
输出:
代码语言:javascript复制This will be printed first
This will be printed last
recover
recover
用于在 panic
发生时捕获错误,从而阻止程序崩溃。recover
必须在被延迟执行的函数中使用,即在 defer
中调用。
func safeFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from", r)
}
}()
panic("something went wrong")
}
在这个例子中,panic
会触发,但 recover
会捕获到这个错误信息,并打印 "Recovered from something went wrong"。程序不会崩溃,会继续执行。
综合示例
代码语言:javascript复制package main
import (
"fmt"
)
func main() {
fmt.Println("Starting the main function")
safeFunction()
fmt.Println("The main function continues")
}
func safeFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from", r)
}
}()
fmt.Println("Calling function that might panic")
mightGoWrong()
fmt.Println("This line will not be executed")
}
func mightGoWrong() {
panic("something went wrong")
}
输出:
代码语言:javascript复制Starting the main function
Calling function that might panic
Recovered from something went wrong
The main function continues
在这个综合示例中,main
函数调用 safeFunction
,safeFunction
在调用 mightGoWrong
后触发 panic
,但 recover
捕获了这个 panic
并打印错误信息,随后 main
函数继续执行。
我们之前实现了中间件机制,错误处理也可以作为一个中间件,增强 gee 框架的能力。
新增文件 gee/recovery.go,在这个文件中实现中间件 Recovery
。
func Recovery() HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
message := fmt.Sprintf("%s", err)
log.Printf("%snn", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
c.Next()
}
}
Recovery
的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error。
你可能注意到,这里有一个 trace() 函数,这个函数是用来获取触发 panic 的堆栈信息,完整代码如下:
gee/recovery.go
代码语言:javascript复制func trace(message string) string {
var pcs [32]uintptr
n := runtime.Callers(3, pcs[:]) // skip first 3 caller
var str strings.Builder
str.WriteString(message "nTraceback:")
for _, pc := range pcs[:n] {
fn := runtime.FuncForPC(pc)
file, line := fn.FileLine(pc)
str.WriteString(fmt.Sprintf("nt%s:%d", file, line))
}
return str.String()
}
func Recovery() HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
message := fmt.Sprintf("%s", err)
log.Printf("%snn", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
c.Next()
}
}
在 trace() 中,调用了 runtime.Callers(3, pcs[:])
,Callers 用来返回调用栈的程序计数器, 第 0 个 Caller 是 Callers 本身,第 1 个是上一层 trace,第 2 个是再上一层的 defer func
。因此,为了日志简洁一点,我们跳过了前 3 个 Caller。
接下来,通过 runtime.FuncForPC(pc)
获取对应的函数,在通过 fn.FileLine(pc)
获取到调用该函数的文件名和行号,打印在日志中。
至此,gee 框架的错误处理机制就完成了。
ps:代码粘贴并不是很完整,有需要的可以去作者github获取
https://github.com/geektutu/7days-golang