Go语言 基于gin定义一个简单的web server 开发框架(一)

1,377 阅读3分钟

基本流程

一个简单的web server开发框架,不管是哪种语言 其实都是遵循着下面的思路:

  1. 初始化配置文件
  2. 初始化日志库
  3. 连接mysql数据库
  4. 连接redis数据库
  5. 注册路由
  6. 启动server

我们也就遵循这个思路 来定义一个简单的 web server 开发框架

初始化配置文件:

我们可以用这个yaml 格式的文件

app:
  name: "web_app"
  mode: "dev"
  port: 8071
log:
  level: "debug"
  filename: "web_app.log"
  max_size: 200
  max_age: 30
  max_backups: 7
mysql:
  host: "127.0.0.1"
  port: 3306
  user: "root"
  password: "1234qwer"
  dbname: "go_test"
  max_open_connection: 200
  max_idle_connection: 10
redis:
  host: "127.0.0.1"
  passowrd: ""
  port: 6379
  db: 0
  pool_size: 0

然后开始读取这个配置文件 这里用到viper库

package setting

import (
   "fmt"

   "github.com/fsnotify/fsnotify"

   "github.com/spf13/viper"
)

// Init 加载配置文件
func Init() error {
   viper.SetConfigName("config") // 配置文件的名称
   viper.SetConfigType("yaml")   // 配置文件的扩展名,这里除了json还可以有yaml等格式
   // 这个配置可以有多个,主要是告诉viper 去哪个地方找配置文件
   // 我们这里就是简单配置下 在当前工作目录下 找配置即可
   viper.AddConfigPath(".")
   err := viper.ReadInConfig()
   if err != nil {
      fmt.Println("viper init failed:", err)
      return err
   }
   viper.WatchConfig()
   viper.OnConfigChange(func(in fsnotify.Event) {
      fmt.Println("配置文件已修改")
   })
   return err
}

利用zap来接管gin 的日志系统

这里webserver 我们就选用gin了,然后就是要将我们gin里面的日志 统统让zap 这个库来接管,并且 可以用zap来正常输出我们的日志 到对应的日志文件中

package logger

import (
   "net"
   "net/http"
   "net/http/httputil"
   "os"
   "runtime/debug"
   "strings"
   "time"

   "github.com/spf13/viper"

   "github.com/gin-gonic/gin"

   "github.com/natefinch/lumberjack"
   "go.uber.org/zap"
   "go.uber.org/zap/zapcore"
)

var lg *zap.Logger

// Init InitLogger 初始化Logger
func Init() (err error) {
   // 这里直接用viper来读取对应配置文件的参数 即可
   writeSyncer := getLogWriter(viper.GetString("log.filename"),
      viper.GetInt("log.max_size"), viper.GetInt("log.max_backups"),
      viper.GetInt("log.max_age"))
   encoder := getEncoder()
   var l = new(zapcore.Level)
   err = l.UnmarshalText([]byte(viper.GetString("log.level")))
   if err != nil {
      return
   }
   core := zapcore.NewCore(encoder, writeSyncer, l)

   lg = zap.New(core, zap.AddCaller())
   zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
   return
}

func getEncoder() zapcore.Encoder {
   encoderConfig := zap.NewProductionEncoderConfig()
   encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
   encoderConfig.TimeKey = "time"
   encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   return zapcore.NewJSONEncoder(encoderConfig)
}

func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
   lumberJackLogger := &lumberjack.Logger{
      Filename:   filename,
      MaxSize:    maxSize,
      MaxBackups: maxBackup,
      MaxAge:     maxAge,
   }
   return zapcore.AddSync(lumberJackLogger)
}

// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
   return func(c *gin.Context) {
      start := time.Now()
      path := c.Request.URL.Path
      query := c.Request.URL.RawQuery
      c.Next()

      cost := time.Since(start)
      lg.Info(path,
         zap.Int("status", c.Writer.Status()),
         zap.String("method", c.Request.Method),
         zap.String("path", path),
         zap.String("query", query),
         zap.String("ip", c.ClientIP()),
         zap.String("user-agent", c.Request.UserAgent()),
         zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
         zap.Duration("cost", cost),
      )
   }
}

// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
   return func(c *gin.Context) {
      defer func() {
         if err := recover(); err != nil {
            // Check for a broken connection, as it is not really a
            // condition that warrants a panic stack trace.
            var brokenPipe bool
            if ne, ok := err.(*net.OpError); ok {
               if se, ok := ne.Err.(*os.SyscallError); ok {
                  if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
                     brokenPipe = true
                  }
               }
            }

            httpRequest, _ := httputil.DumpRequest(c.Request, false)
            if brokenPipe {
               lg.Error(c.Request.URL.Path,
                  zap.Any("error", err),
                  zap.String("request", string(httpRequest)),
               )
               // If the connection is dead, we can't write a status to it.
               c.Error(err.(error)) // nolint: errcheck
               c.Abort()
               return
            }

            if stack {
               lg.Error("[Recovery from panic]",
                  zap.Any("error", err),
                  zap.String("request", string(httpRequest)),
                  zap.String("stack", string(debug.Stack())),
               )
            } else {
               lg.Error("[Recovery from panic]",
                  zap.Any("error", err),
                  zap.String("request", string(httpRequest)),
               )
            }
            c.AbortWithStatus(http.StatusInternalServerError)
         }
      }()
      c.Next()
   }
}

初始化 mysql与redis

package mysql

import (
   "fmt"

   "go.uber.org/zap"

   "github.com/spf13/viper"

   _ "github.com/go-sql-driver/mysql"
   "github.com/jmoiron/sqlx"
)

var db *sqlx.DB

func Init() (err error) {
   dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
      viper.GetString("mysql.user"), viper.GetString("mysql.password"),
      viper.GetString("mysql.host"), viper.GetInt("mysql.port"),
      viper.GetString("mysql.dbname"),
   )
   // 也可以使用MustConnect连接不成功就panic
   db, err = sqlx.Connect("mysql", dsn)
   if err != nil {
      zap.L().Error("connect DB failed, err:%v\n", zap.Error(err))
      return
   }
   db.SetMaxOpenConns(viper.GetInt("mysql.max_open_connection"))
   db.SetMaxIdleConns(viper.GetInt("mysql.max_idle_connection"))
   return
}
package redis

import (
   "fmt"

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

// 声明一个全局的rdb变量
var rdb *redis.Client

// 初始化连接
func Init() error {
   rdb = redis.NewClient(&redis.Options{
      Addr:     fmt.Sprintf("%s:%d", viper.GetString("redis.host"), viper.GetInt("redis.port")),
      Password: viper.GetString("redis.password"), // no password set
      DB:       viper.GetInt("redis.db"),
      PoolSize: viper.GetInt("redis.pool_size"), // use default DB

   })

   _, err := rdb.Ping().Result()
   return err
}

整体项目结构:

image.png

程序入口处:

package main

import (
   "fmt"
   "go_web_app/dao/mysql"
   "go_web_app/dao/redis"
   "go_web_app/logger"
   "go_web_app/route"
   "go_web_app/setting"

   "go.uber.org/zap"
)

func main() {
   // 加载配置文件
   if err := setting.Init(); err != nil {
      fmt.Printf("init settings failed:%s \n", err)
      return
   }
   // 初始化日志
   if err := logger.Init(); err != nil {
      fmt.Printf("init settings failed:%s \n", err)
      return
   }
   zap.L().Debug("logger init success")
   defer zap.L().Sync()
   // 不要遗漏2个 db的close
   defer mysql.Db.Close()
   defer redis.Rdb.Close()
   // 初始化mysql
   if err := mysql.Init(); err != nil {
      fmt.Printf("init mysql failed:%s \n", err)
      return
   }
   zap.L().Debug("mysql init success")
   // 初始化redis
   if err := redis.Init(); err != nil {
      fmt.Printf("init redis failed:%s \n", err)
      return
   }
   zap.L().Debug("redis init success")
   // 注册路由
   route.Setup()
   // 启动服务 (优雅关机)

}

所有代码均已上传: github链接

未完待续