Go Echo 学习笔记(六)Context


Context上下文信息

echo.Context是Echo中的一个接口,表示的是当前 HTTP 请求所对应的上下文(Request以及Response等相关信息)。

Context介绍

以下是Context接口的定义:

	Context interface {
		// Request returns `*http.Request`.
		Request() *http.Request

		// SetRequest sets `*http.Request`.
		SetRequest(r *http.Request)

		// Response returns `*Response`.
		Response() *Response

		// IsTLS returns true if HTTP connection is TLS otherwise false.
		IsTLS() bool

		// Scheme returns the HTTP protocol scheme, `http` or `https`.
		Scheme() string

		// RealIP returns the client's network address based on `X-Forwarded-For`
		// or `X-Real-IP` request header.
		RealIP() string

		// Path returns the registered path for the handler.
		Path() string

		// SetPath sets the registered path for the handler.
		SetPath(p string)

		// Param returns path parameter by name.
		Param(name string) string

		// ParamNames returns path parameter names.
		ParamNames() []string

		// SetParamNames sets path parameter names.
		SetParamNames(names ...string)

		// ParamValues returns path parameter values.
		ParamValues() []string

		// SetParamValues sets path parameter values.
		SetParamValues(values ...string)

		// QueryParam returns the query param for the provided name.
		QueryParam(name string) string

		// QueryParams returns the query parameters as `url.Values`.
		QueryParams() url.Values

		// QueryString returns the URL query string.
		QueryString() string

		// FormValue returns the form field value for the provided name.
		FormValue(name string) string

		// FormParams returns the form parameters as `url.Values`.
		FormParams() (url.Values, error)

		// FormFile returns the multipart form file for the provided name.
		FormFile(name string) (*multipart.FileHeader, error)

		// MultipartForm returns the multipart form.
		MultipartForm() (*multipart.Form, error)

		// Cookie returns the named cookie provided in the request.
		Cookie(name string) (*http.Cookie, error)

		// SetCookie adds a `Set-Cookie` header in HTTP response.
		SetCookie(cookie *http.Cookie)

		// Cookies returns the HTTP cookies sent with the request.
		Cookies() []*http.Cookie

		// Get retrieves data from the context.
		Get(key string) interface{}

		// Set saves data in the context.
		Set(key string, val interface{})

		// Bind binds the request body into provided type `i`. The default binder
		// does it based on Content-Type header.
		Bind(i interface{}) error

		// Validate validates provided `i`. It is usually called after `Context#Bind()`.
		// Validator must be registered using `Echo#Validator`.
		Validate(i interface{}) error

		// Render renders a template with data and sends a text/html response with status
		// code. Renderer must be registered using `Echo.Renderer`.
		Render(code int, name string, data interface{}) error

		// HTML sends an HTTP response with status code.
		HTML(code int, html string) error

		// HTMLBlob sends an HTTP blob response with status code.
		HTMLBlob(code int, b []byte) error

		// String sends a string response with status code.
		String(code int, s string) error

		// JSON sends a JSON response with status code.
		JSON(code int, i interface{}) error

		// JSONPretty sends a pretty-print JSON with status code.
		JSONPretty(code int, i interface{}, indent string) error

		// JSONBlob sends a JSON blob response with status code.
		JSONBlob(code int, b []byte) error

		// JSONP sends a JSONP response with status code. It uses `callback` to construct
		// the JSONP payload.
		JSONP(code int, callback string, i interface{}) error

		// JSONPBlob sends a JSONP blob response with status code. It uses `callback`
		// to construct the JSONP payload.
		JSONPBlob(code int, callback string, b []byte) error

		// XML sends an XML response with status code.
		XML(code int, i interface{}) error

		// XMLPretty sends a pretty-print XML with status code.
		XMLPretty(code int, i interface{}, indent string) error

		// XMLBlob sends an XML blob response with status code.
		XMLBlob(code int, b []byte) error

		// Blob sends a blob response with status code and content type.
		Blob(code int, contentType string, b []byte) error

		// Stream sends a streaming response with status code and content type.
		Stream(code int, contentType string, r io.Reader) error

		// File sends a response with the content of the file.
		File(file string) error

		// Attachment sends a response as attachment, prompting client to save the
		// file.
		Attachment(file string, name string) error

		// Inline sends a response as inline, opening the file in the browser.
		Inline(file string, name string) error

		// NoContent sends a response with no body and a status code.
		NoContent(code int) error

		// Redirect redirects the request to a provided URL with status code.
		Redirect(code int, url string) error

		// Error invokes the registered HTTP error handler. Generally used by middleware.
		Error(err error)

		// Handler returns the matched handler by echo.
		Handler() HandlerFunc

		// SetHandler sets the matched handler by echo.
		SetHandler(h HandlerFunc)

		// Logger returns the `Logger` instance with context and bamai log style.
		Logger() BamaiLogger

		// OriginalLogger return `Logger` without context
		OriginalLogger() Logger

		// Echo returns the `Echo` instance.
		Echo() *Echo

		// Reset resets the context after request completes. It must be called along
		// with `Echo#AcquireContext()` and `Echo#ReleaseContext()`.
		// See `Echo#ServeHTTP()`
		Reset(r *http.Request, w http.ResponseWriter)
	}

