(一)介绍

  • zap 是go 中比较火的一个日志库,提供不同级别的日志,并且速度快

  • 官方文档: https://pkg.go.dev/go.uber.org/zap#section-readme, 也可以github 直接搜索 zap。 文档上面有全面的介绍。鼓励大家观看文档, 可以有视频资料做相关引导,但学东西必须要看到官方文档。尤其是文档也已经适合入门了,先看Quick Start 部分,一般都有入门例子及整体框架介绍。

  • 官方文档介绍,非常清晰,还有相关数据对比,并且一般都会具有 example文件夹提供相关的编程实例。

  • Quick Start(快速开始): zap提供了两种 日志记录器:

    • (1)SugaredLogger:(加了糖的 Logger)

      • 官方文档介绍: 在性能很好但不是很关键的环境中,使用SugaredLogger。它比其他结构化日志包快4-10倍,并且包含结构化和printf风格的api

        // 创建一个 logger
        logger, _ := zap.NewProduction()
        defer logger.Sync() // flushes buffer, if any, 刷新缓冲区,存盘
        sugar := logger.Sugar()
        sugar.Infow("failed to fetch URL",// 结构化上下文为松散类型的键值对。"url", url,"attempt", 3,"backoff", time.Second,
        )
        sugar.Infof("Failed to fetch URL: %s", url)
        
    • (2)Logger
      • 当性能和类型安全至关重要时,使用Logger。它甚至比SugaredLogger还要快,并且分配的数量要少得多,但是它只支持结构化日志

        logger, _ := zap.NewProduction()
        defer logger.Sync()
        logger.Info("failed to fetch URL",// 作为强类型字段值的结构化上下文.zap.String("url", url),zap.Int("attempt", 3),zap.Duration("backoff", time.Second),
        )
        
    • 更多介绍可以看文档 choose a logger
      • 在Logger和SugaredLogger之间进行选择不需要在应用程序范围内进行决定:在两者之间进行转换既简单又便宜。从上面就可以看出来,二者创建使用区别很小。

        logger := zap.NewExample()
        defer logger.Sync()
        sugar := logger.Sugar()
        plain := sugar.Desugar()
        

(二)讲解 zap

1. 日志级别

  • const 文档下面,有介绍日志级别的定义

    const (// DebugLevel logs are typically voluminous, and are usually disabled in// production.DebugLevel = zapcore.DebugLevel// InfoLevel is the default logging priority.InfoLevel = zapcore.InfoLevel// WarnLevel logs are more important than Info, but don't need individual// human review.WarnLevel = zapcore.WarnLevel// ErrorLevel logs are high-priority. If an application is running smoothly,// it shouldn't generate any error-level logs.ErrorLevel = zapcore.ErrorLevel// DPanicLevel logs are particularly important errors. In development the// logger panics after writing the message.DPanicLevel = zapcore.DPanicLevel// PanicLevel logs a message, then panics.PanicLevel = zapcore.PanicLevel// FatalLevel logs a message, then calls os.Exit(1).FatalLevel = zapcore.FatalLevel
    )
    

2. 构建 looger

  • 在文档的 Configuring Zap中:
    构建Logger最简单的方法是使用zap固有的预设:NewExampleNewProductionNewDevelopment。这些预置用一个函数调用构建一个日志记录器。
  • 三者创建的 logger 是有区别的, 我们可以在官方文档的 type logger下面找到三个函数的介绍, 对应不同的场景。
    • func NewExample(options ...Option) *Logger

      • NewExample构建了一个专门为zap的可测试示例设计的Logger。它将DebugLevel及以上的日志作为JSON写入标准输出,但省略了时间戳和调用函数,以保持示例输出的简短和确定性
    • func NewProduction(options ...Option) (*Logger, error)
      • NewProduction构建了一个合理的生产日志记录器,它将infollevel及以上的日志以JSON的形式写入标准错误。
      • 它是NewProductionConfig(). build(…Option)的快捷方式。
    • func NewDevelopment(options ...Option) (*Logger, error)
      • NewDevelopment构建一个开发日志记录器,它以人类友好的格式将DebugLevel及以上级别的日志写入标准错误。
      • 这是NewDevelopmentConfig().Build(…选项)的快捷方式
      • 就好比去商店卖商品,初上自带了几个配置好的模式。 通过配置生成对应的 logger。 我们也可以自定义 配置,生成自己自定义的 logger。

