Go语言之log日志

在我们开发程序后,如果有一些问题需要对程序进行调试的时候,日志是必不可少的,这是我们分析程序问题常用的手段。

日志使用

日志分析,就是根据输出的日志信息,分析挖掘可能的问题,我们使用fmt.Println系列函数也可以达到目的,因为它们也可以把我们需要的信息输出到终端或者其他文件中。不过fmt.Println系列函数输出的系统比较简单,比如没有时间,也没有源代码的行数等,对于我们排查问题,缺少了很多信息。

对此,Go语言为我们提供了标准的log包,来跟踪日志的记录。下面我们看看日志包log的使用。

func main() {
    log.Println("飞雪无情的博客:","http://www.flysnow.org")
    log.Printf("飞雪无情的微信公众号:%s\\n","flysnow_org")}

使用非常简单,函数名字和用法也和fmt包很相似,但是它的输出默认带了时间戳。

2017/04/29 13:18:44 飞雪无情的博客: http://www.flysnow.org
2017/04/29 13:18:44 飞雪无情的微信公众号:flysnow_org

这样我们很清晰的就知道了,记录这些日志的时间,这对我们排查问题,非常有用。

有了时间了,我们还想要更多的信息,必然发生的源代码行号等,对此日志包log为我们提供了可定制化的配制,让我们可以自己定制日志的抬头信息。

func init(){
    log.SetFlags(log.Ldate|log.Lshortfile)}

我们使用init函数,这个函数在main函数执行之前就可以初始化,可以帮我们做一些配置,这里我们自定义日志的抬头信息为时间+文件名+源代码所在行号。也就是log.Ldate|log.Lshortfile,中间是一个位运算符|,然后通过函数log.SetFlags进行设置。现在我们再运行下看看输出的日志。

2017/04/29main.go:10: 飞雪无情的博客:http://www.flysnow.org
2017/04/29 main.go:11: 飞雪无情的微信公众号:flysnow_org

比着上一个例子,多了源文件以及行号,但是少了时间,这就是我们自定义出来的结果。现在我们看看log包为我们提供了那些可以定义的选项常量。

