• golang web 方案


    概要

    轻量的基于 golang 的 web 开发实践.

    golang 上手简单, 第三方库丰富, 对于业务没那么复杂的项目, 作为 API 的后端也是不错的选择. 下面是对 golang 作为 API 后端的 web 开发实践总结.

    开发

    API 后端的功能模块基本已经固定, 基于自己的项目, 主要使用了以下模块:

    1. web 框架: 整个方案的核心
    2. 数据库: orm 框架
    3. 认证: 访问的安全
    4. 日志: 辅助调试和运维
    5. 配置: 提高服务的灵活性
    6. 静态文件服务: 部署打包后的前端
    7. 上传/下载: 其实也是 web 框架提供的功能, 单独提出来是因为和一般的 JSON API 不太一样

    web 框架

    golang 的 API 框架有很多, 我在项目中选择了 gin 框架. 当时是出于以下几点考虑:

    1. 成熟度: gin 早就进入 v1 稳定版, 使用的项目也很多, 成熟度没有问题
    2. 性能: gin 的性能在众多 golang web 框架中不是最好的, 但也不差, 具体可以参见 gin 的 README
    3. 活跃度: github 上的 commit 可以看出, gin 虽然很稳定, 更新频率还可以
    4. 周边支持: gin 的插件非常多, 还有个 contrib 项目, 常用的各种插件基本都有, 另外, gin 的插件写起来也很简单

    虽然选择了 gin, 但是本文中使用的各个模块都不是强依赖 gin 的, 替换任何一个模块的代价都不会太大.

    gin 的使用很简单, 主要代码如下:

    r := gin.Default()
    if gin.Mode() == "debug" {
      r.Use(cors.Default())  // 在 debug 模式下, 允许跨域访问
    }
    
    // ... 设置路由的代码
    
    if err := r.Run(":" + strconv.Itoa(port)); err != nil {
      log.Fatal(err)
    }
    

    数据库

    数据库这层, 选用了 beego ORM 框架, 它的文档比较好, 对主流的几种关系数据库也都支持. 表结构的定义:

    type User struct {
      Id       string     `orm:"pk" json:"id"`
      UserName string     `orm:"unique" json:"username"`
      Password string     `json:"password"`
    
      CreateAt time.Time `orm:"auto_now_add"`
      UpdateAt time.Time `orm:"auto_now"`
    }
    
    func init() {
      orm.RegisterModel(new(User))
    }
    

    数据库的初始化:

    // mysql 配置, postgresql 或者 sqlite 使用其他驱动
    orm.RegisterDriver("default", orm.DRMySQL) // 注册驱动
    var conStr = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&loc=Local",
      c.DB.UserName, c.DB.Password, c.DB.Host, c.DB.Port, c.DB.DBName)
    orm.RegisterDataBase("default", "mysql", conStr)
    
    // sync database
    orm.RunSyncdb("default", false, false)
    

    认证

    认证采用 jwt token, 使用了 gin-jwt 中间件. 加了认证中间件之后, 可以配置路由是否需要认证:

    authMiddleware := controller.JwtMiddleware()
    
    // *不需要* 认证的路由
    r.POST("/register", controller.Register)
    r.POST("/login", authMiddleware.LoginHandler)
    
    // *需要* 认证的路由
    authRoute := r.Group("/auth")
    authRoute.Use(authMiddleware.MiddlewareFunc())
    {
      authRoute.GET("/test", func(c *gin.Context) { fmt.Println("hello") })
    }
    

    日志

    项目不是很复杂, 日志采用了文件的方式, 选择了 beego logs 模块. 虽然使用了 beego logs, 但是为了方便以后替换 logs 模块, 在 beego logs 又封装了一层.

    // Logger
    type Logger interface {
      Debug(format string, v ...interface{})
      Info(format string, v ...interface{})
      Warn(format string, v ...interface{})
      Error(format string, v ...interface{})
    }
    
    // 支持 console 和 file 2 种类型的 log
    func InitLogger(level, logType, logFilePath string) error {
      consoleLogger = nil
      fileLogger = nil
    
      if logType == ConsoleLog {
        consoleLogger = NewConsoleLogger(level)  // 这里实际是通过 beego logs 来实现功能的
      } else if logType == FileLog {
        fileLogger = NewFileLogger(logFilePath, level)  // 这里实际是通过 beego logs 来实现功能的
      } else {
        return fmt.Errorf("Log type is not valid
    ")
      }
    
      return nil
    }
    

    配置

    配置采用 toml 格式, 配置文件中一般存放不怎么改变的内容, 改动比较频繁的配置还是放在数据库比较好.

    import (
      "github.com/BurntSushi/toml"
    )
    
    type Config struct {
      Server serverConfig `toml:"server"`
      DB     dbConfig     `toml:"db"`
      Logger loggerConfig `toml:"logger"`
      File   fileConfig   `toml:"file"`
    }
    
    type serverConfig struct {
      Port int `toml:"port"`
    }
    
    type dbConfig struct {
      Port     int    `toml:"port"`
      Host     string `toml:"host"`
      DBName   string `toml:"db_name"`
      UserName string `toml:"user_name"`
      Password string `toml:"password"`
    }
    
    type loggerConfig struct {
      Level   string `toml:"level"`
      Type    string `toml:"type"`
      LogPath string `toml:"logPath"`
    }
    
    type fileConfig struct {
      UploadDir   string `toml:"uploadDir"`
      DownloadDir string `toml:"downloadDir"`
    }
    
    var conf *Config
    
    func GetConfig() *Config {
      return conf
    }
    
    func InitConfig(confPath string) error {
      _, err := toml.DecodeFile(confPath, &conf)
      return err
    }
    

    静态文件服务

    本工程中静态文件服务的目的是为了发布前端. 前端采用 react 开发, build 之后的代码放在静态服务目录中. 使用 gin 框架的静态服务中间件, 很容易实现此功能:

    // static files
    r.Use(static.Serve("/", static.LocalFile("./public", true)))
    
    // 没有路由匹配时, 回到首页
    r.NoRoute(func(c *gin.Context) {
      c.File("./public/index.html")
    })
    

    上传/下载

    上传/下载 在 gin 框架中都有支持.

    • 上传

      func UploadXls(c *gin.Context) {
        // ... 省略的处理
      
        // upload form field name: uploadXls, 这个名字和前端能对上就行
        // file 就是上传文件的文件流
        file, header, err := c.Request.FormFile("uploadXls")
        if err != nil {
          Fail(c, "param error: "+err.Error(), nil)
          return
        }
      
        // ... 省略的处理
      }
      
    • 下载

      func DownloadXls(c *gin.Context) {
        // ... 省略的处理
      
        c.File(downloadPath)
      }
      

    发布

    基于上面几个模块, 一般业务不是很复杂的小应用都可以胜任. 开发之后, 就是打包发布. 因为这个方案是针对小应用的, 所以把前后端都打包到一起作为一个整体发布.

    docker 打包

    之所有采用 docker 方式打包, 是因为这种方式易于分发. docker file 如下:

    # 编译前端
    FROM node:10.15-alpine as front-builder
    
    WORKDIR /user
    ARG VERSION=no-version
    ADD ./frontend/app-ui .
    RUN yarn
    RUN yarn build
    
    
    # 编译前端
    FROM golang:1.12.5-alpine3.9 as back-builder
    
    WORKDIR /go
    RUN mkdir -p ./src/app-api
    ADD ./backend/src/app-api ./src/app-api
    RUN go install app-api
    
    
    # 发布应用 (这里可以用个更小的 linux image)
    FROM golang:1.12.5-alpine3.9
    
    WORKDIR /app
    COPY --from=front-builder /user/build ./public
    COPY --from=back-builder /go/bin/app-api .
    ADD ./deploy/builder/settings.toml .
    
    CMD ["./app-api", "-f", "./settings.toml", "-prod"]
    

    部署中遇到的问题

    时区问题

    docker 的官方 image 基本都是 UTC 时区的, 所以插入数据库的时间一般会慢 8 个小时. 所以, 在 docker 启动或者打包的时候, 需要对时区做一些处理.

    1. 数据库连接的设置

      // 连接字符串中加上: loc=Local
      var conStr = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&loc=Local",
        c.DB.UserName, c.DB.Password, c.DB.Host, c.DB.Port, c.DB.DBName)
      
    2. 数据库镜像的设置 (环境变量中设置时区)

      # -e TZ=Asia/Shanghai 就是设置时区
      docker run --name xxx -e TZ=Asia/Shanghai -d mysql:5.7
      
    3. 应用镜像的设置 (docker-compose.yml) 在 volumes 中设置时区和主机一样

      services:
      user:
        image: xxx:latest
        restart: always
        networks:
          - nnn
        volumes:
          - "/etc/localtime:/etc/localtime:ro"
      
  • 相关阅读:
    FlatBuffers要点
    tarjan+缩点+强连通定理
    编程之美2.16 最长递增子序列
    Android Studio之多个Activity的滑动切换(二)
    Effective java读书札记第一条之 考虑用静态工厂方法取代构造器
    【PM】关于系统数据库和服务现场升级的一些看法
    用户及权限基础 2---- 权限
    Android双向滑动菜单完全解析,教你如何一分钟实现双向滑动特效
    【转贴】gdb中的信号(signal)相关调试技巧
    基于新浪sae使用php生成图片发布图文微博
  • 原文地址:https://www.cnblogs.com/wang_yb/p/10859900.html
Copyright © 2020-2023  润新知