利用 Context 可以获取当前请求对应的相关数据,并且将对应的数据在写入到context当中。可以说,Echo中最重要的东西就是 Context,其贯穿在请求的整个生命周期中。

不过在一般的项目中原生的Context并不能很好的满足一些项目的定制化需求,因此Echo也支持利用中间件来自定义化 Context:

// 定义一个CustomContext结构体
type CustomContext struct {
	echo.Context // 在原有的Context上进行包装
}
func (c *CustomContext) Foo() {
	println("foo")
}
func (c *CustomContext) Bar() {
	println("bar")
}

// 利用中间件来扩展当前的CustomContext
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		cc := &CustomContext{c}
		return next(cc)
	}
})

// 在请求中进行使用
e.GET("/", func(c echo.Context) error {
	cc := c.(*CustomContext) // 断言验证当前的context类型
	cc.Foo()
	cc.Bar()
	return cc.String(200, "OK")
})

Context 中用的最多的便是获取请求的相关信息,然后进行相应的操作之后返回对应的响应信息。因此接下来介绍 Context 处理请求以及响应的相关方法。

Context-Request

获取请求数据

Context 中的 Request() 方法可以返回一个 *http.Request 指针类型对象,通过该对象可以获取到请求的相关信息:

type Request struct {
	// Method specifies the HTTP method (GET, POST, PUT, etc.).
	// For client requests, an empty string means GET.
	//
	// Go's HTTP client does not support sending a request with
	// the CONNECT method. See the documentation on Transport for
	// details.
	Method string

	// URL specifies either the URI being requested (for server
	// requests) or the URL to access (for client requests).
	//
	// For server requests, the URL is parsed from the URI
	// supplied on the Request-Line as stored in RequestURI.  For
	// most requests, fields other than Path and RawQuery will be
	// empty. (See RFC 7230, Section 5.3)
	//
	// For client requests, the URL's Host specifies the server to
	// connect to, while the Request's Host field optionally
	// specifies the Host header value to send in the HTTP
	// request.
	URL *url.URL

	// The protocol version for incoming server requests.
	//
	// For client requests, these fields are ignored. The HTTP
	// client code always uses either HTTP/1.1 or HTTP/2.
	// See the docs on Transport for details.
	Proto      string // "HTTP/1.0"
	ProtoMajor int    // 1
	ProtoMinor int    // 0

	// Header contains the request header fields either received
	// by the server or to be sent by the client.
	//
	// If a server received a request with header lines,
	//
	//	Host: example.com
	//	accept-encoding: gzip, deflate
	//	Accept-Language: en-us
	//	fOO: Bar
	//	foo: two
	//
	// then
	//
	//	Header = map[string][]string{
	//		"Accept-Encoding": {"gzip, deflate"},
	//		"Accept-Language": {"en-us"},
	//		"Foo": {"Bar", "two"},
	//	}
	//
	// For incoming requests, the Host header is promoted to the
	// Request.Host field and removed from the Header map.
	//
	// HTTP defines that header names are case-insensitive. The
	// request parser implements this by using CanonicalHeaderKey,
	// making the first character and any characters following a
	// hyphen uppercase and the rest lowercase.
	//
	// For client requests, certain headers such as Content-Length
	// and Connection are automatically written when needed and
	// values in Header may be ignored. See the documentation
	// for the Request.Write method.
	Header Header

	// Body is the request's body.
	//
	// For client requests, a nil body means the request has no
	// body, such as a GET request. The HTTP Client's Transport
	// is responsible for calling the Close method.
	//
	// For server requests, the Request Body is always non-nil
	// but will return EOF immediately when no body is present.
	// The Server will close the request body. The ServeHTTP
	// Handler does not need to.
	//
	// Body must allow Read to be called concurrently with Close.
	// In particular, calling Close should unblock a Read waiting
	// for input.
	Body io.ReadCloser

	// GetBody defines an optional func to return a new copy of
	// Body. It is used for client requests when a redirect requires
	// reading the body more than once. Use of GetBody still
	// requires setting Body.
	//
	// For server requests, it is unused.
	GetBody func() (io.ReadCloser, error)

	// ContentLength records the length of the associated content.
	// The value -1 indicates that the length is unknown.
	// Values >= 0 indicate that the given number of bytes may
	// be read from Body.
	//
	// For client requests, a value of 0 with a non-nil Body is
	// also treated as unknown.
	ContentLength int64

	// TransferEncoding lists the transfer encodings from outermost to
	// innermost. An empty list denotes the "identity" encoding.
	// TransferEncoding can usually be ignored; chunked encoding is
	// automatically added and removed as necessary when sending and
	// receiving requests.
	TransferEncoding []string

	// Close indicates whether to close the connection after
	// replying to this request (for servers) or after sending this
	// request and reading its response (for clients).
	//
	// For server requests, the HTTP server handles this automatically
	// and this field is not needed by Handlers.
	//
	// For client requests, setting this field prevents re-use of
	// TCP connections between requests to the same hosts, as if
	// Transport.DisableKeepAlives were set.
	Close bool

	// For server requests, Host specifies the host on which the
	// URL is sought. For HTTP/1 (per RFC 7230, section 5.4), this
	// is either the value of the "Host" header or the host name
	// given in the URL itself. For HTTP/2, it is the value of the
	// ":authority" pseudo-header field.
	// It may be of the form "host:port". For international domain
	// names, Host may be in Punycode or Unicode form. Use
	// golang.org/x/net/idna to convert it to either format if
	// needed.
	// To prevent DNS rebinding attacks, server Handlers should
	// validate that the Host header has a value for which the
	// Handler considers itself authoritative. The included
	// ServeMux supports patterns registered to particular host
	// names and thus protects its registered Handlers.
	//
	// For client requests, Host optionally overrides the Host
	// header to send. If empty, the Request.Write method uses
	// the value of URL.Host. Host may contain an international
	// domain name.
	Host string

	// Form contains the parsed form data, including both the URL
	// field's query parameters and the PATCH, POST, or PUT form data.
	// This field is only available after ParseForm is called.
	// The HTTP client ignores Form and uses Body instead.
	Form url.Values

	// PostForm contains the parsed form data from PATCH, POST
	// or PUT body parameters.
	//
	// This field is only available after ParseForm is called.
	// The HTTP client ignores PostForm and uses Body instead.
	PostForm url.Values

	// MultipartForm is the parsed multipart form, including file uploads.
	// This field is only available after ParseMultipartForm is called.
	// The HTTP client ignores MultipartForm and uses Body instead.
	MultipartForm *multipart.Form

	// Trailer specifies additional headers that are sent after the request
	// body.
	//
	// For server requests, the Trailer map initially contains only the
	// trailer keys, with nil values. (The client declares which trailers it
	// will later send.)  While the handler is reading from Body, it must
	// not reference Trailer. After reading from Body returns EOF, Trailer
	// can be read again and will contain non-nil values, if they were sent
	// by the client.
	//
	// For client requests, Trailer must be initialized to a map containing
	// the trailer keys to later send. The values may be nil or their final
	// values. The ContentLength must be 0 or -1, to send a chunked request.
	// After the HTTP request is sent the map values can be updated while
	// the request body is read. Once the body returns EOF, the caller must
	// not mutate Trailer.
	//
	// Few HTTP clients, servers, or proxies support HTTP trailers.
	Trailer Header

	// RemoteAddr allows HTTP servers and other software to record
	// the network address that sent the request, usually for
	// logging. This field is not filled in by ReadRequest and
	// has no defined format. The HTTP server in this package
	// sets RemoteAddr to an "IP:port" address before invoking a
	// handler.
	// This field is ignored by the HTTP client.
	RemoteAddr string

	// RequestURI is the unmodified request-target of the
	// Request-Line (RFC 7230, Section 3.1.1) as sent by the client
	// to a server. Usually the URL field should be used instead.
	// It is an error to set this field in an HTTP client request.
	RequestURI string

	// TLS allows HTTP servers and other software to record
	// information about the TLS connection on which the request
	// was received. This field is not filled in by ReadRequest.
	// The HTTP server in this package sets the field for
	// TLS-enabled connections before invoking a handler;
	// otherwise it leaves the field nil.
	// This field is ignored by the HTTP client.
	TLS *tls.ConnectionState

	// Cancel is an optional channel whose closure indicates that the client
	// request should be regarded as canceled. Not all implementations of
	// RoundTripper may support Cancel.
	//
	// For server requests, this field is not applicable.
	//
	// Deprecated: Set the Request's context with NewRequestWithContext
	// instead. If a Request's Cancel field and context are both
	// set, it is undefined whether Cancel is respected.
	Cancel <-chan struct{}

	// Response is the redirect response which caused this request
	// to be created. This field is only populated during client
	// redirects.
	Response *Response

	// ctx is either the client or server context. It should only
	// be modified via copying the whole Request using WithContext.
	// It is unexported to prevent people from using Context wrong
	// and mutating the contexts held by callers of the same request.
	ctx context.Context
}

