Go Web 开发常用组件

Zap 日志库

常见的日志级别

  • panic:(error)错误已经发生,会导致系统退出
  • error:错误虽然发生,但是不影响系统的继续进行
  • warn:表明会出现潜在的错误
  • info:通知你一声
  • debug:调试输出,对于调试应用程序非常用帮助

基本配置

登录测试

package main

import (
	"net/http"

	"go.uber.org/zap"
)

func InitLogger() {
	logger, _ = zap.NewProduction() // 生产级别的库
}

func simpleHttpGet(url string) {
	resp, err := http.Get(url)
	if err != nil {
		//zap.String是个key-value的键值对
		logger.Error("打开url失败", zap.String("url", url), zap.Error(err)) 
	} else {
		logger.Info("Success...", zap.String("访问成功", resp.Status), zap.String("url", url))
		resp.Body.Close() //关闭访问
	}
}

var logger *zap.Logger

func main() {
	InitLogger()
	defer logger.Sync() // 把日志记录到磁盘上面
	simpleHttpGet("www.baidu.com")
	simpleHttpGet("http://www.baidu.com")
}

运行出来的结果

输出结果为json格式

[
    {
        "level": "error",
        "ts": 1625745481.5386593,
        "caller": "Log/main.go:16",
        "msg": "打开url失败",
        "url": "www.baidu.com",
        "error": "Get \"www.baidu.com\": unsupported protocol scheme \"\"",
        "stacktrace": "main.simpleHttpGet\n\tE:/DesKtop/go/Module/Log/main.go:16\nmain.main\n\tE:/DesKtop/go/Module/Log/main.go:28\nruntime.main\n\tD:/Go/src/runtime/proc.go:225"
    },
    {
        "level": "info",
        "ts": 1625745481.5898762,
        "caller": "Log/main.go:18",
        "msg": "Success...",
        "访问成功": "200 OK",
        "url": "http://www.baidu.com"
    }
]

配置自定义的日志文件

包括写到文件中,定义写入格式,定义备份,还有日志分割等功能。

package main

import (
	"net/http"

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

func InitLogger() {
	//logger, _ = zap.NewProduction() // 生产级别的库

	codeconfig := zapCoreConfig()                 //调用自定义的记录格式
	encoder := zapcore.NewJSONEncoder(codeconfig) //使用json的方式编码

	//日志写到哪里,这种是原始的
	//file, _ := os.OpenFile("./logtest.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0744)

	//带有日志分割功能的记录
	file := &lumberjack.Logger{
		Filename:   "./testzap.log",
		MaxSize:    1,     //最大多大
		MaxBackups: 3,     //最大备份数量
		MaxAge:     30,    //3天
		Compress:   false, //不压缩
	}
	writeSyncer := zapcore.AddSync(file) //写入到哪个文件

	//传入配置,并声明是debug文件
	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
	logger = zap.New(core, zap.AddCaller()) //配置完成,第二个参数表示,要添加调用者的信息
}

func zapCoreConfig() zapcore.EncoderConfig { //自定义的格式书写类型
	return 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, //更改成is08601格式,而不是时间戳
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	}
}

func simpleHttpGet(url string) {
	resp, err := http.Get(url)
	for i := 0; i < 5000; i++ {
		if err != nil {
			//zap.string是个 json 的键值对
			logger.Error("打开url失败", zap.String("url", url), zap.Error(err))
		} else {
			logger.Info("Success...", zap.String("访问成功", resp.Status), zap.String("url", url))
			resp.Body.Close() //关闭访问
		}
	}

}

var logger *zap.Logger

func main() {
	InitLogger()
	defer logger.Sync() // 把日志记录到磁盘上面
	simpleHttpGet("www.baidu.com")
	simpleHttpGet("http://www.baidu.com")
}

在gin框架中集成loger组件

其实就是相当于写了两个中间件,执行不同的操作

var logger *zap.Logger

func main() {
	r := gin.New()
	//启动了两个日志库
	r.Use(GinLogger(logger), GinRecovery(logger, true))
	r.GET("/hello", func(c *gin.Context) {
		c.String(http.StatusOK, "哈哈,你好")
	})
	r.Run(":5002")
}

// 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 的几个特性

  • 设置默认值
  • 从 JSON、TOML、YAML、格式的配置文件中读取配置信息
  • 实时监控和重读配置文件
  • 从远程系统中,读取并监控配置变化
  • 从命令行参数读取配置
  • 从buffer中读取配置信息
  • 显示配置值

Viper的先后顺序

  • 显示配置 set 设置值
  • 命令行参数
  • 环境变量
  • 配置文件
  • key/value存储
  • 默认值
    重要: 目前的 Viper的配置 key 大小写是不敏感的。
package main

import (
	"fmt"
	"net/http"

	"github.com/fsnotify/fsnotify"
	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
)

func ReadConfig() {
	viper.SetDefault("filedir", "./") //设置默认值,优先级别最低的
	// viper.SetConfigName("config")     //配置文件名称,无扩展名
	// viper.SetConfigType("yaml")       //如果配置文件中没有扩展名,就默认使用这一个
	viper.SetConfigFile("./config.json")   //直接指定要读取的配置w文件
	viper.AddConfigPath("./etc/appname/")  //配置文件所在的路径
	viper.AddConfigPath("./etc/appname2/") //多次调用,以添加多个搜索路径
	viper.AddConfigPath(".")               //还可以在当前多个目录查找
	err := viper.ReadInConfig()            //查找并读取配置文件
	if err != nil {
		panic(fmt.Errorf("配置文件错误: %s", err)) //输出错误信息,通常用这种方式来自定义errorf信息
	}
}

func WriteConfig() {
	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")
}

func main() {
	ReadConfig()
	viper.WatchConfig() //实时加载配置文件
	viper.OnConfigChange(func(e fsnotify.Event) {
		fmt.Println("配置文件变化后的回调函数", e.Name, e.Op) //可以动态的更改文件
	})
	r := gin.Default()
	r.GET("/version", func(c *gin.Context) {
		c.String(http.StatusOK, viper.GetString("student.student1"))
	})
	r.Run(":5002")
}

优雅关机和重启

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

Go Web 开发常用组件

上一篇:CSS基础操作(五)-盒子模型(设置元素宽高、内边距、边框、外边距)


下一篇:MyBatisPlusGenerator使用