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()
}
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