首页 > 其他分享 >Go每日一库之9:log

Go每日一库之9:log

时间:2023-09-14 09:56:18浏览次数:40  
标签:调用 log Age 一库 Go 日志 main Name

简介

在日常开发中,日志是必不可少的功能。虽然有时可以用fmt库输出一些信息,但是灵活性不够。Go 标准库提供了一个日志库log。本文介绍log库的使用。

快速使用

log是 Go 标准库提供的,不需要另外安装。可直接使用:

package main

import (
  "log"
)

type User struct {
  Name string
  Age  int
}

func main() {
  u := User{
    Name: "dj",
    Age:  18,
  }

  log.Printf("%s login, age:%d", u.Name, u.Age)
  log.Panicf("Oh, system error when %s login", u.Name)
  log.Fatalf("Danger! hacker %s login", u.Name)
}

log默认输出到标准错误(stderr),每条日志前会自动加上日期和时间。如果日志不是以换行符结尾的,那么log会自动加上换行符。即每条日志会在新行中输出。

log提供了三组函数:

  • Print/Printf/Println:正常输出日志;
  • Panic/Panicf/Panicln:输出日志后,以拼装好的字符串为参数调用panic
  • Fatal/Fatalf/Fatalln:输出日志后,调用os.Exit(1)退出程序。

命名比较容易辨别,带f后缀的有格式化功能,带ln后缀的会在日志后增加一个换行符。

注意,上面的程序中由于调用log.Panicfpanic,所以log.Fatalf并不会调用。

定制

前缀

调用log.SetPrefix为每条日志文本前增加一个前缀。例如,在上面的程序中设置Login:前缀:

package main

import (
  "log"
)

type User struct {
  Name string
  Age  int
}

func main() {
  u := User{
    Name: "dj",
    Age:  18,
  }

  log.SetPrefix("Login: ")
  log.Printf("%s login, age:%d", u.Name, u.Age)
}

调用log.Prefix可以获取当前设置的前缀。

选项

设置选项可在每条输出的文本前增加一些额外信息,如日期时间、文件名等。

log库提供了 6 个选项:

// src/log/log.go
const (
  Ldate         = 1 << iota
  Ltime                    
  Lmicroseconds            
  Llongfile                
  Lshortfile               
  LUTC                     
)
  • Ldate:输出当地时区的日期,如2020/02/07
  • Ltime:输出当地时区的时间,如11:45:45
  • Lmicroseconds:输出的时间精确到微秒,设置了该选项就不用设置Ltime了。如11:45:45.123123
  • Llongfile:输出长文件名+行号,含包名,如github.com/go-quiz/go-daily-lib/log/flag/main.go:50
  • Lshortfile:输出短文件名+行号,不含包名,如main.go:50
  • LUTC:如果设置了LdateLtime,将输出 UTC 时间,而非当地时区。

调用log.SetFlag设置选项,可以一次设置多个:

package main

import (
  "log"
)

type User struct {
  Name string
  Age  int
}

func main() {
  u := User{
    Name: "dj",
    Age:  18,
  }

  log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds)

  log.Printf("%s login, age:%d", u.Name, u.Age)
}

调用log.Flags()可以获取当前设置的选项。

运行代码,输出:

2020/02/07 11:56:59.061615 main.go:20: dj login, age:18

注意,调用log.SetFlag之后,原有的选项会被覆盖掉!

log库还定义了一个Lstdflag,为Ldate | Ltime,这就是我们默认的选项。

// src/log/log.go
const (
  LstdFlags = Ldate | Ltime
)

这就是为什么默认情况下,每条日志前会自动加上日期和时间。

自定义

实际上,log库为我们定义了一个默认的Logger,名为std,意为标准日志。我们直接调用的log库的方法,其内部是调用std的对应方法:

// src/log/log.go
var std = New(os.Stderr, "", LstdFlags)

func Printf(format string, v ...interface{}) {
  std.Output(2, fmt.Sprintf(format, v...))
}

func Fatalf(format string, v ...interface{}) {
  std.Output(2, fmt.Sprintf(format, v...))
  os.Exit(1)
}

