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 studenterr := 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.gopanic: init database failed, err:dial tcp 127.0.0.1:3306: connect: connection refusedgoroutine 1 [running]:main.main()/mnt/e/Projects/learn/go_learn/day23/error/mysql.go:27 +0x159exit 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/23Ltime // the time in the local time zone: 01:23:23Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.Llongfile // full file name and line number: /a/b/c/d.go:23Lshortfile // final file name element and line number: d.go:23. overrides LlongfileLUTC // if Ldate or Ltime is set, use UTC rather than the local time zoneLmsgprefix // move the "prefix" from the beginning of the line to before the messageLstdFlags = 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. paincfunc (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.go2020/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 structtype Logger struct {Out io.Writer // 输出位置,比如文件句柄或者标准输出Hooks LevelHooks // 钩子函数Formatter Formatter // 日志格式,默认支持 josn 和 textLevel 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()}
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=abda002301WARN[0000] send msg to kafka. action=download fid=abda002301ERRO[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 = runLogFileerrLog.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=abda002301time="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.InfoLeveltestLog(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