const (
    Ldate         = 1 << iota     //日期示例: 2009/01/23
    Ltime                         //时间示例: 01:23:23
    Lmicroseconds                 //毫秒示例: 01:23:23.123123.
    Llongfile                     //绝对路径和行号: /a/b/c/d.go:23
    Lshortfile                    //文件和行号: d.go:23.
    LUTC                          //日期时间转为0时区的
    LstdFlags     = Ldate | Ltime //Go提供的标准抬头信息)

这是log包定义的一些抬头信息,有日期、时间、毫秒时间、绝对路径和行号、文件名和行号等,在上面都有注释说明,这里需要注意的是:如果设置了Lmicroseconds,那么Ltime就不生效了;设置了Lshortfile,Llongfile也不会生效,大家自己可以测试一下。

LUTC比较特殊,如果我们配置了时间标签,那么如果设置了LUTC的话,就会把输出的日期时间转为0时区的日期时间显示。

log.SetFlags(log.Ldate|log.Ltime |log.LUTC)

那么对我们东八区的时间来说,就会减去 8 个小时,我们看输出:

2017/04/29 05:46:29 飞雪无情的博客: http://www.flysnow.org
2017/04/29 05:46:29 飞雪无情的微信公众号:flysnow_org

最后一个LstdFlags表示标准的日志抬头信息,也就是默认的,包含日期和具体时间。

我们大部分情况下,都有很多业务,每个业务都需要记录日志,那么有没有办法,能区分这些业务呢?这样我们在查找日志的时候,就方便多了。

对于这种情况,Go语言也帮我们考虑到了,这就是设置日志的前缀,比如一个用户中心系统的日志,我们可以这么设置。

func init(){

log.SetPrefix("【UserCenter】")

log.SetFlags(log.LstdFlags |log.Lshortfile |log.LUTC)
}

通过log.SetPrefix可以指定输出日志的前缀,这里我们指定为【UserCenter】,然后就可以看到日志的打印输出已经清晰的标记出我们的这些日志是属于哪些业务的啦。

【UserCenter】2017/04/29 05:53:26 main.go:11: 飞雪无情的博客:http://www.flysnow.org
【UserCenter】2017/04/29 05:53:26main.go:12: 飞雪无情的微信公众号:flysnow_org

log包除了有Print系列的函数,还有Fatal以及Panic系列的函数,其中Fatal表示程序遇到了致命的错误,需要退出,这时候使用Fatal记录日志后,然后程序退出,也就是说Fatal相当于先调用Print打印日志,然后再调用os.Exit(1)退出程序。

同理Panic系列的函数也一样,表示先使用Print记录日志,然后调用panic()函数抛出一个恐慌,这时候除非使用recover()函数,否则程序就会打印错误堆栈信息,然后程序终止。

这里贴下这几个系列函数的源代码,更好理解。

func Println(v...interface{}) {

std.Output(2, fmt.Sprintln(v...))
}
func Fatalln(v ...interface{}) {

std.Output(2, fmt.Sprintln(v...))

os.Exit(1)
}
func Panicln(v ...interface{}) {

s := fmt.Sprintln(v...)

std.Output(2, s)   panic(s)
}

实现原理

通过上面的源代码,我们发现,日志包log的这些函数都是类似的,关键的输出日志就在于std.Output方法。

func New(out io.Writer, prefixstring, flag int) *Logger {

return &Logger{out: out,prefix: prefix, flag: flag}
}
var std = New(os.Stderr, "", LstdFlags)

从以上源代码可以看出,变量std其实是一个*Logger,通过log.New函数创建,默认输出到os.Stderr设备,前缀为空,日志抬头信息为标准抬头LstdFlags。

os.Stderr对应的是UNIX里的标准错误警告信息的输出设备,同时被作为默认的日志输出目的地。初次之外,还有标准输出设备os.Stdout以及标准输入设备os.Stdin。

var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr"))

以上就是定义的UNIX的标准的三种设备,分别用于输入、输出和警告错误信息。理解了os.Stderr,现在我们看下Logger这个结构体,日志的信息和操作,都是通过这个Logger操作的。

type Logger struct {

mu     sync.Mutex //ensures atomic writes; protects the following fields

prefix string     //prefix to write at beginning of each line

flag   int       // properties

out    io.Writer // destination for output

buf    []byte    // for accumulating text to write
}

·        字段mu是一个互斥锁,主要是是保证这个日志记录器Logger在多goroutine下也是安全的。

·        字段prefix是每一行日志的前缀。

·        字段flag是日志抬头信息。

·        字段out是日志输出的目的地,默认情况下是os.Stderr。

·        字段buf是一次日志输出文本缓冲,最终会被写到out里。

了解了结构体Logger的字段,现在就可以看下它最重要的方法Output了,这个方法会输出格式化好的日志信息。

func (l *Logger) Output(calldepth int, s string) error {
    now := time.Now() // get this early.
    var file string
    var line int
    //加锁,保证多goroutine下的安全
    l.mu.Lock()
    defer l.mu.Unlock()    
    //如果配置了获取文件和行号的话
    if l.flag&(Lshortfile|Llongfile) != 0 {        
        //因为runtime.Caller代价比较大,先不加锁
        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‘)
    }    
    //输出拼接好的缓冲buf里的日志信息到目的地
    _, err := l.out.Write(l.buf)    
    return err
}

整个代码比较简洁,为了多goroutine安全互斥锁也用上了,但是在获取调用堆栈信息的时候,又要先解锁,因为这个过程比较重。获取到文件、行号等信息后,继续加互斥锁保证安全。

后面的就比较简单了,formatHeader方法主要是格式化日志抬头信息,然后存储在buf这个缓冲中,最后再把我们自己的日志信息拼接到缓冲buf的后面,然后为一次log日志输出追加一个换行符,这样每次日志输出都是一行一行的。

有了最终的日志信息buf,然后把它写到输出的目的地out里就可以了,这是一个实现了io.Writer接口的类型,只要实现了这个接口,都可以当作输出目的地。

