"概要" "開發" "web 框架" "資料庫" "認證" "日誌" "配置" "靜態文件服務" "上傳/下載" "發佈" "docker 打包" "部署中遇到的問題" "時區問題" 概要 輕量的基於 golang 的 web 開發實踐. golang 上手簡單, 第三方庫豐富, 對於業務沒那麼複雜 ...
概要
輕量的基於 golang 的 web 開發實踐.
golang 上手簡單, 第三方庫豐富, 對於業務沒那麼複雜的項目, 作為 API 的後端也是不錯的選擇. 下麵是對 golang 作為 API 後端的 web 開發實踐總結.
開發
API 後端的功能模塊基本已經固定, 基於自己的項目, 主要使用了以下模塊:
- web 框架: 整個方案的核心
- 資料庫: orm 框架
- 認證: 訪問的安全
- 日誌: 輔助調試和運維
- 配置: 提高服務的靈活性
- 靜態文件服務: 部署打包後的前端
- 上傳/下載: 其實也是 web 框架提供的功能, 單獨提出來是因為和一般的 JSON API 不太一樣
web 框架
golang 的 API 框架有很多, 我在項目中選擇了 gin 框架. 當時是出於以下幾點考慮:
- 成熟度: gin 早就進入 v1 穩定版, 使用的項目也很多, 成熟度沒有問題
- 性能: gin 的性能在眾多 golang web 框架中不是最好的, 但也不差, 具體可以參見 gin 的 README
- 活躍度: github 上的 commit 可以看出, gin 雖然很穩定, 更新頻率還可以
- 周邊支持: 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\n")
}
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 啟動或者打包的時候, 需要對時區做一些處理.
資料庫連接的設置
// 連接字元串中加上: 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)
資料庫鏡像的設置 (環境變數中設置時區)
# -e TZ=Asia/Shanghai 就是設置時區 docker run --name xxx -e TZ=Asia/Shanghai -d mysql:5.7
應用鏡像的設置 (docker-compose.yml) 在 volumes 中設置時區和主機一樣
services: user: image: xxx:latest restart: always networks: - nnn volumes: - "/etc/localtime:/etc/localtime:ro"