虽然 Request 对象中也有获取请求数据的方法,但是Context也提供了更加便捷的获取请求数据的方法:

// c为context对象,返回都是字符串
// 获取form表单中的数据
name := c.FormValue("name")
// 获取Get请求参数的数据
name := c.QueryParam("name")
// 获取url路径中的数据
name := c.Param("name")
// 通过FormFile函数获取客户端上传的文件
file, err := c.FormFile("file")

请求数据验证

针对请求的数据是否为必须传入的验证称为请求数据验证操作,Echo 没有内置的数据验证函数,可以自己进行定制然后使用 Echo#Validator 来进行注册,或者使用第三方所提供的(https://github.com/go-playground/validator )。

自定义请求数据验证案例:

// User 用户结构体
type User struct {
	Name  string `json:"name" form:"name" query:"name" validate:"required"`
	Email string `json:"email" form:"email" query:"email" validate:"required,email"`
}

// CustomValidator 自定义数据验证
type CustomValidator struct {
	Validator *validator.Validate
}

// Validate CustomValidate接口对应的数据验证方法
func (cv *CustomValidator) Validate(i interface{}) error {
	// 对结构体数据进行验证
	if err := cv.Validator.Struct(i); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
	}
	return nil
}

// 设置数据验证器
e.Validator = &model.CustomValidator{Validator: validator.New()}

