Echo框架
Q7nl1s admin

项目地址:https://github.com/labstack/echo

官方文档:https://echo.labstack.com/

由于框架更新迭代速度较快,若你阅读本文时已有新版本出现,请到官方文档进行学习。

简介

Echo框架是一个用 go 编写的 Web 框架,它具备可扩展、轻量级等特点。

Echo框架简单高效,几行代码就可以启动一个高性能的 HTTP 服务端。

Echo框架默认包含了MVC框架的 C(Controller)控制器部分,负责 URL 路由和控制器部分。V(View)视图部分和 M(Model)数据操作部分,我们可以使用自己喜欢的工具库来操作。

安装

要求

  • 安装 Go
  • 设置 GOPATH 环境变量

安装 Echo 包:

1
2
cd <PROJECT IN $GOPATH>
go get -u github.com/labstack/echo/...

其中...代表你想要安装的版本比如我这里安装的是 v4 版本,那我输入的命令就是 go get -u github.com/labstack/echo/v4

Echo 使用 Go 1.10.x 开发,并通过了 1.9.x1.10.x 的测试。


中间件

在 Go Echo框架中 中间件(Middleware)指的是可以拦截http请求-响应生命周期的特殊函数。在请求-响应生命周期中可以注册多个中间件,每个中间件执行不同的功能,一个中间执行完再轮到下一个中间件执行。

中间件的运行流程如下:

Echo_0

中间件常见的应用场景如下:

  • 请求限速
  • api 接口签名处理
  • 权限校验
  • 统一错误处理

如果你想拦截所有请求做一些事情都可以开发一个中间件函数去实现。

常见的中间件

常见的中间件有以下几种:

  1. Redirect 中间件
  2. Recover 中间件
  3. Static 中间件
  4. Logger 中间件
  5. Rewrite 中间件
  6. Session 中间件
  7. 自定义中间件

下面对它们进行逐一介绍。

Redirect 中间件

Redirect 重定向中间件支持下面几种重定向机制:

Recover 中间件

Recover 中间件,主要用于拦截 panic 错误并且在控制台打印错误日志,避免 echo 程序直接崩溃。
使用范例如下:

1
2
3
4
5
//初始化echo实例
e := echo.New()

//注册中间件
e.Use(middleware.Recover())

一般 echo 应用都会注册 Recover 中间件,避免程序崩溃退出。

Static 中间件

Static 中间件主要用于处理 js、css 之类的静态资源, 具体用法请参考: 处理静态文件。

Logger 中间件

Logger 中间件主要用于打印 http 请求日志。

1
2
3
4
5
//初始化echo实例
e := echo.New()

//注册中间件
e.Use(middleware.Logger())

开启 Logger 中间件后,访问 http 请求会打印下面日志:

1
{"time":"2022-11-10T19:40:54.8818768+08:00","id":"","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/","user_agent":"PostmanRuntime/7.29.2","status":200,"error":"","latency":0,"latency_human":"0s","bytes_in":0,"bytes_out":14}

Rewrite 中间件

url 重定向中间件,我们可以用于将一个 url 重定向到另外一个 url。
范例如下:

1
2
3
4
5
6
7
8
9
//初始化echo实例
e := echo.New()

e.Pre(middleware.Rewrite(map[string]string{
"/old": "/new", // 将 /old重定向至 /new
"/api/*": "/$1",
"/js/*": "/public/javascripts/$1",
"/users/*/orders/*": "/user/$1/order/$2",
}))

*“星代表任意字符串,$1 代表引用表达式中第一个星(*)的匹配值,$2 代表第二个,以此类推。

Session 中间件

会话中间件。

自定义中间件

下面以一个简单的统计访问量的例子介绍如何自定义中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"net/http"
"sync"

"github.com/labstack/echo"
)

// 记录访问量
var totalRequests = 0

var m sync.RWMutex

// 中间件函数
func Count(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 在这里处理拦截请求的逻辑
// 累计访问量
// 防止竞争条件发生
m.Lock()
defer m.Unlock()
totalRequests++

//在响应头中输出访问量
c.Response().Header().Add("requests", fmt.Sprintf("%d", totalRequests))

//执行下一个中间件或者执行控制器函数, 然后返回执行结果
return next(c)
}
}

func main() {
//初始化echo实例
e := echo.New()

//注册中间件
e.Use(Count)

e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})

e.Logger.Fatal(e.Start(":1323"))
}

响应头如下:

Echo_1


Context

echo.Context 表示当前 HTTP 请求的上下文。通过路径、路径参数、数据、注册处理器和相关 API 进行请求的读取与响应的输出。由于 Context 是一个接口,也可以轻松地使用自定义 API 进行扩展。

扩展 Context

定义一个自定义 context

1
2
3
4
5
6
7
8
9
10
type CustomContext stuct{
echo.Context
}

func (c *CustomContext) Foo(){
println("foo")
}
func (c *CustomContext) Bar(){
println("bar")
}

创建一个中间件来扩展默认的 context

