Golang学习笔记(09-8-log模块)

271 阅读5分钟

1. 错误和异常处理

在Golang 中,错误和异常并不是一个概念。如果程序运行失败是可预期的那几种情况,一般额外返回一个 error,通常命名为 err,比如更新数据库记录失败。如果程序运行失败仅一种情况,一般额外返回一个布尔值,通常命名为 ok ,比如从map中根据key取value。如果程序运行中出现了预期之外的结果,比如bug或者其它不可控问题,那么通常称为异常,使用 panic 抛出,并用 recover 在内部处理。

1.1. error 处理

一般在Go语言编程中,两种处理错误的方式,一种是直接将底层的 error 返回,另一种是通过errors.New()对当前的错误进行封装。返回错误的时候,需要注意,没有错误直接返回 nil !

type student struct {
	Name string `json:"name"`
	ID   string `json:"id"`
}

func strToStudent(src string) (*student, error) {
	var stu student
	err := json.Unmarshal([]byte(src), &stu)
	if err != nil {
		return nil, err
	}
	return &stu, nil
}

func main() {
	toStudent, err := strToStudent(`{"name": "ZhangSan", "id": "tx001023", "ret": "aaa".}`)
	if err != nil {
		fmt.Printf("string to student faile, err:%s\n", err)
		return
	}
	fmt.Println(*toStudent)
}
type student struct {
	Name string `json:"name"`
	ID   string `json:"id"`
}

func NewStudent(id, name string) *student {
	return &student{
		Name: name,
		ID: id,
	}
}

func (s *student)Check() error {
	if s.ID == "" || s.Name == "" {
		return errors.New("invalid student information, name or id is null")
	}
	return nil
}

func main() {
	if err := NewStudent("123", "").Check(); err != nil {
		fmt.Printf("check student info failed, err:%s\n", err.Error())
	}
}

1.2. 异常

Golang 通过panic 抛出异常,在写代码中常见的异常是空指针异常和死锁异常,这两种会导致程序直接崩溃。当然用户也可以通过 panic() 自定义异常,比如当程序初始化mysql连接池失败时,程序没有执行的必要了,此时直接使用panic()抛出异常。
但是部分情况下,在引入第三方库处理数据时,该库可能存在bug或者其它异常,我们可能并不希望程序直接崩溃掉,此时需要使用 recover() 捕获异常,防止程序崩溃。 recover() 必须在 defer 语句中直接调用!

func initDB(username, password, mysqlServer, dbName string, maxConn, idle int) (db *sql.DB, err error) {
	mysqlInfo := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True", username, password, mysqlServer, dbName)
	// 只检验参数,不涉及账号密码验证
	if db, err = sql.Open("mysql", mysqlInfo); err != nil {
		return
	}
	if err = db.Ping(); err != nil { // 验证连接是否正常
		return
	}

	db.SetMaxOpenConns(maxConn) // 最大连接数,小于等于0表示不限制
	db.SetMaxIdleConns(idle)    // 最大空闲连接数
	return
}

func main() {
	db, err := initDB("root", "123456", "127.0.0.1:3306", "test", 100, 20)
	if err != nil {
		panic(fmt.Sprintf("init database failed, err:%s", err.Error()))
	}
	_ = db.Close()
}
[root@duduniao go_learn]# go run day23/error/mysql.go
panic: init database failed, err:dial tcp 127.0.0.1:3306: connect: connection refused

goroutine 1 [running]:
main.main()
        /mnt/e/Projects/learn/go_learn/day23/error/mysql.go:27 +0x159
exit status 2
func testPanic()  {
	panic("raise error")
}

func run() (err error) {
	// 使用recover 捕获异常,避免程序崩溃
	defer func() {
		errInfo := recover()
		err = errors.New(fmt.Sprintf("%v", errInfo))
	}()
	testPanic()
	return
}

func main()  {
	if err := run(); err != nil {
		fmt.Printf("run failed,err:%s\n", err.Error())
		return
	}
}

2. 标准库log

Go语言自带了标准库Log,但是功能比较简单和鸡肋,比如不支持日志级别 Debug,Info,Error... ,也不支持Json格式日志和日志轮转切割,一般仅在一些简单的小项目中使用。或者在项目中,对该模块进行封装,创建适合自己项目的日志库。

2.1. 常用的方法和函数

2.1.1. 常量(设置日志的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
)

2.1.2. 函数

1.	func SetFlags(flag int)
		设置日志的标志位
    
2.	func SetOutput(w io.Writer)
		设置日志输出位置
    
3.	func SetPrefix(prefix string)
		设置日志前缀
1.	普通日志输出
		func Print(v ...interface{})
		func Printf(format string, v ...interface{})
    func Println(v ...interface{})
    
2.	异常日志,程序会记录日志后退出
		func Fatal(v ...interface{})
    func Fatalf(format string, v ...interface{})
    func Fatalln(v ...interface{})
    
3.	崩溃日志,程序记录日志后触发 panic()
		func Panic(v ...interface{})
    func Panicf(format string, v ...interface{})
    func Panicln(v ...interface{})

