1. 错误和异常处理

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

1.1. error 处理

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

  1. type student struct {
  2. Name string `json:"name"`
  3. ID string `json:"id"`
  4. }
  5. func strToStudent(src string) (*student, error) {
  6. var stu student
  7. err := json.Unmarshal([]byte(src), &stu)
  8. if err != nil {
  9. return nil, err
  10. }
  11. return &stu, nil
  12. }
  13. func main() {
  14. toStudent, err := strToStudent(`{"name": "ZhangSan", "id": "tx001023", "ret": "aaa".}`)
  15. if err != nil {
  16. fmt.Printf("string to student faile, err:%s\n", err)
  17. return
  18. }
  19. fmt.Println(*toStudent)
  20. }
  1. type student struct {
  2. Name string `json:"name"`
  3. ID string `json:"id"`
  4. }
  5. func NewStudent(id, name string) *student {
  6. return &student{
  7. Name: name,
  8. ID: id,
  9. }
  10. }
  11. func (s *student)Check() error {
  12. if s.ID == "" || s.Name == "" {
  13. return errors.New("invalid student information, name or id is null")
  14. }
  15. return nil
  16. }
  17. func main() {
  18. if err := NewStudent("123", "").Check(); err != nil {
  19. fmt.Printf("check student info failed, err:%s\n", err.Error())
  20. }
  21. }

1.2. 异常

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

  1. func initDB(username, password, mysqlServer, dbName string, maxConn, idle int) (db *sql.DB, err error) {
  2. mysqlInfo := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True", username, password, mysqlServer, dbName)
  3. // 只检验参数,不涉及账号密码验证
  4. if db, err = sql.Open("mysql", mysqlInfo); err != nil {
  5. return
  6. }
  7. if err = db.Ping(); err != nil { // 验证连接是否正常
  8. return
  9. }
  10. db.SetMaxOpenConns(maxConn) // 最大连接数,小于等于0表示不限制
  11. db.SetMaxIdleConns(idle) // 最大空闲连接数
  12. return
  13. }
  14. func main() {
  15. db, err := initDB("root", "123456", "127.0.0.1:3306", "test", 100, 20)
  16. if err != nil {
  17. panic(fmt.Sprintf("init database failed, err:%s", err.Error()))
  18. }
  19. _ = db.Close()
  20. }
  1. [root@duduniao go_learn]# go run day23/error/mysql.go
  2. panic: init database failed, err:dial tcp 127.0.0.1:3306: connect: connection refused
  3. goroutine 1 [running]:
  4. main.main()
  5. /mnt/e/Projects/learn/go_learn/day23/error/mysql.go:27 +0x159
  6. exit status 2
  1. func testPanic() {
  2. panic("raise error")
  3. }
  4. func run() (err error) {
  5. // 使用recover 捕获异常,避免程序崩溃
  6. defer func() {
  7. errInfo := recover()
  8. err = errors.New(fmt.Sprintf("%v", errInfo))
  9. }()
  10. testPanic()
  11. return
  12. }
  13. func main() {
  14. if err := run(); err != nil {
  15. fmt.Printf("run failed,err:%s\n", err.Error())
  16. return
  17. }
  18. }

2. 标准库log

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

2.1. 常用的方法和函数

2.1.1. 常量(设置日志的Flag)

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

2.1.2. 函数

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

2.1.3. 日志类型类型和方法

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

2.2. 简单案例

2.2.1. 直接调用log库函数

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