func Panicf(format string, v ...interface{}) {
  s := fmt.Sprintf(format, v...)
  std.Output(2, s)
  panic(s)
}

当然,我们也可以定义自己的Logger

package main

import (
  "bytes"
  "fmt"
  "log"
)

type User struct {
  Name string
  Age  int
}

func main() {
  u := User{
    Name: "dj",
    Age:  18,
  }

  buf := &bytes.Buffer{}
  logger := log.New(buf, "", log.Lshortfile|log.LstdFlags)

  logger.Printf("%s login, age:%d", u.Name, u.Age)

  fmt.Print(buf.String())
}

log.New接受三个参数:

  • io.Writer:日志都会写到这个Writer中;
  • prefix:前缀,也可以后面调用logger.SetPrefix设置;
  • flag:选项,也可以后面调用logger.SetFlag设置。

上面代码将日志输出到一个bytes.Buffer,然后将这个buf打印到标准输出。

运行代码:

$ go run main.go 
2020/02/07 13:48:54 main.go:23: dj login, age:18

注意到,第一个参数为io.Writer,我们可以使用io.MultiWriter实现多目的地输出。下面我们将日志同时输出到标准输出、bytes.Buffer和文件中:

package main

import (
  "bytes"
  "io"
  "log"
  "os"
)

type User struct {
  Name string
  Age  int
}

func main() {
  u := User{
    Name: "dj",
    Age:  18,
  }

  writer1 := &bytes.Buffer{}
  writer2 := os.Stdout
  writer3, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE, 0755)
  if err != nil {
    log.Fatalf("create file log.txt failed: %v", err)
  }

  logger := log.New(io.MultiWriter(writer1, writer2, writer3), "", log.Lshortfile|log.LstdFlags)
  logger.Printf("%s login, age:%d", u.Name, u.Age)
}

如果你愿意,还可以发送到到网络。

实现

log库的核心是Output方法,我们简单看一下:

// src/log/log.go
func (l *Logger) Output(calldepth int, s string) error {
  now := time.Now() // get this early.
  var file string
  var line int
  l.mu.Lock()
  defer l.mu.Unlock()
  if l.flag&(Lshortfile|Llongfile) != 0 {
    // Release lock while getting caller info - it's expensive.
    l.mu.Unlock()
    var ok bool
    _, file, line, ok = runtime.Caller(calldepth)
    if !ok {
      file = "???"
      line = 0
    }
    l.mu.Lock()
  }
  l.buf = l.buf[:0]
  l.formatHeader(&l.buf, now, file, line)
  l.buf = append(l.buf, s...)
  if len(s) == 0 || s[len(s)-1] != '\n' {
    l.buf = append(l.buf, '\n')
  }
  _, err := l.out.Write(l.buf)
  return err
}

如果设置了LshortfileLlongfileOuput方法中会调用runtime.Caller获取文件名和行号。runtime.Caller的参数calldepth表示获取调用栈向上多少层的信息,当前层为 0。

一般的调用路径是:

  • 程序中使用log.Printf之类的函数;
  • log.Printf内调用std.Output

我们在Output方法中需要获取调用log.Printf的文件和行号。

calldepth传入 0 表示Output方法内调用runtime.Caller的那一行信息,传入 1 表示log.Printf内调用std.Output那一行的信息,

传入 2 表示程序中调用log.Printf的那一行信息。显然这里要用 2。

然后调用formatHeader处理前缀和选项。

最后将生成的字节流写入到Writer中。

这里有两个优化技巧:

  • 由于runtime.Caller调用比较耗时,先释放锁,避免等待时间过长;
  • 为了避免频繁的内存分配,logger中保存了一个类型为[]bytebuf,可重复使用。前缀和日志内容先写到这个buf中,然后统一写入Writer,减少 io 操作。

总结

log实现了一个小巧的日志库,可供简单使用。本文介绍了它的基本使用,简单地分析了一下源码。

如果log库的功能不能满足需求,我们可以在它之上做二次封装。看煎鱼大佬的这篇文章

