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 相关文章推荐
Go各时间字符串使用解析
Apr 02 Golang
Golang 空map和未初始化map的注意事项说明
Apr 29 Golang
golang 定时任务方面time.Sleep和time.Tick的优劣对比分析
May 05 Golang
关于golang高并发的实现与注意事项说明
May 08 Golang
浅谈Golang 切片(slice)扩容机制的原理
Jun 09 Golang
Go语言实现Base64、Base58编码与解码
Jul 26 Golang
Golang并发操作中常见的读写锁详析
Aug 30 Golang
golang实现一个简单的websocket聊天室功能
Oct 05 Golang
一文搞懂Golang 时间和日期相关函数
Dec 06 Golang
Golang 1.18 多模块Multi-Module工作区模式的新特性
Apr 11 Golang
Golang bufio详细讲解
Apr 21 Golang
Golang入门之计时器
May 04 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
一个ftp类(ini.php)
2006/10/09 PHP
PHP nl2br函数 将换行字符转成 <br>
2009/08/21 PHP
比较discuz和ecshop的截取字符串函数php版
2012/09/03 PHP
PHP中基于ts与nts版本- vc6和vc9编译版本的区别详解
2013/04/26 PHP
PHP中浮点数计算比较及取整不准确的解决方法
2015/01/09 PHP
php实现上传图片文件代码
2015/07/19 PHP
thinkphp jquery实现图片上传和预览效果
2020/07/22 PHP
Yii CDBCriteria常用方法实例小结
2017/01/19 PHP
PHP简单实现欧拉函数Euler功能示例
2017/11/06 PHP
Firefox中autocomplete="off" 设置不起作用Bug的解决方法
2011/03/25 Javascript
open 动态修改img的onclick事件示例代码
2013/11/13 Javascript
JavaScript实现的内存数据库LokiJS介绍和入门实例
2014/11/17 Javascript
Javascript将数值转换为金额格式(分隔千分位和自动增加小数点)
2016/06/22 Javascript
ES6新特征数字、数组、字符串
2016/10/01 Javascript
微信小程序 flex实现导航实例详解
2017/04/26 Javascript
Angular利用内容投射向组件输入ngForOf模板的方法
2018/03/05 Javascript
vue+iview 兼容IE11浏览器的实现方法
2019/01/07 Javascript
详解vue中的父子传值双向绑定及数据更新问题
2019/06/13 Javascript
[38:32]DOTA2上海特级锦标赛A组资格赛#2 Secret VS EHOME第二局
2016/02/26 DOTA
Python开发中爬虫使用代理proxy抓取网页的方法示例
2017/09/26 Python
Python实现嵌套列表去重方法示例
2017/12/28 Python
Python 3.7新功能之dataclass装饰器详解
2018/04/21 Python
使用Python处理Excel表格的简单方法
2018/06/07 Python
如何基于python生成list的所有的子集
2019/11/11 Python
关于ZeroMQ 三种模式python3实现方式
2019/12/23 Python
Python实现i人事自动打卡的示例代码
2020/01/09 Python
ITK 实现多张图像转成单个nii.gz或mha文件案例
2020/07/01 Python
css3实现多个元素依次显示效果
2017/12/12 HTML / CSS
基于Html5实现的语音搜索功能
2019/05/13 HTML / CSS
美国杂志订阅折扣与优惠网站:Magazines.com
2016/08/31 全球购物
法国综合购物网站:RueDuCommerce
2016/09/12 全球购物
戴尔马来西亚官网:Dell Malaysia
2020/05/02 全球购物
社区班子对照检查材料
2014/08/27 职场文书
2014年流动人口工作总结
2014/11/26 职场文书
2015年药房工作总结
2015/04/25 职场文书
解决jupyter notebook启动后没有token的坑
2021/04/24 Python