func (l *Logger) SetOutput(wio.Writer) {

l.mu.Lock()

defer l.mu.Unlock()

l.out = w
}

log包的SetOutput函数,可以设置输出目的地。这里稍微简单介绍下runtime.Caller,它可以获取运行时方法的调用信息。

func Caller(skip int) (pc uintptr, file string, line int, ok bool)

参数skip表示跳过栈帧数,0 表示不跳过,也就是runtime.Caller的调用者。1 的话就是再向上一层,表示调用者的调用者。

log日志包里使用的是 2 ,也就是表示我们在源代码中调用log.Print、log.Fatal和log.Panic这些函数的调用者。

以main函数调用log.Println为例,是main->log.Println->*Logger.Output->runtime.Caller这么一个方法调用栈,所以这时候,skip的值分别代表:

·        0 表示*Logger.Output中调用runtime.Caller的源代码文件和行号。

·        1 表示log.Println中调用*Logger.Output的源代码文件和行号。

·        2 表示main中调用log.Println的源代码文件和行号。

所以这也是log包里的这个skip的值为什么一直是 2 的原因。

定制自己的日志

通过上面的源码分析,我们知道日志记录的根本就在于一个日志记录器Logger,所以我们定制自己的日志,其实就是创建不同的Logger。

var (
    Info *log.Logger
    Warning *log.Logger
    Error * log.Logger)func init(){
    errFile,err:=os.OpenFile("errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666)
    if err!=nil{
        log.Fatalln("打开日志文件失败:",err)
    }

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(os.Stderr,errFile),"Error:",log.Ldate | log.Ltime | log.Lshortfile)}func main() {
    Info.Println("飞雪无情的博客:","http://www.flysnow.org")
    Warning.Printf("飞雪无情的微信公众号:%s\\n","flysnow_org")
    Error.Println("欢迎关注留言")
}

我们根据日志级别定义了三种不同的Logger,分别为Info,Warning,Error,用于不同级别日志的输出。这三种日志记录器都是使用log.New函数进行创建。

这里创建Logger的时候,Info和Warning都比较正常,Error这里采用了多个目的地输出,这里可以同时把错误日志输出到os.Stderr以及我们创建的errors.log文件中。

io.MultiWriter函数可以包装多个io.Writer为一个io.Writer,这样我们就可以达到同时对多个io.Writer输出日志的目的。

io.MultiWriter的实现也很简单,定义一个类型实现io.Writer,然后在实现的Write方法里循环调用要包装的多个Writer接口的Write方法即可。

func (t *multiWriter) Write(p []byte) (n int, err error) {
    for _, w := range t.writers {
        n, err = w.Write(p)
        if err != nil {
                    return
        } 
        if n != len(p) {
            err = ErrShortWrite            
            return
        }
    }    
    return len(p), nil
}

这里我们通过定义了多个Logger来区分不同的日志级别,使用比较麻烦,针对这种情况,可以使用第三方的log框架,也可以自定包装定义,直接通过不同级别的方法来记录不同级别的日志,还可以设置记录日志的级别等。

时间: 2024-09-30 19:56:03

Go语言之log日志的相关文章

Android学习笔记——log无法输出的解决方法和命令行查看log日志

本人邮箱:[email protected],欢迎交流讨论. 欢迎转载,转载请注明网址:http://www.cnblogs.com/JohnTsai/p/3983936.html. 知识点 1.log无法输出的解决方法 2.通过命令行(command line)查看Android调试的log日志 log无法输出的解决方法 今天调试Android程序,发现Eclipse不输出log日志了.这让我很烦恼,程序调试的log都没法看到了.Google搜索了一些资料.发现stackoverflow上有很

解决Linux下Tomcat日志目录下的catalina.log日志文件过大的问题

