Golang 语言的标准库 log 包怎么使用?

简介: Golang 语言的标准库 log 包怎么使用?

01

介绍


Golang 语言的标准库中提供了一个简单的 log 日志包,它不仅提供了很多函数,还定义了一个包含很多方法的类型 Logger。但是它也有缺点,比如不支持区分日志级别,不支持日志文件切割等。

02

函数


Golang 的 log 包主要提供了以下几个具备输出功能的函数:

  • func Fatal(v ...interface{})
  • func Fatalf(format string, v ...interface{})
  • func Fatalln(v ...interface{})
  • func Panic(v ...interface{})
  • func Panicf(format string, v ...interface{})
  • func Panicln(v ...interface{})
  • func Print(v ...interface{})
  • func Printf(format string, v ...interface{})
  • func Println(v ...interface{})

这些函数的使用方法和 fmt 包完全相同,通过查看源码可以发现,Fatal[ln|f]Panic[ln|f] 实际上是调用的 Print[ln|f],而 Print[ln|f] 实际上是调用的 Output() 函数。

其中 Fatal[ln|f] 是调用 Print[ln|f] 之后,又调用了 os.Exit(1) 退出程序。

其中 Panic[ln|f] 是调用 Panic[ln|f] 之后,又调用了 panic() 函数,抛出一个恐慌。

所以,我们很有必要阅读一下 Output() 函数的源码。

函数 Output() 的源码:

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
}

通过阅读 Output() 函数的源码,可以发现使用互斥锁来保证多个 goroutine 写日志的安全,并且在调用 runtime.Caller() 函数之前,先释放互斥锁,获取到信息后再加上互斥锁来保证安全。

使用 formatHeader() 函数来格式化日志的信息,然后保存到 buf 中,然后再把日志信息追加到 buf 的末尾,然后再通过判断,查看日志是否为空或末尾不是 \n,如果是就再把 \n 追加到 buf 的末尾,最后将日志信息输出。

函数 Output() 的源码也比较简单,其中最值得注意的是 runtime.Caller() 函数,源码如下:

func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
 rpc := make([]uintptr, 1)
 n := callers(skip+1, rpc[:])
 if n < 1 {
  return
 }
 frame, _ := CallersFrames(rpc).Next()
 return frame.PC, frame.File, frame.Line, frame.PC != 0
}

通过阅读 runtime.Caller() 函数的源码,可以发现它接收一个 int 类型的参数 skip,该参数表示跳过栈帧数,log 包中的输出功能的函数,使用的默认值都是 2,原因是什么?

举例说明,比如在 main 函数中调用 log.Print,方法调用栈为 main->log.Print->*Logger.Output->runtime.Caller,所以此时参数 skip 的值为 2,表示 main 函数中调用 log.Print 的源文件和代码行号;

参数值为 1,表示 log.Print 函数中调用 *Logger.Output 的源文件和代码行号;参数值为 0,表示 *Logger.Output 函数中调用 runtime.Caller 的源文件和代码行号。

至此,我们发现 log 包的输出功能的函数,全部都是把信息输出到控制台,那么该怎么将信息输出到文件中呢?

函数 SetOutPut 就是用来设置输出目标的,源码如下:

func SetOutput(w io.Writer) {
 std.mu.Lock()
 defer std.mu.Unlock()
 std.out = w
}

我们可以通过函数 os.OpenFile 来打开一个用于 I/O 的文件,返回值作为函数 SetOutput 的参数。

除此之外,读者应该还发现了一个问题,输出信息都是以日期和时间开头,我们该怎么记录更加丰富的信息呢?比如源文件和行号。

这就用到了函数 SetFlags,它可以设置输出的格式,源码如下:

func SetFlags(flag int) {
 std.SetFlags(flag)
}

参数 flag 的值可以是以下任意常量:

const (
 Ldate         = 1 << iota     // the date in the local time zone: 2009/01/23
 Ltime                         // the time in the local time zone: 01:23:23
 Lmicroseconds                 // microsecond resolution: 01:23:23.123123.  assumes Ltime.
 Llongfile                     // full file name and line number: /a/b/c/d.go:23
 Lshortfile                    // final file name element and line number: d.go:23. overrides Llongfile
 LUTC                          // if Ldate or Ltime is set, use UTC rather than the local time zone
 Lmsgprefix                    // move the "prefix" from the beginning of the line to before the message
 LstdFlags     = Ldate | Ltime // initial values for the standard logger
)