除此之外,社区也涌现了很多优秀的、功能丰富的日志库,可以选用。

参考

  1. log官方文档

标签:调用,log,Age,一库,Go,日志,main,Name
From: https://www.cnblogs.com/arena/p/17701683.html

相关文章

  • Golang map集合丶struct结构体
    一.map集合1//map键值对集合2functestMap(){3//Map的定义:var变量名map[keytType]valueType4//细节:5//1.key唯一6//2.map是引用7//3.直接遍历map是无序的8//4.map会自动扩容,make中设置的长度并没有对map任何限制......
  • 从Go1.16开始,ioutil.ReadAll、ioutil.ReadFile和ioutil.ReadDir被弃用
    废弃ioutil包后的替换函数ioutil.ReadAll->io.ReadAllioutil.ReadFile->os.ReadFileioutil.ReadDir->os.ReadDirothersioutil.NopCloser->io.NopCloserioutil.ReadDir->os.ReadDirioutil.TempDir->os.MkdirTempioutil.TempFile->os.CreateT......
  • public ::google::protobuf::Message
     public::google::protobuf::Messagefilelist继承..... .protomessageabc{requiredstringaa=1;optionalstringbb=4; }messageDeparts{repeatedabccc=1;......
  • gorm stdErr = sql: Scan error on column index 0, name "total": converting NULL
    前言使用gorm查询时,报错:stdErr=sql:Scanerroroncolumnindex0,name"total":convertingNULLtofloat64isunsupported代码如下vartotalfloat64res:=db.Model(&model.Record{}).Select("sum(amount)astotal").Where("id=?andtyp......
  • Go每日一库之8:cast(类型转换)
    简介今天我们再来介绍spf13大神的另一个库cast。cast是一个小巧、实用的类型转换库,用于将一个类型转为另一个类型。最初开发cast是用在hugo中的。快速使用先安装:$gogetgithub.com/spf13/cast后使用:packagemainimport("fmt""github.com/spf13/cast")fun......
  • 基于Django的社区疫情管理平台的设计与实现-计算机毕业设计源码+LW文档
    一、研究的背景和意义研究背景:2020年初,新冠疫情在武汉爆发,造成多人感染,社会治安和医疗体系面临巨大挑战。在这前所未有的严峻形式中,国家领导统一指挥,全面部署,积极应对挑战。社区是最小单位,社区防控做好才能取得关键性胜利。在没有经验基础的情况下,各社区除了执行政策条例外,积极摸......
  • (转)Go语言基础——内置函数
    原文:https://blog.csdn.net/m0_60496161/article/details/130836218内置类型值类型:boolint(32or64),int8,int16,int32,int64uint(32or64),uint8(byte),uint16,uint32,uint64float32,float64stringcomplex64,complex128array--固定......
  • (转)对比学习:Golang VS Python3
    原文:https://juejin.cn/post/6844903843050815502Golang和Python都是目前在各自领域最流行的开发语言之一。Golang其高效而又友好的语法,赢得了很多后端开发人员的青睐,最适用于高并发网络编程的语言之一。Python不用说,TIOBE排行榜的前十常驻居民,现在已经稳定在前五了。在机器......
  • 你知道Golang的模板怎么用吗?带你了解动态文本的生成!
    GolangTemplateGo语言中的GoTemplate是一种用于生成文本输出的简单而强大的模板引擎。它提供了一种灵活的方式来生成各种格式的文本,例如HTML、XML、JSON等。GoTemplate的具有以下主要特性:简洁易用:GoTemplate语法简洁而易于理解。它使用一对双大括号“{{}}”来标记模板的......
  • 2023-09-13:用go语言,给定一个整数数组 nums 和一个正整数 k, 找出是否有可能把这个数组
    2023-09-13:用go语言,给定一个整数数组nums和一个正整数k,找出是否有可能把这个数组分成k个非空子集,其总和都相等。输入:nums=[4,3,2,3,5,2,1],k=4。输出:True。来自左程云。答案2023-09-13:第一种算法(canPartitionKSubsets1)使用动态规划的思想,具体过程如下:1.计算数组......