本文摘自:(http://blog.csdn.net/stevencn76/article/details/6246162) 分类: Java技术专区2011-03-13 12:25 5017人阅读 评论(1) 收藏 举报 tomcatlinux工具任务web 由于Tomcat在默认情况下会将没有经过配置的web应用所产生的日志输出已经其本身的日志内容都输出到这个文件中,那么随着时间的推移,这个文件的尺寸将会越来越大,当需要检查日志内容时间会导致文件难以打开,而且同时tomcat依旧在不断的向文

捕获异常、存sd卡、封装请求头体、加密map值、网络工具类、生成Json、Https协议、传log日志到服务器、app崩溃友好重启

点击打开链接,免积分下载 在集成了统计SDK(友盟统计,百度统计等)之后,有一个非常有利于测试的功能:错误分析!此功能能够将程序在运行中碰到的崩溃(runtimeException)问题反馈到服务器,帮助开发者改善产品,多适配机器.然而在公司android开发中不集成这些SDK,那应该怎么实现这样的功能呢?下面让我们来看下如何使用UncaughtExceptionHandler来捕获异常. 在Android开发中,常常会出现uncheched Exception 导致程序的crash,为了提供良

LISTENER.LOG日志大小不能超过2GB

oracle 11G  windows 2008系统  造成了监听死锁 报TNS-00505: 操作超时 最后分析 发现 LISTENER.LOG日志大小超过2GB 解决方案: cd$ORACLE_HOME/network/log lsnrctlset log_status off mvlistener.log listener.bak lsnrctlset log_status on 以下是我在gdimall2上的一个操作记录: 首先改变监听的日志记录状态: $lsnrctlset log_s

【个人使用.Net类库】(2)Log日志记录类

开发接口程序时,要保证程序稳定运行就要时刻监控接口程序发送和接收的数据,这就需要一个日志记录的类将需要的信息记录在日志文件中,便于自己维护接口程序.(Web系统也是如此,只是对应的日志实现比这个要复杂一点). 刚开始考虑的比较少,没有加入控制日志文件数量的功能.运行了一段时间,文件夹内的Log文件如下所示: 如果是这样,那运行一年不就三百多个日志文件了,想一想这太可怕了.通过查找资料,发现.Net中的FileInfo存有文件的信息(包括名称,创建时间,文件大小等),那就自己定义一个文件比较器实现

Nginx Access Log日志统计分析常用命令

Nginx Access Log日志统计分析常用命令 Nginx Access Log日志统计分析常用命令 IP相关统计 统计IP访问量 awk '{print $1}' access.log | sort -n | uniq | wc -l 查看某一时间段的IP访问量(4-5点) grep "07/Apr/2017:0[4-5]" access.log | awk '{print $1}' | sort | uniq -c| sort -nr | wc -l 查看访问最频繁的前100

Arm开发板+Qt学习之路-论can网通讯受log日志的影响

日期:2016-05-25 最近开发过程中发现一个问题,使用两个开发板进行can网通讯,按照经验来说,通讯的速度应该是很快的,项目中将接口的超时时间设置为100ms,在某种情境下,会在短时间内发送多次请求,例如连续点击多次按钮,每次点击都发送一次can数据,此时就会出现超时的报告,纠结了很长时间,排除了各种问题,一直没解决. 今天偶然的一个念头,看见工程里面有许多测试用的log日志,使用了qdebug和str::cerr两种方式打印的,看着太多太烦,就挨个注释掉了,然后,奇迹发生了,竟然不在出现

spring aop 配置切面,记录系统异常存入log日志

1.spring.xml(这里主要配置了抛出异常后的操作after-throwing) 需要注意的地方以黄色标注,主要是几个切入点bean配置 <!-- 激活自动代理功能 --> <aop:aspectj-autoproxy proxy-target-class="true"/> <!-- 系统服务组件的切面Bean --> <bean id="aspectService" class="com.test.http

Log日志打印集合类型

看到Logger可以打印json,当时就想能不能打印集合呢,现在通过学习了一些反射的东西,写了出来,仅供参考. public static <E> void list(List<E> list) { if (IS_DDEBUG) { if (list == null || list.size() == 0) { Logger.d("list为空"); } else { Field[] allFields = null; StringBuilder builder