2.1.3. 日志类型类型和方法

1.	type Logger {}
		日志对象的结构体
    
2.	func New(out io.Writer, prefix string, flag int) *Logger
		日志对象的构造函数
    
3.	print方法
		func (l *Logger) Print(v ...interface{})
    func (l *Logger) Printf(format string, v ...interface{})
    func (l *Logger) Println(v ...interface{})
    
4.	fatal方法
		func (l *Logger) Fatal(v ...interface{})
    func (l *Logger) Fatalf(format string, v ...interface{})
    func (l *Logger) Fatalln(v ...interface{})
    
5.	painc
		func (l *Logger) Panic(v ...interface{})
    func (l *Logger) Panicf(format string, v ...interface{})
    func (l *Logger) Panicln(v ...interface{})

2.2. 简单案例

2.2.1. 直接调用log库函数

func main() {
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
	log.Printf("这是Print日志")
	log.Fatalf("[%d]这是fatal日志", 100)
}
[root@heyingsheng log_test]# go run main.go
2020/06/02 08:26:16 main.go:7: 这是Print日志
2020/06/02 08:26:16 main.go:8: [100]这是fatal日志
exit status 1

2.2.2. 使用Logger对象

func main() {
	accessLogFile, _ := os.OpenFile("logs/access.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	errorLogFile, _ := os.OpenFile("logs/error.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	accessLog := log.New(accessLogFile, "", log.Ldate|log.Ltime|log.Lshortfile)
	errorLog := log.New(errorLogFile, "", log.Ldate|log.Ltime|log.Lshortfile)

	for {
		accessLog.Printf("[%s] This is access log.","INFO")
		errorLog.Printf("[%s] This is error log.","ERROR")
		time.Sleep(time.Second)
	}
}
[root@heyingsheng log_test]# head logs/*.log
==> logs/access.log <==
2020/06/02 08:35:00 main.go:16: [INFO] This is access log.
2020/06/02 08:35:01 main.go:16: [INFO] This is access log.
2020/06/02 08:35:02 main.go:16: [INFO] This is access log.
2020/06/02 08:35:03 main.go:16: [INFO] This is access log.
2020/06/02 08:35:04 main.go:16: [INFO] This is access log.

==> logs/error.log <==
2020/06/02 08:35:00 main.go:17: [ERROR] This is error log.
2020/06/02 08:35:01 main.go:17: [ERROR] This is error log.
2020/06/02 08:35:02 main.go:17: [ERROR] This is error log.
2020/06/02 08:35:03 main.go:17: [ERROR] This is error log.
2020/06/02 08:35:04 main.go:17: [ERROR] This is error log.

3. logrus

标准库中的日志功能很弱,并不适合实际项目场景,logrus 支持日志级别、支持不同的输出方式、可以将日志直接发送到logstash等等。

3.1. 常用方法类型

1.  func New() *Logger
    创建新的 *Logger 对象
    
2.  type Logger struct 
    type Logger struct {
        Out         io.Writer       // 输出位置,比如文件句柄或者标准输出
        Hooks       LevelHooks      // 钩子函数
        Formatter   Formatter       // 日志格式,默认支持 josn 和 text
        Level       Level           // 日志级别
        ExitFunc    exitFunc        // 退出函数,默认为 os.Exit()
    }

3.  func (logger *Logger) WithField(key string, value interface{}) *Entry
    添加 Key/value 到日志
    
4.  func (logger *Logger) WithFields(fields Fields) *Entry
    添加 Key/value 到日志,Fields是map[string]interface{} 格式

5.  日志记录函数
    func (logger *Logger) Tracef(format string, args ...interface{})
    func (logger *Logger) Debugf(format string, args ...interface{}) 
    func (logger *Logger) Infof(format string, args ...interface{})
    func (logger *Logger) Warningf(format string, args ...interface{})
    func (logger *Logger) Errorf(format string, args ...interface{})
    func (logger *Logger) Fatalf(format string, args ...interface{})
    func (logger *Logger) Panicf(format string, args ...interface{})
    
    func (logger *Logger) Trace(args ...interface{})
    func (logger *Logger) Debug(args ...interface{}) 
    func (logger *Logger) Info(args ...interface{})
    func (logger *Logger) Warning(args ...interface{})
    func (logger *Logger) Error(args ...interface{})
    func (logger *Logger) Fatal(args ...interface{})
    func (logger *Logger) Panic(args ...interface{})

3.2. 案例

3.2.1. 文本和json格式的日志

在ELK日志采集中,如果想要从日志源头对字段进行拆分,建立索引,可以使用json格式的日志,反之则可以使用普通文本类型日志。文本日志在TTY终端会使用色彩管理,更加方便区分日志。
logrus内置两种日志格式:json和text。可通过插件实现更多类型的的格式,如:Fluent,logstash等,→坐标

func appLog()  {
	log.WithField("action","upload").WithField("fid","abda002301").Info("send msg to kafka.")
	log.WithField("action","download").WithField("fid","abda002301").Warning("send msg to kafka.")
	log.WithFields(log.Fields{"action":"query", "fid":"abda002301"}).Error("send msg to kafka.")
	log.WithFields(log.Fields{"action":"query", "fid":"abda002301"}).Errorf("send to MQ:%s\n","192.168.1.233")
}

func main()  {
	fmt.Println("----- 文本日志 -----")
	appLog()
	fmt.Println("------ Json -------")
	log.SetFormatter(&log.JSONFormatter{})
	appLog()
}

image.png

3.2.2. 日志级别和字段

一般在开发环境和生产环境需要采用不同的日志级别,logrus 设置日志级别非常方便。

func appLog()  {
	log.WithFields(log.Fields{"action":"query", "fid":"abda002301"}).Debugf("send to MQ:%s\n","192.168.1.233")
	log.WithField("action","upload").WithField("fid","abda002301").Info("send msg to kafka.")
	log.WithField("action","download").WithField("fid","abda002301").Warning("send msg to kafka.")
	log.WithFields(log.Fields{"action":"query", "fid":"abda002301"}).Error("send msg to kafka.")
}

func main()  {
	log.SetLevel(log.InfoLevel)
	appLog()
}
INFO[0000] send msg to kafka.                            action=upload fid=abda002301
WARN[0000] send msg to kafka.                            action=download fid=abda002301
ERRO[0000] send msg to kafka.                            action=query fid=abda002301

3.2.3. 文件日志

在企业开发中,容器应用中,日志可能写到标准输出,但是在传统应用或者日志分类的场景中,需要将日志写入不同的文件,比如error日志记录错误信息,run日志记录运行信息等。

func appLog(logger *log.Logger) {
	logger.WithFields(log.Fields{"action": "query", "fid": "abda002301"}).Debugf("send to MQ:%s\n", "192.168.1.233")
	logger.WithField("action", "upload").WithField("fid", "abda002301").Info("send msg to kafka.")
	logger.WithField("action", "download").WithField("fid", "abda002301").Warning("send msg to kafka.")
	logger.WithFields(log.Fields{"action": "query", "fid": "abda002301"}).Error("send msg to kafka.")
}

var (
	runLog = log.New()
	errLog = log.New()
)

func init() {
	runLogFile,_ := os.OpenFile("logs/run.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	errLogFile,_ := os.OpenFile("logs/error.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	runLog.SetLevel(log.InfoLevel)
	runLog.Out = runLogFile
	errLog.SetLevel(log.ErrorLevel)
	errLog.Out = errLogFile
}

func main() {
	appLog(runLog)
	appLog(errLog)
}
[root@heyingsheng logrus_test]# tail -n 2 logs/*
==> logs/error.log <==
time="2020-06-03T08:39:55+08:00" level=error msg="send msg to kafka." action=query fid=abda002301

==> logs/run.log <==
time="2020-06-03T08:39:55+08:00" level=warning msg="send msg to kafka." action=download fid=abda002301
time="2020-06-03T08:39:55+08:00" level=error msg="send msg to kafka." action=query fid=abda002301

3.2.4. 日志切割

logrus 本身不支持日志轮转切割功能,需要配合 file-rotatelogs 包来实现,防止日志打满磁盘。file-rotatelogs 实现了 io.Writer 接口,并且提供了文件的切割功能,其实例可以作为 logrus 的目标输出,两者能无缝集成。

func testLog(logger *log.Logger) {
	logger.WithFields(log.Fields{"action": "query", "fid": "abda002301"}).Debugf("send to MQ:%s\n", "192.168.1.233")
	logger.WithField("action", "upload").WithField("fid", "abda002301").Info("send msg to kafka.")
	logger.WithField("action", "download").WithField("fid", "abda002301").Warning("send msg to kafka.")
	logger.WithFields(log.Fields{"action": "query", "fid": "abda002301"}).Error("send msg to kafka.")
}


func main()  {
	runLogFile, err := rotate.New(
		"logs/access.log.%Y-%m-%d",
		rotate.WithLinkName("logs/access.log"),
		rotate.WithRotationTime(time.Second * 86400),
		rotate.WithRotationCount(7),
	)
	if err != nil {
		fmt.Printf("Init log handler failed, err:%v\n", err)
	}
	runLogger := log.New()
	runLogger.SetOutput(runLogFile)
	runLogger.Level = log.InfoLevel
	testLog(runLogger)
	testLog(runLogger)
}
[root@heyingsheng logrus_test]# ll logs/access.log*
lrwxrwxrwx 1 root root  26 2020-06-03 20:51:51 logs/access.log -> logs/access.log.2020-06-03
-rw-r--r-- 1 root root 598 2020-06-03 20:51:51 logs/access.log.2020-06-03

3.2.5. 线程安全

默认情况下,logrus 是收到了互斥锁保护的,可用使用logger.SetNoLock()  关闭互斥锁。在以下场景中可关闭:

  • 没有注册 Hooks ,或者 Hooks 中已经设置了线程安全。
  • logger.Out已经是线程安全的:
    • logger.Out受锁保护
    • logger.Out是使用O_APPEND标志打开的os.File处理程序,每次写入都小于4k