go 1.13

自定義配置與讀取:

在我們使用redis和mysql之前,我們先來讀取一下配置,配置呢我們使用常見的yaml,當(dāng)然你也可以使用其他,比如env等。

新建config目錄,用來讀取與監(jiān)聽配置文件(config.yaml):

package config

import (
"fmt"

"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)

type Config struct {
Name string
}

// 對外的初始化配置方法
func Run(cfg string) error {
c := Config{
Name: cfg,
}

if err := c.init(); err != nil {
return err
}

c.watchConfig()

return nil
}

func (c *Config) init() error {
if c.Name != "" {
viper.SetConfigFile(c.Name)
} else {
// 默認(rèn)配置文件是./config.yaml
viper.AddConfigPath(".")
viper.SetConfigName("config")
}

viper.SetConfigType("yaml")
// viper解析配置文件
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

// 簡單打印下配置
fmt.Println(viper.GetString("name"))

return nil
}

func (c *Config) watchConfig() {
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
}

main:

package main

import (
"github.com/spf13/pflag"

"sai0556/demo2-gin-frame/config"
)

var (
conf = pflag.StringP("config", "c", "", "config filepath")
)

func main() {
pflag.Parse()

// 初始化配置
if err := config.Run(*conf); err != nil {
panic(err)
}

}

這里有用到大牛spf13的兩個(gè)包,pflag和viper,命令行參數(shù)解析包pflag可以看作flag的進(jìn)階版本,在我們這里可以用來指定配置文件,viper是讀取配置文件的包,配合fsnotify可以實(shí)現(xiàn)配置的熱更新。(spf13大神還有其他有用的包,相信在你的go編碼生涯會(huì)用到的)

寫完我們可以運(yùn)行一下:

go run main.go -c ./config.yaml

可以看到有打印出我們配置的name。

整合mysql與redis

mysql包我們就選用gorm,redis的使用比較多的是redigo和go-redis,redigo曾在我使用中出現(xiàn)過問題,因而我們選擇后者,后者也支持連接池。

mysql:

package db

import (
"fmt"
"sync"
"errors"

orm "github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/spf13/viper"
)

type MySqlPool struct {}

var instance *MySqlPool
var once sync.Once

var db *orm.DB
var err error

// 單例模式
func GetInstance() *MySqlPool {
once.Do(func() {
instance = &MySqlPool{}
})

return instance
}

func (pool *MySqlPool) InitPool() (isSuc bool) {
// 這里有一種常見的拼接字符串的方式
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=%s", viper.GetString("db.username"), viper.GetString("db.password"), viper.GetString("db.host"), viper.GetString("db.name"), viper.GetString("db.charset"))
db, err = orm.Open("mysql", dsn)
if err != nil {
panic(errors.New("mysql連接失敗"))
return false
}

// 連接數(shù)配置也可以寫入配置,在此讀取
db.DB().SetMaxIdleConns(viper.GetInt("db.MaxIdleConns"))
db.DB().SetMaxOpenConns(viper.GetInt("db.MaxOpenConns"))
// db.LogMode(true)
return true
}

后面獲取連接池就可以直接使用 db.GetInstance()

redis:

package db

import (
"fmt"

"github.com/spf13/viper"
"github.com/go-redis/redis"
)

var RedisClient *redis.Client

func InitRedis() {
RedisClient = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", viper.GetString("redis.host"), viper.GetString("redis.port")),
Password: viper.GetString("redis.auth"),
DB: 0,
})

_, err := RedisClient.Ping().Result()
if err != nil {
panic("redis ping error")
}
}

RedisClient就是我們后面可以用的redis連接池。

在main中加入初始化連接池的代碼即可:// 連接mysql數(shù)據(jù)庫

btn := db.GetInstance().InitPool()
if !btn {
log.Println("init database pool failure...")
panic(errors.New("init database pool failure"))
}

// redis
db.InitRedis()

路由與控制器

為了方便路由,我們把路由管理單獨(dú)到router。

