当前位置: 移动技术网 > IT编程>脚本编程>Go语言 > golang web 方案

golang web 方案

2019年05月14日  | 移动技术网IT编程  | 我要评论
"概要" "开发" "web 框架" "数据库" "认证" "日志" "配置" "静态文件服务" "上传/下载" "发布" "docker 打包" "部署中遇到的问题" "时区问题" 概要 轻量的基于 golang 的 web 开发实践. golang 上手简单, 第三方库丰富, 对于业务没那么复杂 ...

概要

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

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

开发

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

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

web 框架

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

  1. 成熟度: gin 早就进入 v1 稳定版, 使用的项目也很多, 成熟度没有问题
  2. 性能: gin 的性能在众多 golang web 框架中不是最好的, 但也不差, 具体可以参见 gin 的 readme
  3. 活跃度: github 上的 commit 可以看出, gin 虽然很稳定, 更新频率还可以
  4. 周边支持: gin 的插件非常多, 还有个 项目, 常用的各种插件基本都有, 另外, 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, 使用了 中间件. 加了认证中间件之后, 可以配置路由是否需要认证:

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, 但是为了方便以后替换 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\n")
  }

  return nil
}

配置

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

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

上传/下载

上传/下载 在 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"

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网