Golang日志包的使用


Posted in Golang onApril 20, 2022

引言:

在我们的日常编程中,日志很重要。只要是我们写代码,就有可能出现 Bug。日志文件就是一种快速找到这些 bug,更好地了解程序工作状态的方法。

让我们来看一下日志文件的定义:

日志文件是记录操作系统或其他软件运行中发生的事件或通信软件的不同用户之间的消息的文件。记录是保存日志的行为。

日志是开发人员的眼睛和耳朵,可以用来跟踪、定位错误、调试和分析代码,并监控应用程序的性能。在最简单的情况下,消息被写入单个日志文件。

Go 语言标准库之log 包

正因为日志很重要,所以 Go 语言标准库提供了 ​​log​​ 包,可以对日志做一些简单的配置,我们可以定制一套自己的日志记录器。

Golang 中的 ​​log​​ 包实现了简单的 logging 包。它定义了一个类型、Logger 以及格式化输出的方法。 它还具有预定义的“标准”记录器,可通过辅助函数 ​​Print[f|ln]​​、​​Fatal[f|ln]​​ 和 ​​Panic[f|ln]​​ 访问,它们比手动创建记录器更易于使用。

基本的日志项包括:前缀、日期时间戳、该日志由哪个源文件记录的、源文件记录日志所在行,最后是日志消息。让我们来看一下简单的使用 ​​log​​ 包:在除法运算时除 0 时的消息返回程序,而不是直接退出程序。

package main
import (
"errors"
"fmt"
"log"
)
func myDiv(x float64, y float64) (float64, error) {
if y == 0 {
return 0, errors.New("被除数不能为0")
}
return x / y, nil
}
func main() {
var x float64 = 128.2
var y float64
res, err := myDiv(x, y)
if err != nil {
log.Print(err) // Print 写到标准日志记录器
}
fmt.Println(res)
}

运行该程序:

$ go run learninglog.go
2022/04/19 23:18:06 被除数不能为0
0

在上面的程序中,我们导入了三个包:​​errors​​ 、​​fmt​​、​​log​​ 。

然后使用了 ​​log​​ 包中的 ​​Print​​ 函数:此时日志记录器将写入标准错误并打印每条记录消息的日期和时间,这在一般情况下非常有用。每条日志消息在单独的行上都有一个输出:如果正在打印的消息没有在新行结束,记录器将添加一行。

另外,​​log​​ 包中的 ​​Fatal​​ 函数在写入日志消息后调用 ​​os.Exit(1)​​。

func main() {
var x float64 = 128.2
var y float64
res, err := myDiv(x, y)
if err != nil {
log.Fatal(err) // 在调用 Print() 之后会接着调用 os.Exit(1)
}
fmt.Println(res)
}
// 2022/04/19 23:22:44 被除数不能为0
// exit status 1

而 ​​Panic​​ 函数在写入日志消息后调用 ​​panic​​,除非程序执行 ​​recover()​​ 函数,否则会导致程序打印调用栈后终止。

func main() {
var x float64 = 128.2
var y float64
res, err := myDiv(x, y)
if err != nil {
log.Panic(err) // Panic 会在调用 Print() 之后接着用 panic()
}
fmt.Println(res)
}

运行结果:

2022/04/19 23:24:18 被除数不能为0
panic: 被除数不能为0

goroutine 1 [running]:
log.Panic({0xc000086f60?, 0xc000086f70?, 0x404f99?})
/usr/local/go/src/log/log.go:385 +0x65
main.main()
/home/wade/go/src/logLearning/learninglog.go:26 +0x65
exit status 2

由此可知,​​Print​​ 系列函数才是写日志消息的标准方法。

如何将日志消息存储在 Go 中的文件中

上述代码中,只是简单把日志消息打印到控制台,这是远远不够的,因为控制台是实时的,如果关闭控制台日志消息也就关闭了。

鉴于我们已经学习了 Go 语言如何读取文件,所以就可以把日志消息存储到一个文件中,把所有的日志存储到该文件中。

