GoWeb开发常用组件的使用

zap日志库的使用

安装

go get -u go.uber.org/zap

配置Zap logger

zap提供了两种类型的日志记录器——Sugard Logger和Logger

Logger
var logger *zap.Logger

// 初始化
func InitLogger() {
   logger, _ = zap.NewProduction()
}

// 模拟请求发送
func simpleHttpGet(url string) {
   resp, err := http.Get(url)
   if err != nil {
      //日志级别
      logger.Error(
         "Error fetching url",
         zap.String("url", url),
         zap.Error(err))
   } else {
      logger.Info("Success",
         zap.String("statusCade", resp.Status),
         zap.String("url", url))
      resp.Body.Close()
   }
}
func main() {
   InitLogger()
   //在程序调用结束后,把日志记录到磁盘中
   defer logger.Sync()
   simpleHttpGet("www.bilibili.com")
   simpleHttpGet("https://www.bilibili.com")
}

定制Logger

将日志写入文件

使用zap.New()需要传入一个必须的配置

func New(core zapcore.Core, options ...Option) *Logger

zapcore.Core需要三个配置——Encoder,WriteSyncer,LogLevel。

Encoder:编码器(如何写入日志)。我们将使用开箱即用的New.JSONEncoder(),并使用预先设置的ProductionEncoderConfig()。

WriterSyncer:指定日志将写到哪里去。我们使用zapcore. AddSync()函数并且将打开的文件句柄传进去。

LogLevel:哪种级别的日志将被写入。
我们将修改上述部分中的Logger代码,并重写InitLogger() 方法。其余方法-main()/ SimpleHttpGet()保持不变。

package main

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"net/http"
	"os"
)

var logger1 *zap.Logger
var sugarLogger *zap.SugaredLogger

// 初始化
func InitLogger1() {
	//logger, _ = zap.NewProduction()
	writeSyncer := getLogWriter()
	encoder := getEncoder()
	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
	//zap.AddCaller()添加日志的调用信息,即哪个方法调用了日志
	logger1 = zap.New(core,zap.AddCaller())
	sugarLogger = logger1.Sugar()

}

func getEncoder() zapcore.Encoder {
	encoderConfig := zapcore.EncoderConfig{
		TimeKey:        "ts",
		LevelKey:       "level",
		NameKey:        "logger",
		CallerKey:      "caller",
		FunctionKey:    zapcore.OmitKey,
		MessageKey:     "msg",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.LowercaseLevelEncoder,
		EncodeTime:     zapcore.ISO8601TimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	}
	//将新生成的配置项传进去
	return zapcore.NewConsoleEncoder(encoderConfig)
	//return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

}

func getLogWriter() zapcore.WriteSyncer {
	//file, _ := os.Create("./test.log")每次执行都会新建文件
	//执行会往原有文件里面追加
	file, _ := os.OpenFile("./test.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
	return zapcore.AddSync(file)
}

// 模拟请求发送
func simpleHttpGet1(url string) {
	resp, err := http.Get(url)
	if err != nil {
		//日志级别
		sugarLogger.Error(
			"Error fetching url",
			zap.String("url", url),
			zap.Error(err))
	} else {
		sugarLogger.Info("Success",
			zap.String("statusCade", resp.Status),
			zap.String("url", url))
		resp.Body.Close()
	}
}
func main() {
	InitLogger1()
	//在程序调用结束后,把日志记录到磁盘中
	defer logger1.Sync()
	simpleHttpGet1("www.bilibili.com")
	simpleHttpGet1("https://www.bilibili.com")
}

使用Lumberjack进行日志切割归档

要使用第三方库Lumberjack实现

安装

go get gopkg.in/natefinch/lumberjack.v2
package main

import (
   "go.uber.org/zap"
   "go.uber.org/zap/zapcore"
   "gopkg.in/natefinch/lumberjack.v2"
   "net/http"
)

var logger1 *zap.Logger
var sugarLogger *zap.SugaredLogger

// 初始化
func InitLogger1() {
   //logger, _ = zap.NewProduction()
   writeSyncer := getLogWriter()
   encoder := getEncoder()
   core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
   //zap.AddCaller()添加日志的调用信息,即哪个方法调用了日志
   logger1 = zap.New(core, zap.AddCaller())
   sugarLogger = logger1.Sugar()

}

func getEncoder() zapcore.Encoder {
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:        "ts",
      LevelKey:       "level",
      NameKey:        "logger",
      CallerKey:      "caller",
      FunctionKey:    zapcore.OmitKey,
      MessageKey:     "msg",
      StacktraceKey:  "stacktrace",
      LineEnding:     zapcore.DefaultLineEnding,
      EncodeLevel:    zapcore.LowercaseLevelEncoder,
      EncodeTime:     zapcore.ISO8601TimeEncoder,
      EncodeDuration: zapcore.SecondsDurationEncoder,
      EncodeCaller:   zapcore.ShortCallerEncoder,
   }
   //将新生成的配置项传进去
   return zapcore.NewConsoleEncoder(encoderConfig)
   //return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

}