其中 LdateLtimeLmicroseconds 分别表示日期、时间和微秒,需要注意的是,如果设置 Lmicroseconds,那么设置 Ltime,也不会生效。

其中 LlongfileLshortfile 分别代码绝对路径、源文件名、行号,和代码相对路径、源文件名、行号,需要注意的是,如果设置 Lshortfile,那么即使设置 Llongfile,也不会生效。

其中 LUTC 表示设置时区为 UTC 时区。

其中 LstdFlags 表示标准记录器的初始值,包含日期和时间。

截止到现在,还缺少点东西,就是日志信息的前缀,比如我们需要区分日志信息为 DEBUG、INFO 和 ERROR。是的,我们还有一个函数 SetPrefix 可以实现此功能,源码如下:

func SetPrefix(prefix string) {
 std.SetPrefix(prefix)
}

函数 SetPrefix 接收一个 string 类型的参数,用来设置日志信息的前缀。

03

Logger

log 包定义了一个包含很多方法的类型 Logger。我们通过查看输出功能的函数,发现它们都是调用 std.Outputstd 是什么?我们查看 log 包的源码。

type Logger struct {
 mu     sync.Mutex // ensures atomic writes; protects the following fields
 prefix string     // prefix on each line to identify the logger (but see Lmsgprefix)
 flag   int        // properties
 out    io.Writer  // destination for output
 buf    []byte     // for accumulating text to write
}
func New(out io.Writer, prefix string, flag int) *Logger {
 return &Logger{out: out, prefix: prefix, flag: flag}
}
var std = New(os.Stderr, "", LstdFlags)

通过阅读源码,我们发现 std 实际上是 Logger 类型的一个实例,OutputLogger 的一个方法。

std 通过 New 函数创建,参数分别是 os.Stderr、空字符串和 LstdFlags,分别表示标准错误输出、空字符串前缀和日期时间。

Logger 类型的字段,注释已经说明了,这里就不再赘述了。

自定义 Logger:

func main () {
 logFile, err := os.OpenFile("error1.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0755)
 if err != nil {
  fmt.Println(err)
  return
 }
 defer logFile.Close()
 logs := DefinesLogger(logFile, "", log.LstdFlags|log.Lshortfile)
 logs.Debug("message")
 logs.Debugf("%s", "content")
}
// 自定义 logger
type Logger struct {
 definesLogger *log.Logger
}
type Level int8
const(
 LevelDebug Level = iota
 LevelInfo
 LevelError
)
func (l Level) String() string {
 switch l {
 case LevelDebug:
  return " [debug] "
 case LevelInfo:
  return " [info] "
 case LevelError:
  return " [error] "
 }
 return ""
}
func DefinesLogger(w io.Writer, prefix string, flag int) *Logger {
 l := log.New(w, prefix, flag)
 return &Logger{definesLogger: l}
}
func (l *Logger) Debug(v ...interface{}) {
 l.definesLogger.Print(LevelDebug, fmt.Sprint(v...))
}
func (l *Logger) Debugf(format string, v ...interface{}) {
 l.definesLogger.Print(LevelDebug, fmt.Sprintf(format, v...))
}
func (l *Logger) Info(v ...interface{}) {
 l.definesLogger.Print(LevelInfo, fmt.Sprint(v...))
}
func (l *Logger) Infof(format string, v ...interface{}) {
 l.definesLogger.Print(LevelInfo, fmt.Sprintf(format, v...))
}
func (l *Logger) Error(v ...interface{}) {
 l.definesLogger.Print(LevelError, fmt.Sprint(v...))
}
func (l *Logger) Errorf(format string, v ...interface{}) {
 l.definesLogger.Print(LevelError, fmt.Sprintf(format, v...))
}

04

总结


本文主要介绍 Golang 语言的标准库中的 log 包,包括 log 包的函数和自定义类型 logger 的使用方法和一些细节上的注意事项。开篇也提到了,log 包不支持日志文件的切割,我们需要自己编码去实现,或者使用三方库,比如 lumberjack。在生产环境中,一般比较少用 log 包来记录日志,通常会使用三方库来记录日志,比如 zaplogrus 等。





相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2月前
|
Java 开发工具 Windows
Windows环境下面启动jar包,输出的日志出现乱码的解决办法
Windows环境下面启动jar包,输出的日志出现乱码的解决办法
|
2月前
|
SQL 前端开发 Go
编程笔记 GOLANG基础 001 为什么要学习Go语言
编程笔记 GOLANG基础 001 为什么要学习Go语言
|
4月前
|
物联网 Go 网络性能优化
使用Go语言(Golang)可以实现MQTT协议的点对点(P2P)消息发送。MQTT协议本身支持多种消息收发模式
使用Go语言(Golang)可以实现MQTT协议的点对点(P2P)消息发送。MQTT协议本身支持多种消息收发模式【1月更文挑战第21天】【1月更文挑战第104篇】
109 1
|
1天前
|
Go API 开发者
Golang深入浅出之-文件与目录操作:os与path/filepath包
【4月更文挑战第26天】Go语言标准库`os`和`path/filepath`提供文件读写、目录操作等功能。本文涵盖`os.Open`, `os.Create`, `os.Mkdir`, `filepath.Join`等API的使用,强调了文件关闭、路径处理、并发写入和权限问题的处理,并给出实战代码示例,帮助开发者高效、安全地操作文件与目录。注意使用`defer`关闭文件,`filepath`处理路径分隔符,以及通过同步机制解决并发写入冲突。
13 2
|
1天前
|
安全 Unix Go
Golang深入浅出之-Go语言中的时间与日期处理:time包详解
【4月更文挑战第26天】Go语言的`time`包提供处理日期和时间的功能,包括`time.Time`类型、时间戳、格式化与解析。本文讨论了核心概念、常见问题(如时区处理、格式字符串混淆、超时控制和并发安全)及解决方法。推荐使用`time.LoadLocation`管理时区,熟悉时间格式规则,用`context`精确控制超时,并注意并发安全。文中通过代码示例展示了如何获取格式化时间、计算时间差以及创建定时任务。学习和应用这些知识可提高程序的健壮性和准确性。
15 2
|
1天前
|
XML JSON Go
Golang深入浅出之-XML处理在Go语言中的实现:encoding/xml包
【4月更文挑战第26天】Go语言的`encoding/xml`库提供XML处理,包括序列化和反序列化。本文讨论了XML处理的基础,如`xml.Marshal`和`xml.Unmarshal`函数,以及常见问题和易错点,如标签命名、结构体嵌套、omitempty标签和命名空间。建议遵循标签命名规则,正确处理嵌套和属性,谨慎使用omitempty,以及理解并有效利用命名空间。文中还给出了基础示例和处理XML属性的代码示例,帮助读者掌握XML处理技巧。
11 1
Golang深入浅出之-XML处理在Go语言中的实现:encoding/xml包
|
2天前
|
JSON 编解码 Go
Golang深入浅出之-HTTP客户端编程:使用net/http包发起请求
【4月更文挑战第25天】Go语言`net/http`包提供HTTP客户端和服务器功能,简化高性能网络应用开发。本文探讨如何发起HTTP请求,常见问题及解决策略。示例展示GET和POST请求的实现。注意响应体关闭、错误处理、内容类型设置、超时管理和并发控制。最佳实践包括重用`http.Client`,使用`context.Context`,处理JSON以及记录错误日志。通过实践这些技巧,提升HTTP编程技能。
16 1
|
3天前
|
Go 开发者
Golang深入浅出之-HTTP客户端编程:使用net/http包发起请求
【4月更文挑战第24天】Go语言的`net/http`包在HTTP客户端编程中扮演重要角色,但使用时需注意几个常见问题:1) 检查HTTP状态码以确保请求成功;2) 记得关闭响应体以防止资源泄漏;3) 设置超时限制,避免长时间等待;4) 根据需求处理重定向。理解这些细节能提升HTTP客户端编程的效率和质量。
15 1
|
4天前
|
Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第23天】Go语言的`context`包提供`Context`接口用于处理任务取消、超时和截止日期。通过传递`Context`对象,开发者能轻松实现复杂控制流。本文解析`context`包特性,讨论常见问题和解决方案,并给出代码示例。关键点包括:1) 确保将`Context`传递给所有相关任务;2) 根据需求选择适当的`Context`创建函数;3) 定期检查`Done()`通道以响应取消请求。正确使用`context`包能提升Go程序的控制流管理效率。
14 1
|
5天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello(&quot;Alice&quot;)`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
13 1