1
2
3
4
5
6
e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cc := &CustomContext{c}
return h(cc)
}
})

此中间件应在任何其他中间件之前注册。

在处理器中使用

1
2
3
4
5
6
e.Get("/", func(c echo.Context) error {
cc := c.(*CustomContext)
cc.Foo()
cc.Bar()
return cc.String(200, "OK")
})

Cookies

Cookie 是用户访问网站时浏览器上存储的小型文本文件,由服务器发送而来。每当用户加载网站时,浏览器都会将 cookie 发送回服务器以通知用户之前的活动。 Cookie 作为一个可靠验证凭据,可用来记录状态信息(比如在线商城购物车中的商品)或记录用户的浏览器活动(包括单击特定按钮,登录或记录访问过的页面)。Cookie 还可以存储用户先前输入的密码和表单内容,例如信用卡号或地址。

在 Go Echo框架中,我们可以通过 net/http 包的 Cookie 结构体初始化一个 cookie,然后通过 echo.Context 上下文对象的 SetCookie 函数往请求结果设置 cookie。

Cookie 属性

属性 可选
Name No
Value No
Path Yes
Domain Yes
Expires Yes
Secure Yes
HTTPOnly Yes

Echo 使用 golang 自带的 http.Cookie 对象写入/读取从上下文中的 cookie。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Cookie struct {
Name string //cookie名字
Value string //cookie的值

Path string // [可选字段] cookie路径
Domain string // [可选字段] cookie作用域
Expires time.Time // [可选字段] cookie什么时候失效,需要设置一个具体的失效时间跟MaxAge字段二选一即可,

// MaxAge=0 忽略MaxAge属性.
// MaxAge<0 相当于删除cookie, 通常可以设置-1代表删除
// MaxAge>0 多少秒后cookie失效
MaxAge int // [可选字段] cookie有效期,单位是秒
Secure bool // [可选字段] cookie secure属性
HttpOnly bool // [可选字段] cookie http only属性
}

往客户设置一个 cookie 需要两个步骤:

  • 初始化 http.Cookie 对象;
  • 调用 SetCookie 函数设置 cookie 对象。

范例如下:

1
2
3
4
5
6
7
8
9
10
func writeCookie(c echo.Context) error{
// 初始化cookie对象
cookie := new(htttp.Cookie)
cookie.Name = "username"
cookie.Value = "jon"
// 设置cookie具体的失效时间
cookie.Expires = time.Now().Add(24 * time.Hour)
c.SetCookie(cookie)
return c.String(http.StatusOk,"write a cookie")
}
  • 使用 new(http.Cookie) 创建Cookie。
  • cookie 的属性值会被赋值给 http.Cookie 的可导出属性。
  • 最后,使用 c.SetCookie(cookies) 来给 HTTP 响应增加 Set-Cookie 头。

读取 cookie 主要通过 echo.Context 上下文对象的 Cookie 函数进行操作。

范例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Handler
func(c echo.Context) (err error){
// 更具cookie名,获取cookie,cookie存在则返回http.Cookie结构体
cookie,err := c.Cookie("username")
if err != nil {
return err
}

// 打印cookie名
fmt.Println(cookie.Name)

// 打印cookie值
fmt.Println(cookie.Value)
return c.String(http.StatusOK,"cookie操作")
}
  • Cookie 通过名称从 HTTP 请求里读取:c.Cookie("name")
  • Cookie 的属性可以使用 Getter 方法获取。

下面介绍如何一次性查询所有 cookie:

1
2
3
4
5
6
7
8
9
10
// Handler
func(c echo.Context) (err error) {
// 通过c.Cookies函数,查询所有cookie
// 这里通过循环语句打印所有cookie的名字和值
for _, cookie := range c.Cookies() {
fmt.Println(cookie.Name)
fmt.Println(cookie.Value)
}
return c.String(http.StatusOK, "cookie操作")
}

删除 cookie,本质上是通过设置 cookie 的过期时间,让 cookie 立刻失效。

范例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Handler
func(c echo.Context) (err error) {
// 初始化cookie对象
cookie := new(http.Cookie)
// 删除cookie只需要设置cookie名字就可以
cookie.Name = "username"
// cookie有效期为-1秒,注意这里不能设置为0,否则不会删除cookie
cookie.MaxAge = -1

// 设置cookie
c.SetCookie(cookie)
return c.String(http.StatusOK, "cookie操作")
}

错误处理

Echo 提倡通过中间件或处理程序 (handler) 返回 HTTP 错误集中处理。集中式错误处理程序允许我们从统一位置将错误记录到外部服务,并向客户端发送自定义 HTTP 响应。

你可以返回一个标准的 error 或者 echo.*HTTPError

例如,当基本身份验证中间件找到无效凭据时,会返回 401未授权错误 (401-Unauthorized),并终止当前的 HTTP 请求。

1
2
3
4
5
6
7
8
9
10
11
12
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Extract the credentials from HTTP request header and perform a security
// check

// For invalid credentials
return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials")

// For valid credentials call next
// return next(c)
}
})