func getLogWriter() zapcore.WriteSyncer {
   //file, _ := os.Create("./test.log")每次执行都会新建文件
   //执行会往原有文件里面追加
   //file, _ := os.OpenFile("./test.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
   //return zapcore.AddSync(file)
   lumberJackLogger := &lumberjack.Logger{
      //Filename: 日志文件的位置
      //MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)
      //MaxBackups:保留旧文件的最大个数
      //MaxAges:保留旧文件的最大天数
      //Compress:是否压缩/归档旧文件
      Filename:   "./test.log",
      MaxSize:    10, //单位  MB
      MaxBackups: 5,
      MaxAge:     30,
      Compress:   false,
   }
   return zapcore.AddSync(lumberJackLogger)
}

// 模拟请求发送
func simpleHttpGet1(url string) {
   resp, err := http.Get(url)
   if err != nil {
      //日志级别
      sugarLogger.Error(
         "Error fetching url",
         zap.String("url", url),
         zap.Error(err))
   } else {
      sugarLogger.Info("Success",
         zap.String("statusCade", resp.Status),
         zap.String("url", url))
      resp.Body.Close()
   }
}
func main() {
   InitLogger1()
   //在程序调用结束后,把日志记录到磁盘中
   defer logger1.Sync()
   simpleHttpGet1("www.bilibili.com")
   simpleHttpGet1("https://www.bilibili.com")
}

gin框架配置zap记录日志

package main

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

   "github.com/gin-gonic/gin"
   "go.uber.org/zap"
   "go.uber.org/zap/zapcore"
   "gopkg.in/natefinch/lumberjack.v2"
)

var logger2 *zap.Logger
var sugarLogger2 *zap.SugaredLogger

func main() {
   InitLogger2()
   //使用zap日志库处理gin框架日志
   r := gin.New()
   r.Use(GinLogger(logger2), GinRecovery(logger2, true))

   r.GET("/hello", func(c *gin.Context) {
      c.String(http.StatusOK, "hello qimi")
   })
   r.Run()
}

// 初始化
func InitLogger2() {
   //logger, _ = zap.NewProduction()
   writeSyncer := getLogWriter2()
   encoder := getEncoder2()
   core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
   //zap.AddCaller()添加日志的调用信息,即哪个方法调用了日志
   logger2 = zap.New(core, zap.AddCaller())
   sugarLogger2 = logger2.Sugar()

}

func getEncoder2() zapcore.Encoder {
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:        "ts",
      LevelKey:       "level",
      NameKey:        "logger",
      CallerKey:      "caller",
      FunctionKey:    zapcore.OmitKey,
      MessageKey:     "msg",
      StacktraceKey:  "stacktrace",
      LineEnding:     zapcore.DefaultLineEnding,
      EncodeLevel:    zapcore.LowercaseLevelEncoder,
      EncodeTime:     zapcore.ISO8601TimeEncoder,
      EncodeDuration: zapcore.SecondsDurationEncoder,
      EncodeCaller:   zapcore.ShortCallerEncoder,
   }
   //将新生成的配置项传进去
   return zapcore.NewConsoleEncoder(encoderConfig)
   //return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

}

func getLogWriter2() zapcore.WriteSyncer {
   //file, _ := os.Create("./test.log")每次执行都会新建文件
   //执行会往原有文件里面追加
   //file, _ := os.OpenFile("./test.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
   //return zapcore.AddSync(file)
   lumberJackLogger := &lumberjack.Logger{
      //Filename: 日志文件的位置
      //MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)
      //MaxBackups:保留旧文件的最大个数
      //MaxAges:保留旧文件的最大天数
      //Compress:是否压缩/归档旧文件
      Filename:   "./test.log",
      MaxSize:    1, //单位  MB
      MaxBackups: 5,
      MaxAge:     30,
      Compress:   false,
   }
   return zapcore.AddSync(lumberJackLogger)
}

// 模拟请求发送
func simpleHttpGet2(url string) {
   resp, err := http.Get(url)
   if err != nil {
      //日志级别
      sugarLogger2.Error(
         "Error fetching url",
         zap.String("url", url),
         zap.Error(err))
   } else {
      sugarLogger2.Info("Success",
         zap.String("statusCade", resp.Status),
         zap.String("url", url))
      resp.Body.Close()
   }
}

//===========================================================================================

// GinLogger 接收gin框架默认的日志
func GinLogger(logger *zap.Logger) 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)
      logger.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
