Golang 学习笔记 - 基于 echo 的应用脚手架

406 阅读4分钟

简介

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 := &param{}
    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 文件。