
掌握API建模:基本概念和實(shí)踐
在我們使用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包我們就選用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有以下說明:
我們直接使用:
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
簡單說明下處理步驟:
詳細(xì)分析可看底部參考-Golang服務(wù)器熱重啟、熱升級(jí)、熱更新
結(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í)踐
程序員常用的API接口管理工具有哪些?
簡化API縮寫:應(yīng)用程序編程接口終極指南
如何為你的項(xiàng)目挑選最佳API?完整選擇流程解讀
應(yīng)用程序開發(fā)蓬勃發(fā)展的必備開放API
.NET Core Web APi類庫如何內(nèi)嵌運(yùn)行和.NET Core Web API 中的異常處理
.NET Core Web API + Vue By Linux and Windows 部署方案知識(shí)點(diǎn)總結(jié)
優(yōu)化利潤:計(jì)算并報(bào)告OpenAI支持的API的COGS
用于集成大型語言模型的LLM API