你也可以不带消息内容调用 echo.NewHTTPError(),这种情况下状态文本会被用作错误信息,例如 Unauthorized

默认 HTTP 错误处理程序

Echo 提供了默认的 HTTP 错误处理程序,它用 JSON 格式发送错误。

1
2
3
{
"message": "error connecting to redis"
}

标准错误 error 的响应是 500 - Internal Server Error。然而在调试 (debug) 模式模式下,原始的错误信息会被发送。如果错误是 *HTTPError,则使用设置的状态代码和消息发送响应。如果启用了日志记录,则还会记录错误消息。

自定义 HTTP 错误处理程序

通过 e.HTTPErrorHandler 可以设置自定义的 HTTP 错误处理程序 (error handler) 。

通常默认的 HTTP 错误处理程序已经够用;然而如果要获取不同类型的错误并采取相应的操作,则可以使用自定义 HTTP 错误处理程序,例如发送通知邮件或记录日志到应用中心的场景。最后,你还可以发送自定义的错误页面或 JSON 响应给客户端。

错误页

利用自定义 HTTP 错误处理程序,可以在显示不同种类的错误页面的同时,记录错误日志。错误页的名称可写作 <CODE>.html,例如 500.html。你可以在

https://github.com/AndiDittrich/HttpErrorPages看到 Echo 内置的错误页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func customHTTPErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
}
// 记录错误日志
c.Logger().Error(err)
errorPage := fmt.Sprintf("%d.html", code)
if err := c.File(errorPage); err != nil {
// 打开错误页面失败记录日志
c.Logger().Error(err)
}
}

e.HTTPErrorHandler = customHTTPErrorHandler

日志除了记录到 logger,也可以记录到第三方服务,例如 Elasticsearch 或者 Splunk


模板

模板渲染

使用 Context#Render(code int, name string, data interface{}) error 命令渲染带有数据的模板,并发送带有状态代码的 text / html 响应。通过 Echo.Renderer 的设置我们可以使用任何模板引擎。

我们先看下 echo.Renderer 接口定义:

1
2
3
4
5
6
7
8
Renderer interface {
// 渲染函数定义
// 第一参数用于保存渲染模版后的结果
// 第二个参数是模版名字
// 第三个参数是传入模版的参数,可以是任意类型
// 第四个参数是echo.Context
Render(io.Writer, string, interface{}, Context) error
}

通过实现 echo.Renderer 接口自定义当调用 Render 函数的时候我们使用什么模版引擎来渲染模版。

下面是使用 Go html/template 的示例:

  1. 实现 echo.Renderer 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 自定义的模版引擎 struct
    type Template struct {
    templates *template.Template
    }
    // 实现接口,Render函数
    func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
    // 调用模版引擎渲染模版
    return t.templates.ExecuteTemplate(w, name, data)
    }
  2. 预编译模板

    public/views/hello.html

    1
    {{define "hello"}}Hello, {{.}}!{{end}}
    1
    2
    3
    4
    5
    t := &Template{
    //模版引擎支持提前编译模版, 这里对views目录下以html结尾的模版文件进行预编译处理
    // 预编译处理的目的是为了优化后期渲染模版文件的速度
    templates: template.Must(template.ParseGlob("public/views/*.html")),
    }
  3. 声明模板

    1
    2
    3
    4
    5
    6
    //初始化echo实例
    e := echo.New()
    // 向echo实例注册模版引擎
    e.Renderer = t
    // 初始化路由和控制器函数
    e.GET("/hello", Hello)
  4. 在控制器中渲染模板

    1
    2
    3
    4
    func Hello(c echo.Context) error {
    //渲染hello.html模版文件,模版参数为World
    return c.Render(http.StatusOK, "hello.html", "World")
    }

在控制器中渲染模版并返回 HTML 页面

完成模版引擎设置后,就可以在控制器函数中通过 echo.Context 对象的 Render 函数渲染模版并返回 html 页面。

函数定义:
Render(code int, name string, data interface{}) error

参数说明:

参数 说明
code http状态码
name 模版文件名
data 模版参数,可以是任意类型数据

模版文件 views/hello.html 内容:

1
Hello, {{.}}!

渲染模版文件:

1
2
3
4
func Hello(c echo.Context) error {
//渲染hello.html模版文件,模版参数为world
return c.Render(200, "hello.html", "World")
}

渲染结果为:Hello, world!

高级 - 在模版中调用 Echo

在某些情况下,从模板生成 uri 可能很有用,为此,您需要从模板本身调用 Echo#Reverse。此时,Golang 的 html/template 包并不一定合适这种情况,但我们可以通过两种方法实现它:第一种,给所有的传递到模版的对象提供一个公用的方法;第二种,将 map[string]interface{} 作为参数传递并在自定义渲染器中扩充此模版。鉴于后一种方法的灵活性,这里有一个示例程序: template.html