package main
import (
"errors"
"fmt"
"log"
"os"
)
func myDiv(x float64, y float64) (float64, error) {
if y == 0 {
return 0, errors.New("被除数不能为0")
}
return x / y, nil
}
func main() {
var x float64 = 128.2
var y float64
res, exception := myDiv(x, y)
file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
log.Fatal(err)
}
defer file.Close() // 关闭文件
log.SetOutput(file)
log.Print(exception)
fmt.Println(res)
}

运行代码:

$ go run learninglog.go
0

此外,我们会发现目录中多了一个创建的 ​​info.log​​ 文件。打开文件,你会看到打印出类似下面的内容。

定制你的日志记录器

要想创建一个定制的日志记录器,我们需要创建一个 ​​Logger​​ 类型的结构体,然后给每个日志记录器配置一个输出目的地、前缀和标志。而且每一个日志记录器是多 goroutine 安全的,每一个 ​​Logger​​ 结构体都有一个互斥锁,意味着多个 goroutine 可以同时调用来自同一个日志记录器的这些函数,而不会有彼此间的写冲突。来看一下 ​​Logger​​ 结构体的底层实现:

Logger结构体

// A Logger represents an active logging object that generates lines of
// output to an io.Writer. Each logging operation makes a single call to
// the Writer's Write method. A Logger can be used simultaneously from
// multiple goroutines; it guarantees to serialize access to the Writer.
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
}

然后我们来看一个示例程序:

package main
import (
"io"
"io/ioutil"
"log"
"os"
)
var (
Trace *log.Logger // 记录所有日志
Info *log.Logger // 重要的信息
Warning *log.Logger // 警告信息
Error *log.Logger // 错误信息
)
func init() {
file, err := os.OpenFile("errors.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open error log file: ", err)
}
Trace = log.New(ioutil.Discard,
"Trace: ", log.Ldate|log.Ltime|log.Lshortfile)
Info = log.New(os.Stdout, "Info: ", log.Ldate|log.Ltime|log.Lshortfile)
Warning = log.New(os.Stdout, "Warning: ", log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(io.MultiWriter(file, os.Stderr), "Error: ", log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
Trace.Println("hello")
Info.Println("Information")
Warning.Println("Warning")
Error.Println("Error")
}

运行结果:

Info: 2022/04/20 00:37:34 learninglog.go:36: Information
Warning: 2022/04/20 00:37:34 learninglog.go:37: Warning
Error: 2022/04/20 00:37:34 learninglog.go:38: Error

使用 ​​log​​ 包的 ​​New​​ 函数,创建并初始化一个 Logger 类型的值,然后 ​​New​​ 函数返回新创建的值的地址。

New 函数

func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag
}
  • New 函数的第一个参数 out 指定了日志要写入的目的地,这个参数传入的值必须实现了 io.Writer 接口
  • 第二个参数 prefix 会在生成的每行日志的最开始出现
  • 第三个参数 flag 定义日志包含哪些属性

在本程序中:

  • Trace 日志记录器使用了 ioutil 包里的 Discard 变量作为写到的目的地,所有的 Writer 调用都不会有动作,但是会成功返回。当某个等级的日志不重要时,使用 Discard 变量可以禁用这个等级的日志。
  • 日志记录器 Info 和 Warning 都使用 stdout 作为日志输出。
  • 日志记录去 Error 中的 New 函数第一个参数使用了 MultiWriter 函数,这个函数调用会返回一个 io.Writer 接口类型的值,这个值包含之前打开的文件 file,以及 stderr。这个函数是一个变参函数,可以接受任意个实现了 io.Writer 接口的值,所以会把所有传入的 io.Writer 绑定在一起,当对这个返回值进行写入时,会向所有绑在一起的 io.Writer 值做写入。从而实现向多个 Writer 做输出。这样做的好处就是使用 Error 记录器记录日志时,输出会同时写到文件和 stderr 中

总结

Golang 中 ​​log​​ 包的实现是基于对记录日志这个需求长时间的实践和积累而形成的。比如将输出写入 ​​stdout​​,将日志记录到 ​​stderr​​,这同样也是很多基于命令行界面的程序使用的方法。

写日志的第一件事就是找到一个完美的库。然后,在您选择了日志库之后,您还需要计划在代码中的何处调用记录器,如何存储日志,如何在任何给定时间使它们可用,以及如何分析它们。