3. 方法使用

  • 在文档的 types/loggertypes/SaguredLogger 里面记录了相关的looger记录消息的使用方法。

    • 以logger为例子:

    • 都接受一个 msg String, 后面是可选的一些字段。Field 类型,可以查看文档有很多的类型。

    • 写一个Get请求访问相应的网址,记录日志信息

      package mainimport ("net/http""time""go.uber.org/zap"
      )func main() {// 创建一个 logger, 可以选择其他预置创建,会有不同的输出效果logger, _ := zap.NewProduction()defer logger.Sync() // flushes buffer, if any, 刷新缓冲区,存盘// 定义urlurl := "http://www.baidu.com"resp, err := http.Get(url)// 直接出错日志if err != nil {logger.Error("访问失败了",zap.String("url", url),// 跟上错误信息zap.Error(err),)}else {// info 级别的日志信息打印logger.Info("成功访问!",// 作为强类型字段值的结构化上下文.zap.String("url", url),zap.String("status", resp.Status),zap.Duration("backoff", time.Second),)resp.Body.Close()}}
      
      • 结果分别为:
      {"level":"info","ts":1637632861.812557,
      "caller":"zap日志库学习/main.go:25",
      "msg":"成功访问!",
      "url":"http://www.baidu.com",
      "status":"200 OK","backoff":1}{"level":"error",
      "ts":1637633063.4520404,
      "caller":"zap日志库学习/main.go:18",
      "msg":"访问失败了",
      "url":"http://www.xxx.com",
      "error":"Get \"http://www.xxx.com\": dial tcp 69.171.228.20:80: connectex: A connection attempt failed because the connected party did not properly respond after a p
      eriod of time, or established connection failed because connected host has failed to respond.","stacktrace":"main.main\n\tD:/gofiles/go-learning
      -notes/go-learning/zap日志库学习/main.go:18\nruntime.main\n\tD:/software/go/go1.15/src/runtime/proc.go:204"}
      
      • 可以看到执行程序后终端提示相关的信心。 msg 就是自己设置的,时间, url等。 还有一个 caller 调用者信息,指明问题出现的行数
    • NewDevelopment() 创建的生成日志是这样的: 空格隔开。 缺少调用者

      2021-11-23T10:08:26.171+0800    INFO    zap日志库学习/main.go:25        成功访问!     {"url": "http://www.baidu.com", "status": "200 OK", "backoff": "1s"}
      
    • NewExample()

      {"level":"info","msg":"成功访问!","url":"http://www.baidu.com","status":"200 OK","backoff":"1s"}
      

4. 定制化 logger

  • 查看NewProduction 的源码就能知道,实际底层就是: NewProductionConfig().Build(options...)

    • 调用了 NewProductionConfig()方法,内部初始化创建,返回了一个 Config 对象。

    • Build, 内部通过 Config对象的配置, 利用New方法生成相应的 logger对象,并返回。

    • 也就是说,这是 zap库给我们预置的 NewProduction()等方法,内部是按照指定的配置,生成相应的 logger 日志对象。 我们也可以自己调用内部的相关方法, 模仿 NewProductionConfig().Build(options…) 相关过程,自己创建,定制化 logger对象

    • 观察New方法 生成logger 所需要的东西。在Build 函数中:

      // 返回一个 Core对象, 需要的是 三个参数
      func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core
      func New(core zapcore.Core, options ...Option) *Loggerlog := New(zapcore.NewCore(enc, sink, cfg.Level),cfg.buildOptions(errSink)...,)
      
    • 官方对 Core的介绍 :需要打开对应的包,查看文档
      Core是一个最小的、快速的记录器接口。它是为库作者设计的,用来封装更友好的API。

  • 关于 NewProductionConfig()函数, 返回对应的Config 对象,Build 函数根据这个配置,进行生成 logger对象。

  • 我们当然可以自定义这个, 来实现生成自己的logger. 下面看下它的源码

    // NewProductionConfig是一个合理的生产日志配置。
    //在infollevel及以上级别启用日志记录。
    //它使用JSON编码器,写入标准错误,并启用采样。
    // stacktrace会自动包含在ErrorLevel及以上的日志中。
    func NewProductionConfig() Config {return Config{// 日志级别Level:       NewAtomicLevelAt(InfoLevel),Development: false,Sampling: &SamplingConfig{Initial:    100,Thereafter: 100,},// 编码方式Encoding:         "json",// EncoderCofig, 配置 encoder 编辑器的默认配置。EncoderConfig:    NewProductionEncoderConfig(),// 打开的文件, 写入日志信息到这里。OutputPaths:      []string{"stderr"},ErrorOutputPaths: []string{"stderr"},}
    }
    

(1)如何写入日志文件

  • 按照上面的自定义 logger, 创建核心Core需要三个参数,其中就有控制 写入文件的。

  • Encoder编辑器。提供了两种信息的编辑方式

    • 需要传递的参数,可以使用 默认的EncoderConfig: NewProductionEncoderConfig() 传递进去。
    • 一种 文本样式的, 一种 json 样式的信息输入。
  • WriteSyncer指定日志写到哪里。可以定义自己指定的文件路径

    • 通过func AddSync(w io.Writer) WriteSyncer 方法,返回一个。
      AddSync用于转换io。Writer到WriteSyncer。它试图是智能的:如果具体类型的io。Writer实现了WriteSyncer,我们将使用现有的Sync方法。如果没有,我们将添加一个无操作同步。

         // 创建文件对象file, _ := os.Create("./getLog.log")  // 或者是用 OpenFile函数,在原来基础上追加。// file, _ := os.OpenFile("./getLog.log", os.O_APPEND | os.O_RDWR, 0744)// 生成 WriteSyncerwSy := zapcore.AddSync(file)
      
  • LevelEnabler设置哪种级别的日志将被写入

    • 对应的就是前面介绍的日志级别;如:
      zapcore.DebugLevel
  • 创建自定义logger:

    • 根据上面三点参数的理解,就可以指定文件建立了

      // 还剩一个后面的配置信息没有传入,但是已经可以了
      // 默认我们调用 NewProduction()方法也是没有传递啥配置进去的。
      log := New(zapcore.NewCore(传递编辑器(两种), 自定义文件输出, cfg.Level(级别)),)
      
      • 按照这样的建立完成之后就可以使用了,往指定文件里打印日志。
    • 还有 zap 预置的生成 logger的方式,都是通过 NewProductionConfig() 来生成相关配置的, 也可以不用这么麻烦,我觉得直接, 自定义一个 NewProductionConfig() 然后,按着相应的步骤就可以了。 Build方法 就是通过 配置的 Config 对象,来生成的 logger。

    • 这里有个系列教程文章:https://blog.csdn.net/weixin_39620252/article/details/111136566

    • 像我这样:重写方法,只需要加个文件名就可以了

      func myNewProduction(options ...zap.Option) (*zap.Logger, error) {return myNewProductionConfig().Build(options...)
      }func myNewProductionConfig() zap.Config {config := zap.NewProductionConfig()// 配置输出路径为 ./test.log   其他都不变, 内部默认是 json 编码。 只要 json 和 console两种。可以看 Encoding 字段的介绍。config.OutputPaths = []string{"./test.log"}return config
      }// 创建logger 对象
      logger, _ := myNewProduction()
      

(2)更改时间编码 及 添加调用者的详细信息

时间格式(或者说是编码)更改

  • 时间显示这一块,我们可以看到,默认的 NewProductionConfig()函数里创建的字段 中的 对 Encoder 消息编辑器对象的配置 EncoderConfig
    在文档中也能找到它的描述: https://pkg.go.dev/go.uber.org/zap@v1.19.1/zapcore#EncoderConfig

    func NewProductionEncoderConfig() zapcore.EncoderConfig {return zapcore.EncoderConfig{// 包含key的全部都是定义的输出字段名字。TimeKey:        "ts",LevelKey:       "level",NameKey:        "logger",CallerKey:      "caller",FunctionKey:    zapcore.OmitKey,MessageKey:     "msg",StacktraceKey:  "stacktrace",LineEnding:     zapcore.DefaultLineEnding,EncodeLevel:    zapcore.LowercaseLevelEncoder,EncodeTime:     zapcore.EpochTimeEncoder,EncodeDuration: zapcore.SecondsDurationEncoder,EncodeCaller:   zapcore.ShortCallerEncoder,}
    
  • 我们顺着文档去找或者是 直接点进去 EncodeTime: zapcore.EpochTimeEncoder。 EncodeTime 顾名思义: 时间编码。就是这里控制的,点进去后,该函数下方还有好几个设置时间编码的函数, 文档中也能找到。

  • 那么我们可以直接修改 对应的配置, 满足我们的自定义要求。

    • 选择 ISO8601TimeEncoder: “2006-01-02T15:04:05.000Z0700” 比较容易看懂的格式。

    • 按照上面修改导入文件的方法:

      func myNewProductionConfig() zap.Config {config := zap.NewProductionConfig()// 修改导入文件路径config.OutputPaths = []string{"./test.log"}// 修改时间格式:config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoderreturn config
      }
      
      • 再次运行,时间编码就会改变
      {"level":"info","ts":"2021-11-23T17:13:39.430+0800","caller":"zap日志库学习/main.go:43","msg":"成功访问!","url":"http://www.baidu.com","status":"200 OK","backoff":1}
      
    • 我们这里的写法,还是不断的 修改 NewProductionConfig() 为我们预置的配置。 如果想大改,创建自己的话, 就一步步创建 Core 核心。 然后 重新定义自己的 NewProductionEncoderConfig()。

显示详细的 添加调用者的详细信息。

  • 根据时间的理解, 这里很容易想到: 配置项当中的 EncodeCaller, 也可以指定相关的函数,用来打印 调用者的信息。

  • 还有一种方式是: 我们创建Core, zap.New()创建 logger, 分析第二个参数的 Options 类型, 文档中可以找到相关的方法,就有添加调试 显示调用人信息的方法。

    func New(core zapcore.Core, options ...Option) *Logger
    log := zap.New(zapcore.NewCore(enc, sink, cfg.Level),zap.AddCaller(),)
    

5. Lumberjack 进行日志文件的切割归档

  • 这部分就比较难了,没有实际操作过,就不懂。 参考文章或者相关视频,二者结合使用。

  • 日志存的多的话, 日志文件越来越大, 几个G的话,打开和操作就太麻烦了! 那么就需要对日志文件进行分割,归档。

  • 需要安装第三方库 :Lumberjack: go get -u github.com/natefinch/lumberjack

  • 使用时仍然是打开文件, 配合 zap使用,需要创建新的 zapcore.WriteSyncer

  • lumberjack.Logger 实现了 io.writer 接口,可以作为参数。

    func getLogWriter() zapcore.WriteSyncer {lumberJackLogger := &lumberjack.Logger{Filename: "./test.log",  // 导入文件名MaxSize: 10, // 大小M兆MaxBackups: 5,  // 最大备份数量MaxAge: 30,       // 最大备份天数Compress: false,  // 是否压缩}return zapcore.AddSync(lumberJackLogger)
    }
    
  • 进行测试:

    // 创建新的logger对象
    encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
    writeSyncer := getLogWriter()
    // 创建核心
    newCore := zapcore.NewCore(encoder, writeSyncer, zapcore.InfoLevel)
    // 创建logger
    logger := zap.New(newCore)
    // 插入日志
    for i := 0; i < 10000; i++ {logger.Info("成功访问!",// 作为强类型字段值的结构化上下文.zap.String("url", "测试归档"),zap.String("status", "添加数据"),zap.Duration("backoff", time.Second),)
    }
    defer logger.Sync() // flushes buffer, if any, 刷新缓冲区,存盘
    
    • 效果展示:以当前时间戳建立新文件
    • 归档文件中有八万多数据
    • test.log 中只有: 3000多条

6. gin框架中配置zap记录日志

  • 在上一篇文章中: 《详细讲解go web框架之gin框架源码解析记录及思路流程和理解》

    • 讲解了 gin.Default 创建引擎, 默认的添加了两个中间件。 一个是 logger 日志,一个是 recover 恢复。 gin 自带的 logger 就是在这里实现起作用的。

      • 那么我们也需要将 zap封装为 logger 中间件(HandlerFunc)。
      • 具体实现可以参考 给出的两个 logger recover 中间件的实现,加以修改。
  • 下面进行实际讲解, logger,recover 可以自己实现,也有现成的:https://github.com/gin-contrib/zap/blob/master/zap.go#L3

  • yaml格式的配置文件:

    app:name: "web_app"# 开发模式mode: "dev"port: 8080log:level: "debug"filename: "web_app.log"# 最大存储大小max_size: 200# 最大存储时间max_age: 30# 备份数量max_backups: 7
    
  • 实现logger:

    • 1.自定义logger对象
    • 2.自定义(模仿原来的logger,recover)实现这俩中间件
    • 其中要注意的是viper 读取配置文件,这是在下一节讲的:https://blog.csdn.net/pythonstrat/article/details/121513996
    • 其中:UnmarshalText函数,读取 配置中的文本信息,转换为 日志级别类型。
      • 需要首先定义一个 日志级别类型的指针,后UnmarshalText函数将赋值进去。
    package loggerimport ("github.com/gin-gonic/gin""github.com/natefinch/lumberjack""github.com/spf13/viper""go.uber.org/zap""go.uber.org/zap/zapcore""net""net/http""net/http/httputil""os""runtime/debug""strings""time"
    )// 初始化Logger
    func Init() (err error) {// 创建Core三大件,进行初始化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)// 将 yaml 配置文件中的 表示级别的文本,转换为 相应的 级别类型,赋值给 创建的指针err = l.UnmarshalText([]byte(viper.GetString("log.level")))if err != nil {return}// 创建核心core := zapcore.NewCore(encoder, writeSyncer, l)// 创建 logger 对象log := zap.New(core, zap.AddCaller())// 替换全局的 logger, 后续在其他包中只需使用zap.L()调用即可zap.ReplaceGlobals(log)return
    }func getEncoder() zapcore.Encoder {// 使用zap提供的 NewProductionEncoderConfigencoderConfig := zap.NewProductionEncoderConfig()// 设置时间格式encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder// 时间的keyencoderConfig.TimeKey = "time"// 级别encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder// 显示调用者信息encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder// 返回json 格式的 日志编辑器return zapcore.NewJSONEncoder(encoderConfig)
    }func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {// 使用 lumberjack 归档切片日志lumberJackLogger := &lumberjack.Logger{Filename:   filename,MaxSize:    maxSize,MaxBackups: maxBackup,MaxAge:     maxAge,}return zapcore.AddSync(lumberJackLogger)
    }// GinLogger 接收gin框架默认的日志
    func GinLogger() gin.HandlerFunc {logger := zap.L()return func(c *gin.Context) {start := time.Now()path := c.Request.URL.Pathquery := c.Request.URL.RawQueryc.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
    func GinRecovery(stack bool) gin.HandlerFunc {logger := zap.L()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 boolif 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: errcheckc.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()}
    }
    

7. gin中使用日志的补充

  • 我们上面代码生成的 核心,默认的级别以及是向文件中进行输出的。 那么我们在生产环境中的时候,就会很不方便查看日志的信息。 解决办法,可以定义一个 mode string 类型的参数。根据需要去创建 core满足,在控制台输出的需求。

  • 更改上述代码中创爱核心的操:

    // 新增: 根据 mode 参数创建var core zapcore.Coreif mode == "dev" {// 开发模式,日志输出到终端consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())// NewTee创建一个核心,将日志条目复制到两个或多个底层核心中。core = zapcore.NewTee(zapcore.NewCore(encoder, writeSyncer, l),zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel),)} else {// 创建核心core = zapcore.NewCore(encoder, writeSyncer, l)}
    
    • 这里面 我们上面讲过json 形式的日志, 还有就是 zapcore.NewConsoleEncoder想控制台打印信息。使用的也是默认自带的配置。
    • 其中zap的 zapcore.NewTee方法,可以支持多个核心。像上面这样 日志既会正常的在文件中写入,还会在终端中打印。
  • 另外对于 gin 日志的相关,gin文档中也有相关的描述:

    • 写日志文件: https://www.kancloud.cn/shuangdeyu/gin_book/949424

      func main() {// 禁用控制台颜色gin.DisableConsoleColor()// 创建记录日志的文件f, _ := os.Create("gin.log")gin.DefaultWriter = io.MultiWriter(f)// 如果需要将日志同时写入文件和控制台,请使用以下代码// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)router := gin.Default()router.GET("/ping", func(c *gin.Context) {c.String(200, "pong")})router.Run(":8080")
      }
      
      • 写日志文件里有对上面定义的: os.Stdout的描述,但是zap里不能直接使用, 还要通过 Lock() 函数进行类型转换下。
    • 自定义日志格式:https://www.kancloud.cn/shuangdeyu/gin_book/949425

  • 另外 gin 默认的是debug 模式,会输出一些信息在终端:

  • 也提示我们 如果要在生产环境中 可以通过两种方式实现, 第二种是在程序中,创建 engine 实例之前调用就行了。

  • 下面是 gin源码里的三种模式常量

    const (// DebugMode indicates gin mode is debug.DebugMode = "debug"// ReleaseMode indicates gin mode is release.ReleaseMode = "release"// TestMode indicates gin mode is test.TestMode = "test"
    )
    

(1)go web开发之 zap日志库的使用及gin框架配置zap记录日志详细文档讲解分析相关推荐

  1. Python Web开发之WSGI

    Python Web开发之WSGI WSGI(全称Web Server Gate Interface,Web服务器网关接口)是Python为了规范和简化Web服务开发过程,定义了一种Web服务器和应用 ...

  2. 移动web开发之rem布局(rem基础、媒体查询、 less 基础、rem适配方案)

    移动web开发之rem布局 一.rem基础 rem单位 rem (root em)是一个相对单位,类似于em,em是父元素字体大小. 不同的是rem的基准是相对于html元素的字体大小. 比如,根元素 ...

  3. Swift Web 开发之 Vapor - 入门(一)

    简介 Vapor 是一个基于纯 Swift 构建出的 Web 开发框架,目前可以运行在 macOS 和 Ubuntu ,用于构建出漂亮易用的网站或者 API 服务. 官方称是用的最多的 Swift w ...

  4. 18. 【移动Web开发之rem适配布局】

    文章目录 [移动Web开发之rem适配布局]前端小抄(18) 一.rem单位 1.1 rem 单位 二.媒体查询 2.1 什么是媒体查询 2.2 语法规范 2.2.1 mediatype 查询类型 2 ...

  5. 「学习笔记」移动Web开发之rem适配布局10

    「学习笔记」移动Web开发之rem适配布局10 一.rem单位 1.1 rem 单位 二.媒体查询 2.1 什么是媒体查询 2.2 语法规范 2.2.1 mediatype 查询类型 2.2.2 关键 ...

  6. python 动态调整控件大小_python GUI库图形界面开发之PyQt5动态(可拖动控件大小)布局控件QSplitter详细使用方法与实例...

    PyQt5动态(可拖动控件大小)布局控件QSplitter简介 PyQt还提供了特殊的布局管理器QSplitter.它可以动态地拖动子控件之间的边界,算是一个动态的布局管理器,QSplitter允许用 ...

  7. go语言 gin框架中集成zap日志库

    在go语言gin框架中,日志是默认输出到终端的,但是我们在实际工作中,一般来说是需要记录服务器日志的.而最常用的日志库就是zap日志库,我们需要将gin在终端输出的内容通过zap日志库记录到文件中,首 ...

  8. Go开发中配置一个Logger日志的功能实现(结合zap日志库)

    为什么需要Logger 一般在开发项目的时候我们都是需要一个存储日志的文件,因为在部署项目以后,我们只能通过去筛查日志进行检索问题,这时候日志是否可以呈现清晰这个对于我们进行排查工作是十分重要的,所以 ...

  9. 【Go进阶】如何让你Go项目中日志清晰有趣-Zap日志库

    本文先介绍了Go语言原生的日志库的使用,然后详细介绍了非常流行的Uber开源的zap日志库,同时介绍了如何搭配Lumberjack实现日志的切割和归档. Zap日志库在Go语言项目中的使用 在许多Go ...

最新文章

  1. 点击改变div高度_css实现div两列布局(两种方法)
  2. Velocity Toolbox
  3. 每日 30 秒 ⏱ 无障碍世界
  4. matlab fsolve()函数的使用。
  5. 一切从用户的需求与体验出发
  6. sizeof()计算结构体的大小
  7. 这就是搜索引擎--读书笔记五--索引的建立与更新
  8. 15日直播预告丨SQL条件等价改写秘笈(主讲人:怀晓明)
  9. One River CEO:从长远来看比特币可能达到每枚50万美元
  10. 阿里Sophix热修复框架使用入门
  11. Android WebView 播放视频无法播放问题和视频适应屏幕大小
  12. 接口测试+自动化接口测试详解入门到精通
  13. 利用Python库中的imageio生成GIF格式的动图
  14. bzoj 4134: ljw和lzr的hack比赛 sg函数+字典树
  15. 幼儿园案例经验迁移_【投石问路】让案例分析成为幼儿教师自我成长的阶梯
  16. epics安装css,EPICS-synApps/areaDetector安装
  17. MYSQL5.7(64位)安装包及安装步骤
  18. 投资常识-指数-A股各指数特点
  19. 伴随状语的动作与主句的动作间的关系
  20. 崔毅东 C++程序设计入门(下) 第9单元:白公曾咏牡丹芳,一种鲜妍独“异常” 笔记

热门文章

  1. java sse spring_【SpringBoot WEB 系列】SSE 服务器发送事件详解
  2. 【开发经验】服务器单向推送——SSE
  3. Linux和Windows系统目录结构对比
  4. 基于SSM和Boostrap实现的电影评论网站设计
  5. 软考A计划-电子商务设计师-系统开发项目管理
  6. 字体当货币? ——字体手护计划带来创意“字助商店”弘扬汉字文化
  7. Excel截取两个相同字符前中后字符串
  8. 说一下 Deferred Shading MSAA那些事
  9. 【常见CSS扫盲之渐变效果】好看的24种CSS渐变效果汇总(附源码)
  10. oracle的shared server模式和dedicated server模式