// 在请求处理函数中进行数据验证
if err := c.Validate(u); err != nil {
	return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

针对错误的email请求数据会返回相应的错误:

{
    "error": "code=400, message=code=500, message=Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag",
    "message": "code=500, message=Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag"
}

Context-Response

Echo 支持返回 String、HTML、Json、xml、file等格式的数据。

返回String

要向客户端返回 String,需要用到 Context#String(code int, s string) 方法:

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

返回HTML

要向客户端返回对应的 HTML页面信息(纯文本),需要用到 Context#HTML(code int, html string) 方法:

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

想要输出数据流形式的 HTML 页面信息,需要使用 Context#HTMLBlob(code int, b []byte) 方法。

返回Json

要向客户端返回 Json 数据,需要用到 Context#JSON(code int, i interface{}) 方法,该方法会将一个 Go 中的 type 类型的对象编码为 json 并将其配合状态码响应给客户端:

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

// Handler
func(c echo.Context) error {
  u := &User{
    Name:  "Jon",
    Email: "jon@labstack.com",
  }
  return c.JSON(http.StatusOK, u)
}

Context#JSON(code int, i interface{}) 方法在内部使用到了 json.MarShal() 方法来将 type 对象编码为 Json 格式的数据,对于比较复杂的 type 可能会出错,因此可以使用流式 Json 来手动编码 Json:

func(c echo.Context) error {
  u := &User{
    Name:  "Jon",
    Email: "jon@labstack.com",
  }
  c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
  c.Response().WriteHeader(http.StatusOK)
  return json.NewEncoder(c.Response()).Encode(u)
}

同时 Echo 也提供了美观的 Json 响应数据提供方法:Context#JSONPretty(code int, i interface{}, indent string) ,该方法会按照指定的 indent 来对要返回的 Json 数据进行缩进:

func(c echo.Context) error {
  u := &User{
    Name:  "Jon",
    Email: "joe@labstack.com",
  }
  return c.JSONPretty(http.StatusOK, u, "  ")
}

// 显示效果
{
  "email": "joe@labstack.com",
  "name": "Jon"
}

同时 Echo 也支持将已经编码好的 Json 数据返回给对应的客户端,使用的是:Context#JSONBlob(code int, b []byte) 方法:

func(c echo.Context) error {
  encodedJSON := []byte{} // Encoded JSON from external source
  return c.JSONBlob(http.StatusOK, encodedJSON)
}

对于需要进行跨域服务器调用的场景,Echo 可以使用 JSONP 方法来实现,JSONP是一种允许跨域服务器调用的方法:

func TestJsonP(c echo.Context) error {
	callback := c.QueryParam("callback")
	var content struct {
		Response  string    `json:"response"`
		Timestamp time.Time `json:"timestamp"`
		Random    int       `json:"random"`
	}
	content.Response = "Sent via JSONP"
	content.Timestamp = time.Now().UTC()
	content.Random = rand.Intn(1000)
	return c.JSONP(http.StatusOK, callback, &content)
}

对应的请求处理:

http://localhost:8080/jsonp?callback=jQuery111106157039283048003_1619404341601&_=1619404341634

返回的结果是:

jQuery111106157039283048003_1619404341601({
  "response": "Sent via JSONP",
  "timestamp": "2021-04-26T02:39:03.681172Z",
  "random": 825
}
);

返回XML

Echo 利用 Context#XML(code int, i interface{}) 方法来将一个结构体对象编码为 XML 进行返回。

func(c echo.Context) error {
  u := &User{
    Name:  "Jon",
    Email: "jon@labstack.com",
  }
  return c.XML(http.StatusOK, u)
}

同样的, Context#XML(code int, i interface{}) 中使用到了 xml.Marshal() 方法来完成结构体对象到 XML文本的编码过程,对于一些较为复杂的对象也可以使用如下的方式生成原生的 XML 文本:

func(c echo.Context) error {
  u := &User{
    Name:  "Jon",
    Email: "jon@labstack.com",
  }
  c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
  c.Response().WriteHeader(http.StatusOK)
  return xml.NewEncoder(c.Response()).Encode(u)
}

同时,Context#XMLPretty(code int, i interface{}, indent string) 方法也可以支持对返回的 XML 文本进行缩进排版:

func(c echo.Context) error {
  u := &User{
    Name:  "Jon",
    Email: "joe@labstack.com",
  }
  return c.XMLPretty(http.StatusOK, u, "  ")
}

显示效果如下:

<?xml version="1.0" encoding="UTF-8"?>
<User>
  <Name>Jon</Name>
  <Email>joe@labstack.com</Email>
</User>

对于已经编码好的XML 文本来说,可以使用 Context#XMLBlob(code int, b []byte) 方法来将其返回给客户端。

返回文件

Echo 可以使用 Context#File(file string) 方法来向客户端返回一个文件并自动处理相应的缓存问题。

func(c echo.Context) error {
  return c.File("<PATH_TO_YOUR_FILE>")
}

同时 Echo 还提供了 Context#Attachment(file, name string) 方法来将文件按照给定的名称作为附件返回给客户端:

func(c echo.Context) error {
  return c.Attachment("<PATH_TO_YOUR_FILE>", "<ATTACHMENT_NAME>")
}

对于多个文件需要排队返回给客户端的情况下,可以使用 Context#Inline(file, name string) 来进行返回:

func(c echo.Context) error {
  return c.Inline("<PATH_TO_YOUR_FILE>")
}

返回其他格式文件

对于一些没有专门方法支持的文件类型的返回可以利用 Context#Blob(code int, contentType string, b []byte) 来指定返回的文件类型以及对应的文件数据:

func(c echo.Context) (err error) {
  data := []byte(`0306703,0035866,NO_ACTION,06/19/2006
	  0086003,"0005866",UPDATED,06/19/2006`)
	return c.Blob(http.StatusOK, "text/csv", data)
}

返回数据流文件

对于数据流类型的文件来说,可以使用 Context#Stream(code int, contentType string, r io.Reader)来流式返回给客户端:

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)
}

返回空内容

对于没有内容需要返回的话可以利用 Context#NoContent(code int) 只传递一个对应的状态码就可以:

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

重定向

Context#Redirect(code int, url string) 方法可以将请求重定义到一个指定的 url 上:

func(c echo.Context) error {
  return c.Redirect(http.StatusMovedPermanently, "<URL>")
}

Hooks钩子方法

Echo 支持在请求的响应被写入的前后过程中注册相应的钩子方法(支持注册多个方法):

  • Context#Response#Before(func()):所注册的方法在响应被写入之前调用;
  • Context#Response#After(func()):所注册的方法在响应被写入之后调用(但假如“ Content-Length”未知,则不会执行after函数。);
func(c echo.Context) error {
  c.Response().Before(func() {
    println("before response")
  })
  c.Response().After(func() {
    println("after response")
  })
  return c.NoContent(http.StatusNoContent)
}