func GinRecovery(logger *zap.Logger, 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 {
               logger.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 {
               logger.Error("[Recovery from panic]",
                  zap.Any("error", err),
                  zap.String("request", string(httpRequest)),
                  zap.String("stack", string(debug.Stack())),
               )
            } else {
               logger.Error("[Recovery from panic]",
                  zap.Any("error", err),
                  zap.String("request", string(httpRequest)),
               )
            }
            c.AbortWithStatus(http.StatusInternalServerError)
         }
      }()
      c.Next()
   }
}

viper

Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。

安装

go get github.com/spf13/viper

viper的使用

package main

import (
   "fmt"
   "github.com/fsnotify/fsnotify"
   "github.com/spf13/viper"
)

func main() {
   //设置默认值
   viper.SetDefault("name", "./")
   //读取配置文件
   viper.SetConfigFile("./config.yaml")  // 指定配置文件路径
   viper.SetConfigName("config")         // 配置文件名称(无扩展名)
   viper.SetConfigType("yaml")           // 如果配置文件的名称中没有扩展名,则需要配置此项
   viper.AddConfigPath("/etc/appname/")  // 查找配置文件所在的路径
   viper.AddConfigPath("$HOME/.appname") // 多次调用以添加多个搜索路径
   viper.AddConfigPath(".")              // 还可以在工作目录中查找配置
   err := viper.ReadInConfig()           // 查找并读取配置文件
   if err != nil {                       // 处理读取配置文件的错误
      panic(fmt.Errorf("Fatal error config file: %s \n", err))
   }
   //在加载配置文件出错时,你可以像下面这样处理找不到配置文件的特定情况:
   if err := viper.ReadInConfig(); err != nil {
      if _, ok := err.(viper.ConfigFileNotFoundError); ok {
         // 配置文件未找到错误;如果需要可以忽略
      } else {
         // 配置文件被找到,但产生了另外的错误
      }
   }
   // 配置文件找到并成功解析
   //===================写入配置文件==================
   viper.WriteConfig() // 将当前配置写入“viper.AddConfigPath()”和“viper.SetConfigName”设置的预定义路径
   viper.SafeWriteConfig()
   viper.WriteConfigAs("/path/to/my/.config")
   viper.SafeWriteConfigAs("/path/to/my/.config") // 因为该配置文件写入过,所以会报错
   viper.SafeWriteConfigAs("/path/to/my/.other_config")
   //=====================监控并重新读取配置文件==========
   //实时监控配置文件的变化
   viper.WriteConfig()
   viper.OnConfigChange(func(e fsnotify.Event) {
      //配置文件发生变更之后会调用的回调函数
      fmt.Println("config file changed", e.Name)
   })

}

优雅的关机或重启

优雅关机

优雅关机就是服务端关机命令发出后不是立即关机,而是等待当前还在处理的请求全部处理完毕后再退出程序,是一种对客户端友好的关机方式。而执行Ctrl+C关闭服务端时,会强制结束进程导致正在访问的请求出现问题。

在Go1.8版本之后,http.Server内置的Shutdown()方法就支持优雅的关机

代码实现:

package main

import (
   "context"
   "github.com/fvbock/endless"
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
   "os"
   "os/signal"
   "syscall"
   "time"
)

func main() {
   //==========================关机======================
   r := gin.Default()
   r.GET("/", func(c *gin.Context) {
      time.Sleep(5 * time.Second)
      c.String(http.StatusOK, "Welcome Gin Server")
   })
   srv := &http.Server{
      Addr:    ":8080",
      Handler: r,
   }
   go func() {
      // 开启一个goroutine启动服务
      if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
         log.Fatalf("listen: %s\n", err)
      }
   }()
   // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
   quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
   // kill 默认会发送 syscall.SIGTERM 信号
   // kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
   // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
   // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
   signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
   <-quit                                               // 阻塞在此,当接收到上述两种信号时才会往下执行
   log.Println("Shutdown Server ...")
   // 创建一个5秒超时的context
   ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   defer cancel()
   // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
   if err := srv.Shutdown(ctx); err != nil {
      log.Fatal("Server Shutdown: ", err)
   }

   log.Println("Server exiting")
   //==========================重启===========================
   r.GET("/", func(c *gin.Context) {
      time.Sleep(5 * time.Second)
      c.String(http.StatusOK, "hello gin!")
   })
   // 默认endless服务器会监听下列信号:
   // syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP
   // 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号)
   // 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机
   // 接收到 SIGUSR2 信号将触发HammerTime
   // SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数
   if err := endless.ListenAndServe(":8080", r); err != nil {
      log.Fatalf("listen: %s\n", err)
   }

   log.Println("Server exiting")
}

当你的项目是使用类似supervisor的软件管理进程时就不适用这种方式了。

GoWeb项目分层理念(CLD)

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值