2.2.2. 使用Logger对象

  1. func main() {
  2. accessLogFile, _ := os.OpenFile("logs/access.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
  3. errorLogFile, _ := os.OpenFile("logs/error.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
  4. accessLog := log.New(accessLogFile, "", log.Ldate|log.Ltime|log.Lshortfile)
  5. errorLog := log.New(errorLogFile, "", log.Ldate|log.Ltime|log.Lshortfile)
  6. for {
  7. accessLog.Printf("[%s] This is access log.","INFO")
  8. errorLog.Printf("[%s] This is error log.","ERROR")
  9. time.Sleep(time.Second)
  10. }
  11. }
  1. [root@heyingsheng log_test]# head logs/*.log
  2. ==> logs/access.log <==
  3. 2020/06/02 08:35:00 main.go:16: [INFO] This is access log.
  4. 2020/06/02 08:35:01 main.go:16: [INFO] This is access log.
  5. 2020/06/02 08:35:02 main.go:16: [INFO] This is access log.
  6. 2020/06/02 08:35:03 main.go:16: [INFO] This is access log.
  7. 2020/06/02 08:35:04 main.go:16: [INFO] This is access log.
  8. ==> logs/error.log <==
  9. 2020/06/02 08:35:00 main.go:17: [ERROR] This is error log.
  10. 2020/06/02 08:35:01 main.go:17: [ERROR] This is error log.
  11. 2020/06/02 08:35:02 main.go:17: [ERROR] This is error log.
  12. 2020/06/02 08:35:03 main.go:17: [ERROR] This is error log.
  13. 2020/06/02 08:35:04 main.go:17: [ERROR] This is error log.

3. logrus

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

3.1. 常用方法类型

  1. 1. func New() *Logger
  2. 创建新的 *Logger 对象
  3. 2. type Logger struct
  4. type Logger struct {
  5. Out io.Writer // 输出位置,比如文件句柄或者标准输出
  6. Hooks LevelHooks // 钩子函数
  7. Formatter Formatter // 日志格式,默认支持 josn 和 text
  8. Level Level // 日志级别
  9. ExitFunc exitFunc // 退出函数,默认为 os.Exit()
  10. }
  11. 3. func (logger *Logger) WithField(key string, value interface{}) *Entry
  12. 添加 Key/value 到日志
  13. 4. func (logger *Logger) WithFields(fields Fields) *Entry
  14. 添加 Key/value 到日志,Fieldsmap[string]interface{} 格式
  15. 5. 日志记录函数
  16. func (logger *Logger) Tracef(format string, args ...interface{})
  17. func (logger *Logger) Debugf(format string, args ...interface{})
  18. func (logger *Logger) Infof(format string, args ...interface{})
  19. func (logger *Logger) Warningf(format string, args ...interface{})
  20. func (logger *Logger) Errorf(format string, args ...interface{})
  21. func (logger *Logger) Fatalf(format string, args ...interface{})
  22. func (logger *Logger) Panicf(format string, args ...interface{})
  23. func (logger *Logger) Trace(args ...interface{})
  24. func (logger *Logger) Debug(args ...interface{})
  25. func (logger *Logger) Info(args ...interface{})
  26. func (logger *Logger) Warning(args ...interface{})
  27. func (logger *Logger) Error(args ...interface{})
  28. func (logger *Logger) Fatal(args ...interface{})
  29. func (logger *Logger) Panic(args ...interface{})

3.2. 案例

3.2.1. 文本和json格式的日志

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

  1. func appLog() {
  2. log.WithField("action","upload").WithField("fid","abda002301").Info("send msg to kafka.")
  3. log.WithField("action","download").WithField("fid","abda002301").Warning("send msg to kafka.")
  4. log.WithFields(log.Fields{"action":"query", "fid":"abda002301"}).Error("send msg to kafka.")
  5. log.WithFields(log.Fields{"action":"query", "fid":"abda002301"}).Errorf("send to MQ:%s\n","192.168.1.233")
  6. }
  7. func main() {
  8. fmt.Println("----- 文本日志 -----")
  9. appLog()
  10. fmt.Println("------ Json -------")
  11. log.SetFormatter(&log.JSONFormatter{})
  12. appLog()
  13. }

image.png

3.2.2. 日志级别和字段

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

  1. func appLog() {
  2. log.WithFields(log.Fields{"action":"query", "fid":"abda002301"}).Debugf("send to MQ:%s\n","192.168.1.233")
  3. log.WithField("action","upload").WithField("fid","abda002301").Info("send msg to kafka.")
  4. log.WithField("action","download").WithField("fid","abda002301").Warning("send msg to kafka.")
  5. log.WithFields(log.Fields{"action":"query", "fid":"abda002301"}).Error("send msg to kafka.")
  6. }
  7. func main() {
  8. log.SetLevel(log.InfoLevel)
  9. appLog()
  10. }
  1. INFO[0000] send msg to kafka. action=upload fid=abda002301
  2. WARN[0000] send msg to kafka. action=download fid=abda002301
  3. ERRO[0000] send msg to kafka. action=query fid=abda002301

3.2.3. 文件日志

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

  1. func appLog(logger *log.Logger) {
  2. logger.WithFields(log.Fields{"action": "query", "fid": "abda002301"}).Debugf("send to MQ:%s\n", "192.168.1.233")
  3. logger.WithField("action", "upload").WithField("fid", "abda002301").Info("send msg to kafka.")
  4. logger.WithField("action", "download").WithField("fid", "abda002301").Warning("send msg to kafka.")
  5. logger.WithFields(log.Fields{"action": "query", "fid": "abda002301"}).Error("send msg to kafka.")
  6. }
  7. var (
  8. runLog = log.New()
  9. errLog = log.New()
  10. )
  11. func init() {
  12. runLogFile,_ := os.OpenFile("logs/run.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
  13. errLogFile,_ := os.OpenFile("logs/error.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
  14. runLog.SetLevel(log.InfoLevel)
  15. runLog.Out = runLogFile
  16. errLog.SetLevel(log.ErrorLevel)
  17. errLog.Out = errLogFile
  18. }
  19. func main() {
  20. appLog(runLog)
  21. appLog(errLog)
  22. }
  1. [root@heyingsheng logrus_test]# tail -n 2 logs/*
  2. ==> logs/error.log <==
  3. time="2020-06-03T08:39:55+08:00" level=error msg="send msg to kafka." action=query fid=abda002301
  4. ==> logs/run.log <==
  5. time="2020-06-03T08:39:55+08:00" level=warning msg="send msg to kafka." action=download fid=abda002301
  6. 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 的目标输出,两者能无缝集成。

  1. func testLog(logger *log.Logger) {
  2. logger.WithFields(log.Fields{"action": "query", "fid": "abda002301"}).Debugf("send to MQ:%s\n", "192.168.1.233")
  3. logger.WithField("action", "upload").WithField("fid", "abda002301").Info("send msg to kafka.")
  4. logger.WithField("action", "download").WithField("fid", "abda002301").Warning("send msg to kafka.")
  5. logger.WithFields(log.Fields{"action": "query", "fid": "abda002301"}).Error("send msg to kafka.")
  6. }
  7. func main() {
  8. runLogFile, err := rotate.New(
  9. "logs/access.log.%Y-%m-%d",
  10. rotate.WithLinkName("logs/access.log"),
  11. rotate.WithRotationTime(time.Second * 86400),
  12. rotate.WithRotationCount(7),
  13. )
  14. if err != nil {
  15. fmt.Printf("Init log handler failed, err:%v\n", err)
  16. }
  17. runLogger := log.New()
  18. runLogger.SetOutput(runLogFile)
  19. runLogger.Level = log.InfoLevel
  20. testLog(runLogger)
  21. testLog(runLogger)
  22. }
  1. [root@heyingsheng logrus_test]# ll logs/access.log*
  2. lrwxrwxrwx 1 root root 26 2020-06-03 20:51:51 logs/access.log -> logs/access.log.2020-06-03
  3. -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