Go 日志的最佳实践是从主应用程序进程中调用自定义记录器,而不是在 goroutines 中。同时应该将应用程序中的日志消息写入本地文件,做到永久保存,同时针对错误的信息应该给到提醒,方便及时定位和处理。

到此这篇关于如何在 Go语言中使用日志包的文章就介绍到这了!

Golang 相关文章推荐
浅谈golang 中time.After释放的问题
May 05 Golang
Golang Gob编码(gob包的使用详解)
May 07 Golang
golang fmt格式“占位符”的实例用法详解
Jul 04 Golang
go开发alertmanger实现钉钉报警
Jul 16 Golang
golang实现一个简单的websocket聊天室功能
Oct 05 Golang
浅谈GO中的Channel以及死锁的造成
Mar 18 Golang
golang三种设计模式之简单工厂、方法工厂和抽象工厂
Apr 10 Golang
Golang流模式之grpc的四种数据流
Apr 13 Golang
Go语言安装并操作redis的go-redis库
Apr 14 Golang
Golang 结构体数据集合
Apr 22 Golang
Go Grpc Gateway兼容HTTP协议文档自动生成网关
Jun 16 Golang
Golang获取List列表元素的四种方式
Apr 20 #Golang
Golang 对es的操作实例
Apr 20 #Golang
Golang 遍历二叉树
Apr 19 #Golang
Golang MatrixOne使用介绍和汇编语法
Apr 19 #Golang
Golang 字符串的常见操作
Golang 链表的学习和使用
Golang Elasticsearches 批量修改查询及发送MQ
Apr 19 #Golang
You might like
PHP自带ZIP压缩、解压缩类ZipArchiv使用指南
2015/03/03 PHP
php精确的统计在线人数的方法
2015/10/21 PHP
PHP图形计数器程序显示网站用户浏览量
2016/07/20 PHP
根据json字符串生成Html的一种方式
2013/01/09 Javascript
jQuery 获取URL的GET参数值的小例子
2013/04/18 Javascript
iframe的onreadystatechange事件在firefox下的使用
2014/04/16 Javascript
Javascript的严格模式strict mode详细介绍
2014/06/06 Javascript
微信小程序 MINA文件结构
2016/10/17 Javascript
详谈js中window.location.search的用法和作用
2017/02/13 Javascript
js+html制作简单验证码
2017/02/16 Javascript
简单谈谈require模块化jquery和angular的问题
2017/06/23 jQuery
vue实现抖音时间转盘
2019/09/08 Javascript
vue中nextTick用法实例
2019/09/11 Javascript
Vue数字输入框组件示例代码详解
2020/01/15 Javascript
vue跳转页面的几种方法(推荐)
2020/03/26 Javascript
JS数据类型判断的几种常用方法
2020/07/07 Javascript
[46:53]Secret vs Liquid 2019国际邀请赛小组赛 BO2 第一场 8.15
2019/08/17 DOTA
python简单程序读取串口信息的方法
2015/03/13 Python
详解Python中contextlib上下文管理模块的用法
2016/06/28 Python
Python SQLite3简介
2018/02/22 Python
python添加模块搜索路径和包的导入方法
2019/01/19 Python
Python supervisor强大的进程管理工具的使用
2019/04/24 Python
nginx黑名单和django限速,最简单的防恶意请求方法分享
2019/08/09 Python
在TensorFlow中屏蔽warning的方式
2020/02/04 Python
python属于软件吗
2020/06/18 Python
澳大利亚波西米亚风连衣裙在线商店:Fortunate One
2019/04/01 全球购物
CAT鞋加拿大官网:CAT Footwear加拿大
2020/08/05 全球购物
介绍Java的内部类
2012/10/27 面试题
经典C++面试题一
2016/11/06 面试题
若通过ObjectOutputStream向一个文件中多次以追加方式写入object,为什么用ObjectInputStream读取这些object时会产生StreamCorruptedException?
2016/10/17 面试题
员工年终演讲稿
2014/01/03 职场文书
学校运动会霸气口号
2014/06/07 职场文书
节水口号标语
2014/06/19 职场文书
商务专员岗位职责范本
2014/06/29 职场文书
社区母亲节活动总结
2015/02/10 职场文书
遗嘱格式范本
2015/08/07 职场文书