1
2
3
4
5
6
7
8
9
10
<html>
<body>
<!-- 预编译模板 -->
<h1>Hello {{index . "name"}}</h1>
<p>{{ with $x := index . "reverse" }}
{{ call $x "foobar" }} &lt;-- this will call the $x with parameter "foobar"
{{ end }}
</p>
</body>
</html>

server.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"html/template"
"io"
"net/http"

"github.com/labstack/echo/v4"
)

// 实现 echo.Renderer 接口
// TemplateRenderer is a custom html/template renderer for Echo framework
type TemplateRenderer struct {
templates *template.Template
}

// Render renders a template document
func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {

// Add global methods if data is a map
if viewContext, isMap := data.(map[string]interface{}); isMap {
viewContext["reverse"] = c.Echo().Reverse
}

return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
// 声明模板
e := echo.New()
renderer := &TemplateRenderer{
templates: template.Must(template.ParseGlob("*.html")),
}
e.Renderer = renderer
// 在 action 中渲染模板
// Named route "foobar"
e.GET("/something", func(c echo.Context) error {
return c.Render(http.StatusOK, "template.html", map[string]interface{}{
"name": "Dolly!",
})
}).Name = "foobar"

e.Logger.Fatal(e.Start(":8000"))
}

测试结果如下:

Echo_4


请求

请求参数

此处介绍四种获取请求参数的方式:绑定 struct 数据、获取 Get 参数、获取 Post 参数以及获取路径参数。

绑定 struct 数据

通过将请求参数绑定到一个 struct 对象的方式获取数据。这种方式获取请求参数支持 json、xml、k/v 键值对等多种方式。

下面例子是将请求参数绑定到 User struct 对象。

1
2
3
4
5
// User 结构体定义
type User struct {
Name string `json:"name" form:"name" query:"name"`
Email string `json:"email" form:"email" query:"email"`
}

通过定义 struct 字段的标签,定义请求参数和 struct 字段的关系。
下面对 User 的 Name 字段的标签进行说明。

struct标签说明:

标签 说明
json:”name” 支持post请求,数据格式为json格式,并且字段名为name
form:”name” 支持post请求,并且参数名为name
query:”name” 支持get请求,并且参数名为name

我们可以根据自己的需要选择支持的请求方式和数据类型,例如需要支持 xml 数据格式,可以这样定义字段标签: xml:”name”。

下面看控制器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Handler
func(c echo.Context) (err error) {
u := new(User)
// 调用 echo.Context 的 Bind 函数将请求参数和 User 对象进行绑定。
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials")
}

// 请求参数绑定成功后 u 对象就保存了请求参数。
// 这里直接将请求参数以json格式显示
// 注意:User 结构体,字段标签定义中,json 定义的字段名,就是 User 对象转换成 json 格式对应的字段名。
return c.JSON(http.StatusOK, u)
}

要在创建User对象后调用 echo.Context 的 Bind 函数将请求参数和 User 对象进行绑定,才可以将请求的实际参数和 struct 字段建立起交互关系。

获取 Get 请求数据

通过 echo.Context 对象的 QueryParam 函数可以直接获取 Get 请求参数。

1
2
3
4
5
6
7
// Handler
func(c echo.Context) error {
// 获取name参数, 通过QueryParam获取的参数值也是String类型。
name := c.QueryParam("name")
// 直接输出name参数
return c.String(http.StatusOK, name)
})

注:c.QueryParam(“name”) 中的 name 名字必须和 get 传参的名字一致,否则为无效传值。

query 对应 Get 请求

获取 Post 请求数据

通过 echo.Context 对象的 FormValue 函数可以直接获取 Post 请求参数。

1
2
3
4
5
6
7
8
// Handler
func(c echo.Context) error {
// 获取name参数
name := c.FormValue("name")

// 直接输出name参数
return c.String(http.StatusOK, name)
}

通过 FormValue 函数获取参数的值,数据类型都是 String 类型, 如果需要其他类型的数据,需要自己转换数据格式。

获取 path 路径参数

通过 echo.Context 对象的 Param 获取 url 路径参数。

1
2
3
4
5
6
7
8
//例子: url 路由规则为 /users/:name , :name 为参数。
e.GET("/users/:name", func(c echo.Context) error {
// 获取路径参数:name的值
name := c.Param("name")
// 如果请求url为: /users/codebaoku 则 name 的值为 codebaoku
// Param 获取的值也是 String类型
return c.String(http.StatusOK, name)
})

一个小 demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"net/http"

"github.com/labstack/echo"
)


type User struct {
Name string `json:"name" form:"name" query:"name"`
// Email string `json:"email" form:"email" query:"email"`
}

func main() {
//初始化echo实例
e := echo.New()

e.POST("/", func(c echo.Context) error {
u := new(User)
// Bind 函数将请求参数和 User 对象进行绑定
if err := c.Bind(u); err != nil {
return err
}
// 获取name参数, 通过QueryParam获取的参数值也是String类型。
// name := c.QueryParam("name")
// 直接输出name参数
return c.JSON(http.StatusOK, u)
})

e.Logger.Fatal(e.Start(":1323"))
}

上面改成 GET 传值也行

显示响应信息:

Echo_2

更改上述的路由定义,换为 GET 请求,修改绑定的控制器函数为 GET 请求数据,且将定义的 User 结构体注释掉

1
2
3
4
5
6
e.GET("/", func(c echo.Context) error {
name := c.QueryParam("name")
// 直接输出name参数
// 此处 name 只是一个用来接收值的变量而非一个键值对,所以返回结果 "haha"
return c.String(http.StatusOK, name)
})

进行测试得

Echo_3

请求结果

Go Echo框架支持以字符串、json、xml、文件等格式返回请求结果,响应请求。

在 Go Echo框架中,使用 echo.Context 上下文对象来支持多种返回处理结果。

以字符串方式响应请求

Context#String(code int, s string) 用于发送一个带有状态码的纯文本响应。

函数定义:
String(code int, s string) error

参数说明:

参数 说明
code http 状态码
s 返回字符串结果

范例如下:

1
2
3
4
5
6
func(c echo.Context) error {
// http.StatusOK == 200
return c.String(http.StatusOK, "欢迎访问 codebaoku.com!")
// 也可以直接设置http状态码
// return c.String(200, "欢迎访问 codebaoku.com!")
}

net/http 包定义了多种常用的状态码常量,例如:http.StatusOK == 200, http.StatusMovedPermanently == 301, http.StatusNotFound == 404等,具体可以参考 net/http包。

以 HTML 格式响应请求

Context#HTML(code int, html string) 用于发送一个带有状态码的简单 HTML 响应。如果你需要动态生成 HTML 内容请查看模版

示例

1
2
3
func(c echo.Context) error {
return c.HTML(http.StatusOK, "<strong>Hello, World!</strong>")
}

以 json 格式响应请求

函数定义:
JSON(code int, i interface{}) error

参数说明:

参数 说明
code http状态码
i 返回结果对象,通常传入struct对象

范例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// User 定义
type User struct {
Name string `json:"name"` // 通过json标签定义struct字段转换成json字段的名字。
Email string `json:"email"`
}

// Handler 控制器
func(c echo.Context) error {
//初始化user对象
u := &User{
Name: "wuxi",
Email: "qiyuewuxi@yeah.net",
}
//返回json数据
//返回结果:{"name":"wuxi", "email":"qiyuewuxi@yeah.net"}
return c.JSON(200, u)
}

JSON流

Context#JSON() 内部使用 json.Marshal 来转换 JSON 数据,但该方法面对大量的 JSON 数据会显得效率不足,对于这种情况可以直接使用 JSON 流。

示例

