简介
Echo 项目是一个功能强大且用途广泛的 Web 框架,用于使用 Go 编程语言构建可扩展且高性能的 Web 应用程序。它遵循简单、灵活和性能的原则,为开发人员提供构建强大 Web 应用程序的高效工具包。根据官方测试 echo 的速度比 gin 还要更快。
本项目是基于 echo 封装的脚手架,纯后端项目,封装了一些常用的项目基本功能,项目结构如下:
- config:配置读取,使用 iconfig 模块;
- controllers:应用 api;
- engine:echo 实例管理;
- iorm:整合了 iorm 模块,基于 gorm 框架的持久化模块扩展,提供了泛型配置,封装了通用的 crud 方法,事务方法,软删除切换;
- logger:整合了 ilogger 模块,基于 zap 的日志模块封装,日志文件分割配置;
- routers:api 路由注册,路由常用配置;
- main:应用启动入口。
模块
engine
engine 模块主要管理 echo 的实例以及做了一些基础配置,部分代码示例:
var ShutDown func()
func Run() {
mode = iconfig.Server.Mode
engine := echo.New()
engine.Use(middleware.Recover())
// request logger
engine.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339} | ${status} | ${latency_human} | ${remote_ip} | ${method} | ${uri} \n",
Output: ilogger.LoggerWriter(),
}))
// echo logger
engine.Logger.SetHeader("${time_rfc3339} ${level} ${short_file}:${line}")
engine.Logger.SetOutput(ilogger.LoggerWriter())
routers.Router(engine)
ShutDown = func() {
// 禁止接收新请求并等待处理未完成的请求
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := engine.Shutdown(ctx); err != nil {
ilogger.Error(err.Error())
}
}
addr := fmt.Sprintf("%s:%s", iconfig.Server.Addr, iconfig.Server.Port)
go func() {
if err := engine.Start(addr); err != nil && !errors.Is(err, http.ErrServerClosed) {
ilogger.Info("listen: %s\n", err)
}
}()
}
- middleware.Recover() 配置了 recover 中间件,防止应用从 panic 中崩溃。
- middleware.LoggerWithConfig(middleware.LoggerConfig{})) 配置了 echo 的 request logger,这个 logger 是在 echo 接收请求时会输出本次请求的信息,包括 uri,请求 status,请求时间,请求方法等信息,配置了其输出格式以及输出位置与 ilogger 模块保持一致。
- engine.Logger.SetHeader(),engine.Logger.SetOutput() 配置了 echo 的 echo logger,这个 logger 可在 Handler 中使用 ctx.echo().Logger.Info() 输出日志,配置了其输出格式以及输出位置与 ilogger 模块保持一致。
- 异步启动 echo 实例并指定其 ShutDown 函数用于优雅停止应用。
routers
routers 模块提供了 api Handler 注册入口,以及路由配置,部分代码示例如下:
// HandlerRegisterFunc 在根路由组下添加 api
var HandlerRegisterFunc = func(root *echo.Group) {}
func Router(engine *echo.Echo) {
// 入参校验
engine.Validator = &iValidator{validator: validator.New()}
// error 处理
engine.HTTPErrorHandler = errorHandler()
rootGroup := engine.Group("/iecho")
// api
HandlerRegisterFunc(rootGroup)
}
type iValidator struct {
validator *validator.Validate
}
func (cv *iValidator) Validate(md interface{}) error {
return cv.validator.Struct(md)
}
func errorHandler() echo.HTTPErrorHandler {
return func(err error, ctx echo.Context) {
if ctx.Response().Committed {
return
}
var code int
var message string
var ve validator.ValidationErrors
var be *echo.BindingError
var he *echo.HTTPError
switch {
case errors.As(err, &ve):
var stringErrors []string
for _, e := range ve {
stringErrors = append(stringErrors, translate(e))
}
code = http.StatusBadRequest
message = strings.Join(stringErrors, "; ")
case errors.As(err, &he):
code = he.Code
// 404
if code == http.StatusNotFound {
message = "page not found"
} else {
switch m := he.Message.(type) {
case string:
message = m
}
}
case errors.As(err, &be):
code = be.Code
switch m := be.Message.(type) {
case string:
message = m
}
default:
code = http.StatusInternalServerError
message = err.Error()
}
if message == "" {
message = http.StatusText(code)
}
response := models.ErrorResponseWithCode(code, message)
if err = ctx.JSON(response.Code, response); err != nil {
ctx.Logger().Error(err)
}
}
}
func translate(e validator.FieldError) string {
field := e.Field()
switch e.Tag() {
case "required":
return fmt.Sprintf("Field '%s' is required", field)
case "max":
return fmt.Sprintf("Field '%s' must be less or equal to %s", field, e.Param())
case "min":
return fmt.Sprintf("Field '%s' must be more or equal to %s", field, e.Param())
}
return e.Error()
}
- 由于 echo 官方没有提供入参校验功能,官方建议使用 go-playground/validator 配合校验:iValidator。
- errorHandler() 拦截了 validator.ValidationErrors,echo.BindingError,echo.HTTPError 统一错误响应格式,translate() 方法将 validator 中的入参错误信息以更加友好的格式输出。
controllers
controllers 模块用于管理业务 api,部分代码示例如下:
type customType string
func (t *customType) UnmarshalParam(src string) error {
*t = customType("custom" + src)
return nil
}
func init() {
routers.HandlerRegisterFunc = func(root *echo.Group) {
root.Use(tokenFilter)
root.GET("/401", authentication)
root.GET("/500", internalServerError)
root.GET("/query", query)
root.POST("/post", post)
}
}
func authentication(ctx echo.Context) error {
return abortWithCode(401, errors.New("401"))
}
func internalServerError(ctx echo.Context) error {
return abort(errors.New("internal server error"))
}
// http://127.0.0.1:8080/iecho/query?p=p&string=hello&int=123&bool=true&name=zhangsan&ints=1&ints=2&delimiterString=1,2,3&customType=type&token=token
func query(ctx echo.Context) error {
type params struct {
P string `json:"p"`
String string `json:"string"`
Int int `json:"int"`
Bool bool `json:"bool"`
Name string `json:"name"`
Ints []int `json:"ints"`
DelimiterString []string `json:"delimiterString"`
CustomType customType `json:"customType"`
}
md := params{}
md.P = ctx.QueryParam("p")
err := echo.QueryParamsBinder(ctx).
String("string", &md.String).Int("int", &md.Int).Bool("bool", &md.Bool).
MustString("name", &md.Name).
Ints("ints", &md.Ints).
BindWithDelimiter("delimiterString", &md.DelimiterString, ",").
BindUnmarshaler("customType", &md.CustomType).
BindError()
if err != nil {
return err
}
return writeSuccess(ctx, &md)
}
func post(ctx echo.Context) error {
type param struct {
String string `json:"string" validate:"required"`
Int int `json:"int" validate:"min=1,max=10"`
Bool bool `json:"bool"`
Name string `json:"name" validate:"max=5"`
}
md := ¶m{}
err := ctx.Bind(md) // (&echo.DefaultBinder{}).BindBody(ctx, &md)
if err != nil {
return err
}
// valid
err = ctx.Validate(md)
if err != nil {
return err
}
return writeSuccess(ctx, md)
}
// Header Token
func tokenFilter(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
var headers map[string]string
err := (&echo.DefaultBinder{}).BindHeaders(ctx, &headers)
if err != nil {
return err
}
if headers["Token"] == "" {
return abortWithCode(401, errors.New("authorization required"))
}
return next(ctx)
}
}
func abort(err error) error {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
func abortWithCode(code int, err error) error {
return echo.NewHTTPError(code, err.Error())
}
func writeSuccess(ctx echo.Context, data interface{}) error {
return ctx.JSON(http.StatusOK, models.OkResponse(data))
}
- customType 是一个自定义类型,UnmarshalParam 方法返回了该类型的转换值。
- authentication() 与 internalServerError() 抛出了不同状态码的 error 并被统一拦截响应。
- query() 展示了 query 请求中不同参数的获取方式,使用 ctx.QueryParam() 获取单个参数或者使用 echo.QueryParamsBinder(ctx) 链式获取参数并提供不同类型值的获取方法。
- post() 展示了 post 请求下请求体的值的绑定以及请求体值的校验。
- tokenFilter 是一个简单的拦截器,用于校验该请求是否包含 token header,否则返回 401 响应。
main
应用启动并响应中断信号:
func main() {
config.Init("../config.yml")
logger.Init()
orm.Init()
routers.HandlerRegisterFunc = func(root *echo.Group) {
root.GET("/ping", func(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, "hello world")
})
}
engine.Run()
quit := make(chan os.Signal, 1)
// kill -2,kill -15
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
ilogger.Warn("received signal: %s\n", sig)
engine.ShutDown()
orm.Close()
ilogger.Close()
ilogger.Info("server shutdown success")
}
最后
完整代码已经上传仓库:igolang,更多示例请参考模块目录下的 test 文件。