复现:原链接
一个Web框架需要支持的功能,
hello/:name
,hello/*
一个启动http服务的示例,
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/hello", helloHandler)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
// return headers
func helloHandler(writer http.ResponseWriter, request *http.Request) {
for k, v := range request.Header {
fmt.Fprintf(writer, "Header[%q] = %q\n", k, v)
}
}
// return the r.URL.Path
func indexHandler(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "URL.Path = %q\n", request.URL.Path)
}
可以看到启动服务时使用的是,http.ListenAndServe(":8080", nil)
,传入了nil,实际上可以传入一个handler,用于处理请求,也就是所有请求的入口。
可以看源码,实际上就是一个接口,
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
于是我们可以实现一个处理器用来处理链接
type Engine struct {
}
func (e *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)
if err := http.ListenAndServe(":8080", engine); err != nil {
log.Fatal(err)
}
}
组织代码
gee/
|--gee.go
|--go.mod
main.go
go.mod
其中go.mod
内容为
module exmaple
go 1.20
require (
gee v0.0.0
)
replace (
gee => ./gee
)
将gee
指向./gee
,相对路径包的引用方式。
gee/go.mod
的内容
module gee
go 1.20
主函数
main.go
package main
import (
"fmt"
"gee"
"net/http"
)
func main() {
r := gee.New()
r.GET("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
})
r.GET("/hello", func(w http.ResponseWriter, r *http.Request) {
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
})
r.Run(":8080")
}
这就与gin
的使用方法很像了,使用new
创建gee实例,如何处理动态路由,GET
添加了路由,
GET方法和Run方法都在gee中实现。
最后用Run
启动服务。
gee.go
的实现
package gee
import (
"fmt"
"net/http"
)
// 定义HandlerFunc 类型
type HandlerFunc func(w http.ResponseWriter, r *http.Request)
// 定义引擎,其中有路由表
type Engine struct {
router map[string]HandlerFunc
}
// 创建一个引擎
func New() *Engine {
return &Engine{router: make(map[string]HandlerFunc)}
}
// 添加路由
func (e *Engine) addRouter(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
e.router[key] = handler
}
// 添加 GET请求路由
func (e *Engine) GET(pattern string, handler HandlerFunc) {
//调用 添加路由方法
e.addRouter("GET", pattern, handler)
}
// 添加 POST请求路由
func (e *Engine) POST(pattern string, handler HandlerFunc) {
e.addRouter("POST", pattern, handler)
}
// 开启一个http server
func (e *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, e)
}
// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
key := req.Method + "-" + req.URL.Path
if handler, ok := e.router[key]; ok {
handler(w, req)
} else {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "404 NOT FOUND %q\n", req.URL.Path)
}
}
引擎中有路由表,负责实现请求到回调函数的映射,而添加路由的方法则是通过,GET
,POST
等方法实现的。
主要增加功能
router
独立出来,方便后续增强context
,即封装Request
和Response
,提供对JSON
、HTML
的支持使用效果
package main
import (
"gee"
"net/http"
)
func main() {
r := gee.New()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "Hello Gee
")
})
r.GET("/hello", func(c *gee.Context) {
// xx/hello?name = tom
c.String(http.StatusOK, "hello %s, your location %s ", 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(":8080")
}
为什么?
对Web服务来说,无非是根据请求*http.Request
,构造响应http.ResponseWriter
。但是两个部分,我们应该构建一个完整的响应,包括需要的各部分内容。headers
,statusCode
等
封装前
obj = map[string]interface{}{
"name": "geektutu",
"password": "1234",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(obj); err != nil {
http.Error(w, err.Error(), 500)
}
封装后
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name
,参数:name
的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载
封装实现
gee/context.go
package gee
import (
"encoding/json"
"fmt"
"net/http"
)
// 定义Json封装类型,更加简洁
type H map[string]interface{}
type Context struct {
//原始对象
Writer http.ResponseWriter
Req *http.Request
//请求信息
Path string
Method string
//响应信息
StatusCode int
}
func newContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Req: r,
Path: r.URL.Path,
Method: r.Method,
}
}
// 查询post参数的值
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}
// 查询get url的参数
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...)))
}
// 设置响应JSON,
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)
}
// 设置响应HTML
func (c *Context) HTML(code int, html string) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
c.Writer.Write([]byte(html))
}
将路由提取出来,单独实现,gee/router.go
,便于功能增强,
微调实现,将handler变为 context,小写,封装在包内使用
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) addRouter(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)
}
}
修改gee.go
package gee
import (
"net/http"
)
// 定义HandlerFunc 类型
type HandlerFunc func(ctx *Context)
// 定义引擎,其中有路由表
type Engine struct {
router *router
}
// 创建一个引擎
func New() *Engine {
return &Engine{router: newRouter()}
}
// 添加 GET请求路由
func (e *Engine) GET(pattern string, handler HandlerFunc) {
//调用 添加路由方法
e.router.addRouter("GET", pattern, handler)
}
// 添加 POST请求路由
func (e *Engine) POST(pattern string, handler HandlerFunc) {
e.router.addRouter("POST", pattern, handler)
}
// 开启一个http server
func (e *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, e)
}
// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := newContext(w, r)
e.router.handle(c)
}
将路由独立出来了,单独实现,引擎中只需要一个指针即可。
内容:
name
和*filepath
,现在路由的缺陷,
现在使用map[string]handlerFun
确实比较高效一一对应,但是只能支持静态路由。
例如想要实现/hello/:name
这样的动态路由则不行,
动态路由指一条路由规则可以匹配某一类型而不是一条固定的路由。
例如: /hello/:name
,可以匹配/hello/Jack
,/hello/Tom
.
动态路由的实现方式有许多方法。
比如:正则gorouter
;
前缀树是也是一个好的解决方案,以/
作为分隔,
HTTP恰好是用/
分割路径的,动态路由的两个功能
:
,例如/p/:lang/doc
可以匹配/p/c/doc
和/p/go/doc
*
,例如/static/*filepath
,…也能递归匹配子路径。包含有待匹配的路由,路由中的一部分,字节点,是否精准匹配;
type node struct {
pattern string // 待匹配的路由,
part string //路由中的一部分
children []*node //子节点
isWild bool //是否精准匹配
}
首先实现查找功能,给定一个part
,查找所有满足条件的子节点,
// 第一个匹配成功的节点,用于插入
func (n *node) matchChild(part string) *node {
for _, child := range n.children { //遍历子节点
if child.part == part || child.isWild { //子节点是否与当前路径部分相等, child.isWild ?
return child
}
}
return nil
}
// 返回所有匹配成功的节点,用于查找
func (n *node) mathChildren(part string) []*node {
nodes := make([]*node, 0)
for _, child := range n.children {
if child.part == part || child.isWild {
nodes = append(nodes, child)
}
}
return nil
}
对于路由来说,最重要的当然是注册与匹配。对应Trie树
就是插入和查询功能。
插入功能,递归查找每一层的节点,如果没有匹配到当前part
,则创建节点。
查询功能,递归查询每层的节点,退出规则为匹配到*
,匹配失败,或匹配到第len(parts)
层节点。
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
}
引用到Router中去,
用roots
存储每一种请求方式的Trie
树根节点。
暂时不明白
package gee
import (
"log"
"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)}
}
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) 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) addRouter(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
}
gee/context.go
type Context struct {
//原始对象
Writer http.ResponseWriter
Req *http.Request
//请求信息
Path string
Method string
Params map[string]string
//响应信息
StatusCode int
}
func (c *Context) Param(key string) string {
value, _ := c.Params[key]
return value
}
gee/router.go
func (r *router) handle(c *Context) {
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
log.Printf("Status %d %s", c.StatusCode, c.Path)
c.Params = params
key := c.Method + "-" + n.pattern
r.handlers[key](c)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
log.Printf("Status %d %s", c.StatusCode, c.Path)
}
}
这一章最难理解,前缀树还不太熟悉,掌握插入、查找方法应该能增强理解。
实现路由的分组控制,
就是加前缀
对于不同的组采取不同的策略
/post
开头的路由匿名可访问。/admin
开头的路由需要鉴权。/api
开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。分组需要有前缀
,支持嵌套要知道父节点是谁,存储中间件。
在gee/gee.go
中
type RouterGroup struct {
prefix string
middlewares []HandlerFunc
parent *RouterGroup
engine *Engine //共享一个engine
}
同时对Engine
作修改:
// 定义引擎,其中有路由表
type Engine struct {
*RouterGroup
router *router
group []*RouterGroup //存储所有的分组
}
将所有和路由相关的函数,都交给RouterGroup
实现
gee.go
package gee
import (
"log"
"net/http"
)
type RouterGroup struct {
prefix string
middlewares []HandlerFunc
parent *RouterGroup
engine *Engine //共享一个engine
}
// 定义HandlerFunc 类型
type HandlerFunc func(ctx *Context)
// 定义引擎,其中有路由表
type Engine struct {
*RouterGroup
router *router
groups []*RouterGroup //存储所有的分组
}
// 创建一个引擎
func New() *Engine {
engine := &Engine{router: newRouter()}
engine.RouterGroup = &RouterGroup{
engine: engine,
}
engine.groups = []*RouterGroup{engine.RouterGroup}
return engine
}
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请求路由
func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
//调用 添加路由方法
group.addRoute("GET", pattern, handler)
}
// 添加 POST请求路由
func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
//调用 添加路由方法
group.addRoute("POST", pattern, handler)
}
// 开启一个http server
func (e *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, e)
}
// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := newContext(w, r)
e.router.handle(c)
}
main.go
func main() {
r := gee.New()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "Hello Gee
")
})
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "Hello Gee Group
")
})
v1.GET("/hello", func(c *gee.Context) {
// expect /hello/tom
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
}
v2 := r.Group("/v2")
{
v2.POST("/login", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
})
}
r.Run(":8080")
}
简单来说多了一个前缀,支持嵌套,每个分组里有父指针,有子分组数组。
内容:
非业务的技术组件。
web框架不可能实现所有功能,所以提供一个插口,
可以允许用户自定义功能。
关键点
参考Gin
中间件的定义与Handler一样,处理第输入是Context
对象,允许用户做一些自己的操作。
用户通过(*Context).Next()
函数,中间件可以等待Handler处理完成后做一些操作。
我们的希望:
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))
}
}
中间件应该在RouterGroup
上,最顶层相当于都使用,
gee/context.go
type Context struct {
//原始对象
Writer http.ResponseWriter
Req *http.Request
//请求信息
Path string
Method string
Params map[string]string
//响应信息
StatusCode int
//中间件
handlers []HandlerFunc
index int
}
func newContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
Writer: w,
Req: r,
Path: r.URL.Path,
Method: r.Method,
index: -1, //表示执行到第几个中间件了
}
}
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
定义Use
函数,将中间件用到某个组。
还需要改变ServeHTTP
的实现,将某组的middlerwares
传递给Context
gee/gee.go
// 使用中间件
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var middlewares []HandlerFunc
for _, group := range e.groups {
if strings.HasPrefix(r.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
c := newContext(w, r)
c.handlers = middlewares
e.router.handle(c)
}
在handle函数中,将路由匹配的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])
log.Printf("Status %d %s", c.StatusCode, c.Path)
} else {
c.handlers = append(c.handlers, func(ctx *Context) {
ctx.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
})
log.Printf("Status %d %s", c.StatusCode, c.Path)
}
c.Next()
}
真正执行Handler
是在c.Next()
中。
使用
package main
import (
"gee"
"log"
"net/http"
"time"
)
func onlyForV2() gee.HandlerFunc {
return func(ctx *gee.Context) {
t := time.Now()
ctx.Fail(500, "Internal Server Error")
log.Printf("[%d] %s in %v for Group V2", ctx.StatusCode, ctx.Req.RequestURI, time.Since(t))
}
}
func main() {
r := gee.New()
r.Use(gee.Logger())
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "Hello Gee
")
})
v1 := r.Group("/v1")
{
v1.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "Hello Gee Group
")
})
v1.GET("/hello", func(c *gee.Context) {
// expect /hello/tom
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
}
v2 := r.Group("/v2")
v2.Use(onlyForV2())
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/tom
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(":8080")
}
内容:
现在大多数Web服务都是前后端分离的。
对爬虫不友好。因为访问的是渲染前的页面。
实现服务端渲染。
将静态资源返回给客户端。
文件返回net/http
库已经实现了。
需要做的是找到服务器上,文件的真实位置。然后交给http.FileServer
即可。
gee/gee.go
// 静态文件处理器
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) func(c *Context) {
absoluePath := path.Join(group.prefix, relativePath)
fileServer := http.StripPrefix(absoluePath, http.FileServer(fs))
return func(c *Context) {
file := c.Param("filepath")
if _, err := fs.Open(file); err != nil {
c.Status(http.StatusNotFound)
return
}
fileServer.ServeHTTP(c.Writer, c.Req)
}
}
// 静态文件服务
func (group *RouterGroup) Static(relativePath string, root string) {
handler := group.createStaticHandler(relativePath, http.Dir(root))
urlPattern := path.Join(relativePath, "/*filepath")
group.GET(urlPattern, handler)
}
将文件映射,并添加处理函数。
使用html/template
模板,
gee/gee.go
// 定义引擎,其中有路由表
type Engine struct {
*RouterGroup
router *router
groups []*RouterGroup //存储所有的分组
htmlTemplates *template.Template
funcMap template.FuncMap
}
func (e *Engine) SetFuncMap(funcMap template.FuncMap) {
e.funcMap = funcMap
}
func (e *Engine) LoadHTMLGlob(pattern string) {
e.htmlTemplates = template.Must(template.New("").Funcs(e.funcMap).ParseGlob(pattern))
}
为 Engine 示例添加了 *template.Template
和 template.FuncMap
对象,前者将所有的模板加载进内存,后者是所有的自定义模板渲染函数。
给用户分别提供了设置自定义渲染函数funcMap
和加载模板的方法。
对(*Context).HTML()
方法修改。
gee/context.go
type Context struct {
...
engine *Engine //添加Engine指针
}
// 设置响应HTML
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
// 路由封装实现
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
c := newContext(w, r)
c.handlers = middlewares
c.engine = e
e.router.handle(c)
}
使用
package main
import (
"fmt"
"gee"
"html/template"
"log"
"net/http"
"time"
)
type student struct {
Name string
Age int8
}
func FormatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d-%02d-%02d", year, month, day)
}
func onlyForV2() gee.HandlerFunc {
return func(ctx *gee.Context) {
t := time.Now()
//ctx.Fail(500, "Internal Server Error")
log.Printf("[%d] %s in %v for Group V2", ctx.StatusCode, ctx.Req.RequestURI, time.Since(t))
}
}
func main() {
r := gee.New()
r.Use(gee.Logger())
r.SetFuncMap(template.FuncMap{
"FormatAsDate": FormatAsDate,
})
r.LoadHTMLGlob("templates/*")
r.Static("/assets", "./static")
stu1 := &student{Name: "Tom", Age: 22}
stu2 := &student{Name: "Judy", Age: 23}
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "css.tmpl", nil)
})
v1 := r.Group("/v1")
{
v1.GET("/student", func(c *gee.Context) {
c.HTML(http.StatusOK, "arr.tmpl", gee.H{
"title": "gee",
"stuArr": [2]*student{stu1, stu2},
})
})
v1.GET("/hello", func(c *gee.Context) {
// expect /hello/tom
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
}
v2 := r.Group("/v2")
v2.Use(onlyForV2())
{
v2.GET("/date", func(ctx *gee.Context) {
ctx.HTML(http.StatusOK, "custom_func.tmpl", gee.H{
"title": "gee",
"now": time.Date(2023, 6, 23, 14, 22, 0, 0, time.UTC),
})
})
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/tom
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(":8080")
}
内容:
go语言中,常见的错误处理是返回error,由调用者决定后续如何处理。
如果无法恢复,则手动触发panic;数组越界自动触发panic
panic会导致程序被终止,但在退出前,会先处理已经defer的任务,执行完成再退出。
Go提供了recover函数,避免因为panic
导致整个程序终止,recover
函数只在defer中失效。
// hello.go
func test_recover() {
defer func() {
fmt.Println("defer func")
if err := recover(); err != nil {
fmt.Println("recover success")
}
}()
arr := []int{1, 2, 3}
fmt.Println(arr[4])
fmt.Println("after panic")
}
func main() {
test_recover()
fmt.Println("after recover")
}
go run hello.go
defer func
recover success
after recover
使用中间件增强gee
框架的能力。
gee/recovery.go
package gee
import (
"fmt"
"log"
"net/http"
"runtime"
"strings"
)
// print stack trace for debug
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("\n\t%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("%s\n\n", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
c.Next()
}
}
Recovery
的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error。
使用
r.Use(gee.Recovery())
lo.go
func test_recover() {
defer func() {
fmt.Println(“defer func”)
if err := recover(); err != nil {
fmt.Println(“recover success”)
}
}()
arr := []int{1, 2, 3}
fmt.Println(arr[4])
fmt.Println("after panic")
}
func main() {
test_recover()
fmt.Println(“after recover”)
}
```go
go run hello.go
defer func
recover success
after recover
使用中间件增强gee
框架的能力。
gee/recovery.go
package gee
import (
"fmt"
"log"
"net/http"
"runtime"
"strings"
)
// print stack trace for debug
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("\n\t%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("%s\n\n", trace(message))
c.Fail(http.StatusInternalServerError, "Internal Server Error")
}
}()
c.Next()
}
}
Recovery
的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error。
使用
r.Use(gee.Recovery())