package router

import (
"net/http"

"github.com/gin-gonic/gin"

"sai0556/demo2-gin-frame/controller"
)

func Load(g *gin.Engine) *gin.Engine {
g.Use(gin.Recovery())
// 404
g.NoRoute(func (c *gin.Context) {
c.String(http.StatusNotFound, "404 not found");
})

g.GET("/", controller.Index)

return g
}

controller:

package controller

import (
"net/http"

"github.com/gin-gonic/gin"
)

// 返回
type Response struct {
Code int json:"code" Message string json:"message" Data interface{} json:"data" } // api返回結(jié)構(gòu) func ApiResponse(c *gin.Context, code int, message string, data interface{}) { c.JSON(http.StatusOK, Response{ Code: code, Message: message, Data: data, }) } func Index(c *gin.Context) { ApiResponse(c, 0, "success", nil) }

到這呢,基本也就差不多了。

我們來看下完整的main:

package main

// import 這里我習(xí)慣把官方庫,開源庫,本地module依次分開列出
import (
"log"
"errors"

"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/gin-gonic/gin"

"sai0556/demo2-gin-frame/config"
"sai0556/demo2-gin-frame/db"
"sai0556/demo2-gin-frame/router"
)

var (
conf = pflag.StringP("config", "c", "", "config filepath")
)

func main() {
pflag.Parse()

// 初始化配置
if err := config.Run(*conf); err != nil {
panic(err)
}

// 連接mysql數(shù)據(jù)庫
btn := db.GetInstance().InitPool()
if !btn {
log.Println("init database pool failure...")
panic(errors.New("init database pool failure"))
}

// redis
db.InitRedis()

gin.SetMode(viper.GetString("mode"))
g := gin.New()
g = router.Load(g)
g.Run(viper.GetString("addr"))
}

整合日志

這里我們先定義下log:

log:
level: debug # 日志級(jí)別,info,debug,error
file_format: "%Y%m%d" # 文件格式
max_save_days: 30 # 保存天數(shù)
file_type: one # one, level 單文件存儲(chǔ)還是以level級(jí)別存儲(chǔ)

整合logger:

package logger

import (
"io"
"log"
"time"

"github.com/lestrrat-go/file-rotatelogs"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/spf13/viper"
)

var Logger *zap.Logger
var LogLevel string
var FileFormat string

// 初始化日志 logger
func init() {
// 設(shè)置一些基本日志格式
config := zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
EncodeLevel: zapcore.CapitalLevelEncoder,
TimeKey: "ts",
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05"))
},
CallerKey: "file",
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendInt64(int64(d) / 1000000)
},
}
encoder := zapcore.NewConsoleEncoder(config)

FileFormat, saveType, LogLevel := "%Y%m%d", "one", "info"

if viper.IsSet("log.file_format") {
FileFormat = viper.GetString("log.file_format")
}

if viper.IsSet("log.level") {
LogLevel = viper.GetString("log.level")
}

if viper.IsSet("log.save_type") {
saveType = viper.GetString("log.save_type")
}

logLevel := zap.DebugLevel
switch LogLevel {
case "debug":
logLevel = zap.DebugLevel
case "info":
logLevel = zap.InfoLevel
case "error":
logLevel = zap.ErrorLevel
default:
logLevel = zap.InfoLevel
}

switch saveType {
case "level":
Logger = getLevelLogger(encoder, logLevel, FileFormat)
default:
Logger = getOneLogger(encoder, logLevel, FileFormat)
}

}

func getLevelLogger(encoder zapcore.Encoder, logLevel zapcore.Level, fileFormat string) *zap.Logger {
infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl == zapcore.InfoLevel && lvl >= logLevel
})

debugLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl == zapcore.DebugLevel && lvl >= logLevel
})

errorLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.ErrorLevel && lvl >= logLevel
})
// 獲取 info、warn日志文件的io.Writer 抽象 getLoggerWriter() 在下方實(shí)現(xiàn)
infoWriter := getLoggerWriter("./log/info", fileFormat)
errorWriter := getLoggerWriter("./log/error", fileFormat)
debugWriter := getLoggerWriter("./log/debug", fileFormat)

// 最后創(chuàng)建具體的Logger
core := zapcore.NewTee(
zapcore.NewCore(encoder, zapcore.AddSync(debugWriter), debugLevel),
zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), infoLevel),
zapcore.NewCore(encoder, zapcore.AddSync(errorWriter), errorLevel),
)
return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.WarnLevel))
}

func getOneLogger(encoder zapcore.Encoder, logLevel zapcore.Level, fileFormat string) *zap.Logger {
infoWriter := getLoggerWriter("./log/info", fileFormat)

infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl == zapcore.InfoLevel && lvl >= logLevel
})

core := zapcore.NewTee(
zapcore.NewCore(encoder, zapcore.AddSync(infoWriter), infoLevel),
)
return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.WarnLevel))
}

func getLoggerWriter(filename, fileFormat string) io.Writer {
// 生成rotatelogs的Logger 實(shí)際生成的文件名 file_YYmmddHH.log
hook, err := rotatelogs.New(
filename+fileFormat+".log",
rotatelogs.WithLinkName(filename),
// 保存天數(shù)
rotatelogs.WithMaxAge(time.Hour*24*30),
// 切割頻率24小時(shí)
rotatelogs.WithRotationTime(time.Hour*24),
)
if err != nil {
log.Println("日志啟動(dòng)異常")
panic(err)
}
return hook
}

func Debug(format string, v ...interface{}) {
Logger.Sugar().Debugf(format, v...)
}

func Info(format string, v ...interface{}) {
Logger.Sugar().Infof(format, v...)
}

func Error(format string, v ...interface{}) {
Logger.Sugar().Errorf(format, v...)
}

這里注意init函數(shù),我們直接調(diào)用logger其中函數(shù)即可,程序加載包的過程中會(huì)自動(dòng)執(zhí)行init函數(shù)。關(guān)于init有以下說明:

  1. init函數(shù)是用于程序執(zhí)行前做包的初始化的函數(shù),比如初始化包里的變量等
  2. 每個(gè)包可以擁有多個(gè)init函數(shù)
  3. 包的每個(gè)源文件也可以擁有多個(gè)init函數(shù)
  4. 同一個(gè)包中多個(gè)init函數(shù)的執(zhí)行順序go語言沒有明確的定義(說明)
  5. 不同包的init函數(shù)按照包導(dǎo)入的依賴關(guān)系決定該初始化函數(shù)的執(zhí)行順序
  6. init函數(shù)不能被其他函數(shù)調(diào)用,而是在main函數(shù)執(zhí)行之前,自動(dòng)被調(diào)用

我們直接使用:

logger.Info("i'm log123-----Info")
logger.Error("i'm log123-----Error")

平滑重啟

當(dāng)程序在線上穩(wěn)定運(yùn)行后,我們可能會(huì)去更新一些功能,但發(fā)布代碼的同時(shí),假如有用戶正在使用,盲目發(fā)布代碼可能會(huì)造成用戶短暫失真,這時(shí)候平滑重啟就來了。

對于平滑重啟,其實(shí)有很多方案,這里我們只從自身代碼級(jí)別來完成,而即便是代碼級(jí)別,目前也有多種實(shí)現(xiàn)方案,比如第三方庫endless這種,我這里主要參考了

https://github.com/kuangchanglang/gracefulgithub.com

簡單說明下處理步驟:

  1. 監(jiān)聽信號(hào)(USR2,可自定義其他信號(hào))
  2. 收到信號(hào)時(shí)fork子進(jìn)程(使用相同的啟動(dòng)命令),將服務(wù)監(jiān)聽的socket文件描述符傳遞給子進(jìn)程
  3. 子進(jìn)程監(jiān)聽父進(jìn)程的socket,這個(gè)時(shí)候父進(jìn)程和子進(jìn)程都可以接收請求
  4. 子進(jìn)程啟動(dòng)成功之后,父進(jìn)程停止接收新的連接,等待舊連接處理完成(或超時(shí))
  5. 父進(jìn)程退出,重啟完成