1
2
3
4
5
6
7
8
9
func(c echo.Context) error {
u := &User{
Name: "wuxi",
Email: "qiyuewuxi@yeah.net",
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
c.Response().WriteHeader(http.StatusOK)
return json.NewEncoder(c.Response()).Encode(u)
}

JSON 美化 (JSON Pretty)

Context#JSONPretty(code int, i interface{}, indent string) 可以发送带有缩进(可以使用空格和 tab)的更为好看的 JSON 数据。

示例

1
2
3
4
5
6
7
func(c echo.Context) error {
u := &User{
Name: "wuxi",
Email: "qiyuewuxi@yeah.net",
}
return c.JSONPretty(http.StatusOK, u, " ")
}

通过在请求URL查询字符串中附加 pretty ,你也可以使用 Context#JSON() 来输出带有缩进的 JSON 数据。

示例

将上述的控制器中的 return 改成以下样式

1
return c.JSONPretty(http.StatusOK, u, "wuhuwuhu")

测试的以下结果:
Echo_5

通过在请求URL查询字符串中附加 pretty ,你也可以使用 Context#JSON() 来输出带有缩进的 JSON 数据。

示例

1
curl http://localhost:1323/?pretty

以 xml 格式响应请求

函数定义:
XML(code int, i interface{}) error

参数说明:

参数 说明
code http状态码
i 返回结果对象,通常传入struct对象

范例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// User 定义, 默认struct的名字就是xml的根节点名字,这里转换成xml后根节点的名字为User.
type User struct {
Name string `xml:"name"` // 通过xml标签定义struct字段转换成xml字段的名字。
Email string `xml:"email"`
}

// Handler 控制器
func(c echo.Context) error {
// 初始化user对象
u := &User{
Name: "wuxi",
Email: "qiyuewuxi@yeah.net",
}
// 返回json数据
// 返回结果:
// <?xml version="1.0" encoding="UTF-8"?>
// <User>
// <name>wuxi</name>
// <email>qiyuewuxi@yeah.net</email>
// </User>
return c.XML(200, u)
}

当然也可以使用 XML 美化 (XML Pretty)

示例

1
2
3
4
5
6
7
func(c echo.Context) error {
u := &User{
Name: "wuxi",
Email: "qiyuewuxi@yeah.net",
}
return c.XMLPretty(http.StatusOK, u, " ")
}
1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<User>
<Name>wuxi</Name>
<Email>qiyuewuxi@yeah.net</Email>
</User>

以文件格式响应请求

如果我们需要实现文件下载功能,可以直接返回文件。
echo支持多种方式返回文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 例子1:
func(c echo.Context) error {
// 通过File函数,直接返回本地文件,参数为本地文件地址。
// 函数说明:c.File("文件路径")
return c.File("/var/www/1.jpg")
}

例子2
func(c echo.Context) error {
// 通过Attachment函数,返回本地文件,类似 File 函数,区别是可以指定下载的文件名。
// 函数说明: c.Attachment("文件路径", "下载的文件名")
return c.Attachment("/var/www/1.jpg", "1.jpg")
}

例子3
func(c echo.Context) (err error) {
data := []byte(`0306703,0035866,NO_ACTION,06/19/2006
0086003,"0005866",UPDATED,06/19/2006`)
// 通过Blob函数,以二进制数据格式返回文件
// 函数说明:c.Blob(状态码, "contentType", byte数组)
return c.Blob(http.StatusOK, "text/csv", data)
}

例子4
func(c echo.Context) error {
// 打开文件
f, err := os.Open("/var/www/1.jpg")
if err != nil {
return err
}
// 通过Stream函数,以stream流的方式返回文件
// 函数说明:
// Stream(code int, contentType string, r io.Reader) error
// 参数说明:
// code - 状态码
// contentType - html内容类型
// r - 实现io.Reader接口的struct对象都可以直接输出内容
return c.Stream(http.StatusOK, "image/png", f)
}

发送流 (Stream)

Context#Stream(code int, contentType string, r io.Reader) 可用于发送带有内容类型 (content type) 、状态代码、io.Reader 的任意类型数据流。

示例

1
2
3
4
5
6
7
func(c echo.Context) error {
f, err := os.Open("<PATH_TO_IMAGE>")
if err != nil {
return err
}
return c.Stream(http.StatusOK, "image/png", f)
}

发送空内容 (No Content)

Context#NoContent(code int) 可用于发送带有状态码的空内容。

示例

1
2
3
func(c echo.Context) error {
return c.NoContent(http.StatusOK)
}

Hooks

响应之前

Context#Response#Before(func()) 可以用来注册在写入响应之前调用的函数。

响应之后

Context#Response#After(func()) 可以用来注册在写入响应之后调用的函数。但是如果 “Content-Length” 是未知状态,则不会有任何方法会被执行。

示例

1
2
3
4
5
6
7
8
9
func(c echo.Context) error {
c.Response().Before(func() {
println("before response")
})
c.Response().After(func() {
println("after response")
})
return c.NoContent(http.StatusNoContent)
}

可以在响应之前与之后注册多个方法

示例

按上述进行测试得到结果如下:

Echo_6

可以看到无 Content-Length 值,因为我们在控制器中返回的是空内容

Echo_7

所以经过多次请求后,程序也只打印 before response

现在让我们对控制器进行修改

1
return c.String(http.StatusOK, "欢迎访问 codebaoku.com!")

让其以字符串方式响应请求

Echo_8

我们可以看到由于存在响应信息 Content-Length 发挥作用

Echo_9

进经过一次访问,可以看到 before responseafter response 都被打印了出来。

设置 http 响应头(设置 Header)

1
2
3
4
5
func(c echo.Context) error {
// 设置http响应 header
c.Response().Header().Add("site", "codebaoku")
return c.String(200, "欢迎访问 codebaoku.com!")
}

路由

基于 radix tree ,Echo 的路由查询速度非常快。路由使用 sync pool 来重用内存,实现无 GC 开销下的零动态内存分配。

路由规则

一条路由规则由三部分组成:

  • http 请求方法
  • url 路径
  • 控制器函数

通过特定的 HTTP 方法,url 路径和一个匹配的处理程序 (handler) 可以注册一个路由。例如,下面的代码则展示了一个注册路由的例子:它包括 Get 的访问方式, /hello 的访问路径,以及发送 Hello World HTTP 响应的处理程序。

1
2
3
4
5
6
// 业务处理
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
// 路由
e.GET("/hello", hello)

你可以用 Echo.Any(path string, h Handler) 来为所有的 HTTP 方法发送注册 处理程序(handler);如果仅需要为某个特定的方法注册路由,可使用 Echo.Match(methods []string, path string, h Handler)

Echo 通过 func(echo.Context) error 定义 handler 方法,其中 echo.Context 已经内嵌了 HTTP 请求和响应接口。

url 路径

echo 框架,url 路径有三种写法:

  • 静态url路径
  • 带路径参数的url路径
  • 带星号(*)模糊匹配参数的url路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 例子1, 静态Url路径, 即不带任何参数的url路径
/users/center
/user/101
/food/100

// 例子2,带路径参数的url路径,url路径上面带有参数,参数由冒号(:)跟着一个字符串定义。
// 路径参数值可以是数值,也可以是字符串

// 定义参数:id, 可以匹配/user/1, /user/899 /user/xiaoli 这类Url路径
/user/:id

// 定义参数:id, 可以匹配/food/2, /food/100 /food/apple 这类Url路径
/food/:id

// 定义参数:type和:page, 可以匹配/foods/2/1, /food/100/25 /food/apple/30 这类Url路径
/foods/:type/:page

// 例子3. 带星号(*)模糊匹配参数的url路径
// 星号代表匹配任意路径的意思

// 匹配:/foods/1, /foods/200, /foods/1/20, /foods/apple/1
//以/foods/ 开头的所有路径都匹配
/foods/*

Url 路径匹配顺序

上面列出了三种 Url 路径的写法,那么如果我们项目里面三种路径都有使用,而且出现一个 http 请求匹配多种路径的时候,echo 框架选择哪个路径呢?

例如:一个 http Get 请求的路径为 /user/10。

同时匹配下面三种 url 路径定义:

1
2
3
/user/10
/user/:id
/user/*

如果出现上述,一个 http 请求路径匹配多个定义的url路径,echo 框架按下面顺序匹配,先匹配到那个就用那个定义。

  • 匹配静态 url 路径 (固定路径)
  • 匹配带路径参数的 url 路径 (参数路径)
  • 匹配带星号(*)模糊匹配参数的 url 路径 (匹配所有)

实例

1
2
3
4
5
6
7
8
9
e.GET("/users/:id", func(c echo.Context) error {
return c.String(http.StatusOK, "/users/:id")
})
e.GET("/users/new", func(c echo.Context) error {
return c.String(http.StatusOK, "/users/new")
})
e.GET("/users/1/files/*", func(c echo.Context) error {
return c.String(http.StatusOK, "/users/1/files/*")
})

上面定义的路由将按下面的优先级顺序匹配:

  • /users/new
  • /users/:id
  • /users/1/files/*

路由可以按照任意顺序定义。

组路由

Echo#Group(prefix string, m ...Middleware) *Group

可以将具有相同前缀的路由归为一组从而定义具有可选中间件的新子路由。除了一些特殊的中间件外,组路由也会继承父中间件。若要在组路由中添加中间件,则需使用 Group.Use(m ...Middleware) 。最后,组路由也可以嵌套。

下面的代码,我们创建了一个 admin 组,它需要对 /admin/* 路由进行基本的 HTTP 身份认证。

1
2
3
4
5
6
7
g := e.Group("/admin")
g.Use(middleware.BasicAuth(func(username, password string) bool {
if username == "joe" && password == "secret" {
return true
}
return false
}))

路由命名

每个路由都会返回一个 Route 对象,这个对象可以用来给路由命名。比如:

1
2
3
4
5
6
7
8
routeInfo := e.GET("/users/:id", func(c echo.Context) error {
return c.String(http.StatusOK, "/users/:id")
})
routeInfo.Name = "user"
// 或者这样写
e.GET("/users/new", func(c echo.Context) error {
return c.String(http.StatusOK, "/users/new")
}).Name = "newuser"

当你需要在模版生成 uri 但是又无法获取路由的引用,或者多个路由使用相同的处理器(handler)的时候,路由命名就会显得更方便。

构造URI

Echo#URI(handler HandlerFunc, params ...interface{}) 可以用来在任何业务处理代码里生成带有特殊参数的 URI 。这样在你重构自己的应用程序的时候,可以很方便的集中处理所有的 URI 。

下面的代码中 e.URI(h, 1) 将生成 /users/1

1
2
3
4
5
6
7
// 业务处理
h := func(c echo.Context) error {
return c.String(http.StatusOK, "OK")
}

// 路由
e.GET("/users/:id", h)

除了 Echo#URI,还可以使用 Echo#Reverse(name string, params ...interface{}) 方法根据路由名生成 uri。比如,当 foobar 进行如下设置时,使用 Echo#Reverse("foobar", 1234) 就会生成 /users/1234

1
2
3
4
5
6
7
// Handler
h := func(c echo.Context) error {
return c.String(http.StatusOK, "OK")
}

// Route
e.GET("/users/:id", h).Name = "foobar"

路由列表

Echo#Routes() []*Route 会根据路由定义的顺序列出所有已经注册的路由。每一个路由包含 http 方法,路径和对应的处理器(handler)。 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Handlers
func createUser(c echo.Context) error {
}
func findUser(c echo.Context) error {
}
func updateUser(c echo.Context) error {
}
func deleteUser(c echo.Context) error {
}
// Routes
e.POST("/users", createUser)
e.GET("/users", findUser)
e.PUT("/users", updateUser)
e.DELETE("/users", deleteUser)

用下面的代码你将所有的路由信息输出到 JSON 文件:

1
2
3
4
5
data, err := json.MarshalIndent(e.Routes(), "", "  ")
if err != nil {
return err
}
ioutil.WriteFile("routes.json", data, 0644)

routes.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
{
"method": "POST",
"path": "/users",
"handler": "main.createUser"
},
{
"method": "GET",
"path": "/users",
"handler": "main.findUser"
},
{
"method": "PUT",
"path": "/users",
"handler": "main.updateUser"
},
{
"method": "DELETE",
"path": "/users",
"handler": "main.deleteUser"
}
]

静态文件

例如图片,JavaScript,CSS,PDF,字体文件等等…

echo.Static 函数定义

我们可以通过 echo.Static 函数初始化 static 中间件。

echo.Static 函数定义:

1
Static(prefix, root string) *Route

参数说明:

参数 说明
prefix 静态资源 url 的前缀
root 静态资源文件的根目录, . 代表当前目录

使用 Echo#Static()

Echo#Static(prefix, root string) 使用路径前缀注册一个新路由,以便由根目录提供静态文件。

用法 1

1
2
e := echo.New()
e.Static("/static", "assets")

如上所示, assets 目录中 /static/* 路径下的任何文件都会被访问。例如,一个访问 /static/js/main.js 的请求会匹配到 assets/js/main.js 这个文件。

用法 2

1
2
e := echo.New()
e.Static("/", "assets")

如上所示, assets 目录中 /* 路径下的任何文件都会被访问。例如,一个访问 /js/main.js 的请求将会匹配到 assets/js/main.js 文件。

echo.Static 函数范例

1
2
3
4
5
//初始化echo实例
e := echo.New()

//设置Static中间件
e.Static("/res", "static")

根据这个例子的设置,如果我们访问 /res/codebaoku.jpg 这个url路径,实际上就是访问 static/codebaoku.jpg 这个路径的内容(即访问 static 目录下面 codebaoku.jpg 文件)

Echo.File 函数定义

我们也可以通过 Echo.File 函数为一个 url 地址绑定一个静态资源文件。

echo.File 函数定义:

1
File(url, filename string) *Route

参数说明:

参数 说明
url 静态资源 url
filename 静态资源的文件名

使用 Echo#File()

Echo#File(path, file string) 使用路径注册新路由以提供静态文件。

用法 1

使用 public/index.html 提供索引页面

1
e.File("/", "public/index.html")

用法 2

使用 images/favicon.ico 提供一个图标

1
e.File("/favicon.ico", "images/favicon.ico")

Echo.File 函数范例

1
2
3
4
5
6
7
8
// 初始化echo实例
e := echo.New()

// 访问 / 就是访问public/index.html文件, index.html 相当于站点默认首页
e.File("/", "public/index.html")

// 访问 /favicon.ico 就是访问 images/favicon.ico文件, 相当于为站点设置了图标
e.File("/favicon.ico", "images/favicon.ico")

测试

测试处理程序 (Testing handler)

GET /users/:id

下面的处理程序是根据用户的 id 从数据库取到该用户数据,如果用户不存在则返回 404 和提示语句。

创建 User

POST /users

  • 接受 JSON 格式的关键信息
  • 创建成功返回 201 - Created
  • 发生错误返回 500 - Internal Server Error

获取 User

GET /users/:email

  • 获取成功返回 200 - OK
  • 未获取 User 返回 404 - Not Found
  • 发生其它错误返回 500 - Internal Server Error

handler.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package handler

import (
"net/http"

"github.com/labstack/echo/v4"
)

type (
User struct {
Name string `json:"name" form:"name"`
Email string `json:"email" form:"email"`
}
handler struct {
db map[string]*User
}
)

func (h *handler) createUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return err
}
return c.JSON(http.StatusCreated, u)
}

func (h *handler) getUser(c echo.Context) error {
email := c.Param("email")
user := h.db[email]
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
return c.JSON(http.StatusOK, user)
}

handler_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package handler

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

var (
mockDB = map[string]*User{
"jon@labstack.com": &User{"Jon Snow", "jon@labstack.com"},
}
userJSON = `{"name":"Jon Snow","email":"jon@labstack.com"}`
)

func TestCreateUser(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
h := &handler{mockDB}

// Assertions
if assert.NoError(t, h.createUser(c)) {
assert.Equal(t, http.StatusCreated, rec.Code)
assert.Equal(t, userJSON, rec.Body.String())
}
}

func TestGetUser(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/users/:email")
c.SetParamNames("email")
c.SetParamValues("jon@labstack.com")
h := &handler{mockDB}

// Assertions
if assert.NoError(t, h.getUser(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, userJSON, rec.Body.String())
}
}

使用 Form 表单作为关键信息

1
2
3
4
5
6
// import "net/url"
f := make(url.Values)
f.Set("name", "Jon Snow")
f.Set("email", "jon@labstack.com")
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode()))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)

设置路径 (Path) 参数

1
2
c.SetParamNames("id", "email")
c.SetParamValues("1", "jon@labstack.com")

设置查询 (Query) 参数

1
2
3
4
// import "net/url"
q := make(url.Values)
q.Set("email", "jon@labstack.com")
req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil)

测试中间件

待定 你可以在这里查看框架自带中间件的测试代码。

参考:

Guide | Echo - High performance, minimalist Go web framework (labstack.com)

Echo框架 教程 - 编程宝库 (codebaoku.com)

HttpErrorPages/HttpErrorPages: Simple HTTP Error Page Generator (github.com)

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View