詳細(xì)分析可看底部參考-Golang服務(wù)器熱重啟、熱升級(jí)、熱更新

啟動(dòng)檢查

結(jié)合上面的優(yōu)雅重啟,我們在啟動(dòng)時(shí)配置上啟動(dòng)健康檢查:

package main

// import 這里我習(xí)慣把官方庫,開源庫,本地module依次分開列出
import (
"fmt"
"time"
"errors"
"net/http"

"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/gin-gonic/gin"
"sai0556/demo2-gin-frame/config"
"sai0556/demo2-gin-frame/db"
"sai0556/demo2-gin-frame/router"
"sai0556/demo2-gin-frame/logger"
"sai0556/demo2-gin-frame/graceful"
)

var (
conf = pflag.StringP("config", "c", "", "config filepath")
)

func main() {
pflag.Parse()

// 初始化配置
if err := config.Run(*conf); err != nil {
panic(err)
}

// logger.Info("i'm log123-----Info")
// logger.Error("i'm log123-----Error")

// 連接mysql數(shù)據(jù)庫
DB := db.GetDB()
defer db.CloseDB(DB)

// redis
db.InitRedis()

gin.SetMode(viper.GetString("mode"))
g := gin.New()
g = router.Load(g)

// g.Run(viper.GetString("addr"))

go func() {
if err := pingServer(); err != nil {
fmt.Println("fail:健康檢測失敗", err)
}
fmt.Println("success:健康檢測成功")
}()

logger.Info("啟動(dòng)http服務(wù)端口%s\n", viper.GetString("addr"))

if err := graceful.ListenAndServe(viper.GetString("addr"), g); err != nil && err != http.ErrServerClosed {
logger.Error("fail:http服務(wù)啟動(dòng)失敗: %s\n", err)
}
}

// 健康檢查
func pingServer() error {
for i := 0; i < viper.GetInt("max_ping_count"); i++ {
url := fmt.Sprintf("%s%s%s", "http://127.0.0.1", viper.GetString("addr"), viper.GetString("healthCheck"))
fmt.Println(url)
resp, err := http.Get(url)
if err == nil && resp.StatusCode == 200 {
return nil
}
time.Sleep(time.Second)
}
return errors.New("健康檢測404")
}

這里就比較簡單,另外啟動(dòng)一個(gè)協(xié)程,去ping健康檢測的url即可。

打包腳本

shell

#!/bin/bash
SERVER="demo2-gin-frame"

function status()
{
if [ "pgrep $SERVER -u $UID" != "" ];then echo $SERVER is running else echo $SERVER is not running fi } function build() { echo "build..." CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./$SERVER main.go if [ $? -ne "0" ];then echo "built error!!!" return fi echo "built success!" } case "$1" in 'status') status ;; 'build') build ;; *) echo "unknown, please: $0 {status or build}" exit 1 ;; esac

bat

echo "build..."
SET CGO_ENABLED=0
SET GOOS=linux
go build -o demo2-gin-frame
echo commitid=%commitid%
if %errorlevel% == 0 (
echo "built successfully"
) else (
echo "built fail!!!"
)

對于程序的重啟和保活,建議配合supervisor使用。

好,到這里我們的round 2就結(jié)束了。下一輪我們來玩玩釘釘智能機(jī)器人。

直達(dá)完整代碼https://github.com/13sai/go-example/tree/main/demo2-gin-frame

參考:

本文章轉(zhuǎn)載微信公眾號(hào)@SaiWeng

上一篇:

掌握API建模:基本概念和實(shí)踐

下一篇:

.NET Core WebAPI 文件分片上傳與跨域請求處理
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場景實(shí)測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對比試用API 限時(shí)免費(fèi)