由于这个问题问得比较多,因此专门为大家撰写了这一章节。

基本介绍

目前大多数Golang的第三方WebServer库均没有默认对HTTP请求处理过程中产生的异常进行捕获,轻者错误产生后无法记录到日志造成排查错困难,重则异常造成进程直接崩溃,服务不可用。

当你选择goframe,你很幸运。作为一款企业级的基础开发框架,默认情况下,执行过程中产生的panic是有被Server自动捕获的,产生panic时当前执行流程会立即中止,但是绝对不会影响进程直接崩溃。

获取异常错误

HTTP执行流程中产生panic异常时,默认处理是记录到Server的日志文件中。当然,开发者也可以通过注册中间件方式手动捕获,然后自定义相关的错误处理。这一操作其实在中间件章节的示例中也有介绍,我们这里来再仔细说明下。

相关方法

异常的捕获我们通过Request对象中的GetError方法获取。

开发者不能通过recover方法来捕获异常,因为goframe框架的Server已经做了捕获,并且为保证默认情况下异常不会引起进程崩溃,因此不会再次往上抛异常。

// GetError returns the error occurs in the procedure of the request.
// It returns nil if there's no error.
func (r *Request) GetError() error

该方法往往使用在流程控制组件中,如后置中间件或者HOOK钩子方法中。

使用示例

我们这里使用一个全局的后置中间件来捕获异常,当异常产生后,捕获并写入到指定的日志文件中,返回固定友好的提示信息,避免敏感的报错信息暴露给调用端。

需要注意的是:

  • 即使开发者有自己捕获记录异常错误的日志,但是Server依旧会打印到Server自己的错误日志文件中。由开发者调用日志接口方法输出的日志属于业务日志(与业务相关),而Server自行管理的日志属于服务日志(类似于nginxerror.log)。
  • 由于goframe框架大部分的底层错误都包含有错误时的堆栈信息,如果对于error的具体堆栈信息感兴趣(具体调用链、报错文件路径、源码行号等),可以使用gerror来获取,具体请参考 错误处理-堆栈特性 章节。如果异常包含堆栈信息,默认情况下会打印到Servererror日志文件中。
package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareErrorHandler(r *ghttp.Request) {
	r.Middleware.Next()
	if err := r.GetError(); err != nil {
		// 记录到自定义错误日志文件
		g.Log("exception").Error(err)
		//返回固定的友好信息
		r.Response.ClearBuffer()
		r.Response.Writeln("服务器居然开小差了,请稍后再试吧!")
	}
}

func main() {
	s := g.Server()
	s.Use(MiddlewareErrorHandler)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("db error: sql is xxxxxxx")
		})
	})
	s.SetPort(8199)
	s.Run()
}

执行后,我们通过curl工具来试试吧:

$ curl -v "http://127.0.0.1:8199/api.v2/user/list"
> GET /api.v2/user/list HTTP/1.1
> Host: 127.0.0.1:8199
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< Server: GF HTTP Server
< Date: Sun, 19 Jul 2020 07:44:30 GMT
< Content-Length: 52
< Content-Type: text/plain; charset=utf-8
<
服务器居然开小差了,请稍后再试吧!

获取异常堆栈

异常堆栈信息

WebServer本身在捕获异常时,如果抛出的异常信息并不包含堆栈内容,那么WebServer会自动获取抛出异常位点(即panic的位置)的堆栈并创建一个新的包含该堆栈信息的错误对象。我们来看一个示例。

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareErrorHandler(r *ghttp.Request) {
	r.Middleware.Next()
	if err := r.GetError(); err != nil {
		r.Response.ClearBuffer()
		r.Response.Writef("%+v", err)
	}
}

func main() {
	s := g.Server()
	s.Use(MiddlewareErrorHandler)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("db error: sql is xxxxxxx")
		})
	})
	s.SetPort(8199)
	s.Run()
}

可以看到,我们通过%+v的格式化打印来获取异常错误中的堆栈信息,具体原理请参考章节:错误处理-堆栈特性。执行后,我们通过curl工具来测试下:

$ curl "http://127.0.0.1:8199/api.v2/user/list"
db error: sql is xxxxxxx
1. db error: sql is xxxxxxx
   1).  main.main.func1.1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:25
   2).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.8
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:111
   3).  github.com/gogf/gf/v2/net/ghttp.niceCallFunc
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46
   4).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:110
   5).  github.com/gogf/gf/v2/util/gutil.TryCatch
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46
   6).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47
   7).  main.MiddlewareErrorHandler
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:10
   8).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.9
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:117
   9).  github.com/gogf/gf/v2/net/ghttp.niceCallFunc
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46
   10). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:116
   11). github.com/gogf/gf/v2/util/gutil.TryCatch
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46
   12). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47
   13). github.com/gogf/gf/v2/net/ghttp.(*Server).ServeHTTP
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_server_handler.go:122

错误堆栈信息

如果抛出的异常是一个通过gerror组件的错误对象,或者实现堆栈打印接口的错误对象,由于该异常的错误对象已经包含了详细的堆栈信息,那么WebServer将会直接返回该错误对象,不会自动创建错误对象。我们来看一个示例。

package main

import (
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareErrorHandler(r *ghttp.Request) {
	r.Middleware.Next()
	if err := r.GetError(); err != nil {
		r.Response.ClearBuffer()
		r.Response.Writef("%+v", err)
	}
}

func DbOperation() error {
	// ...
	return gerror.New("DbOperation error: sql is xxxxxxx")
}

func UpdateData() {
	err := DbOperation()
	if err != nil {
		panic(gerror.Wrap(err, "UpdateData error"))
	}
}

func main() {
	s := g.Server()
	s.Use(MiddlewareErrorHandler)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.ALL("/user/list", func(r *ghttp.Request) {
			UpdateData()
		})
	})
	s.SetPort(8199)
	s.Run()
}

执行后,我们通过curl工具来测试下:

$ curl "http://127.0.0.1:8199/api.v2/user/list"
UpdateData error: DbOperation error: sql is xxxxxxx
1. UpdateData error
   1).  main.UpdateData
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:25
   2).  main.main.func1.1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:34
   3).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.8
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:111
   4).  github.com/gogf/gf/v2/net/ghttp.niceCallFunc
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46
   5).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:110
   6).  github.com/gogf/gf/v2/util/gutil.TryCatch
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46
   7).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47
   8).  main.MiddlewareErrorHandler
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:10
   9).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.9
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:117
   10). github.com/gogf/gf/v2/net/ghttp.niceCallFunc
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46
   11). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:116
   12). github.com/gogf/gf/v2/util/gutil.TryCatch
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46
   13). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47
   14). github.com/gogf/gf/v2/net/ghttp.(*Server).ServeHTTP
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_server_handler.go:122
2. DbOperation error: sql is xxxxxxx
   1).  main.DbOperation
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:19
   2).  main.UpdateData
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:23
   3).  main.main.func1.1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:34
   4).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.8
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:111
   5).  github.com/gogf/gf/v2/net/ghttp.niceCallFunc
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46
   6).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:110
   7).  github.com/gogf/gf/v2/util/gutil.TryCatch
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46
   8).  github.com/gogf/gf/v2/net/ghttp.(*middleware).Next
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47
   9).  main.MiddlewareErrorHandler
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:10
   10). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.9
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:117
   11). github.com/gogf/gf/v2/net/ghttp.niceCallFunc
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46
   12). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:116
   13). github.com/gogf/gf/v2/util/gutil.TryCatch
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46
   14). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47
   15). github.com/gogf/gf/v2/net/ghttp.(*Server).ServeHTTP
    	/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_server_handler.go:122








Content Menu

  • No labels

10 Comments

  1. 喜欢这个异常处理方式(thumbs up)

  2. 强哥,如果是整个应用 非请求响应的崩溃信息,如果能捕获到?

    1. 会自动捕获当前goroutinepanic

  3. 强哥,我遇到一个问题,当我提前把gerror对象定义好之后,再panic这个变量的时候,堆栈信息就只有我定义这个错误变量的位置,而没有panic的位置,请问要怎么解决

    1. 你定义的error如果是通过panic抛出来,那么久需要捕获后通过%+v的格式化打印才能看得到。示例中的panic+error完整堆栈信息是Server自动捕获后日通过%+v日志打印出来的。

  4. 强哥,如果在逻辑里子携程里有pannic 需要写recover么

  5. 真多真丰富, 感觉学了gf就算了入门了go. 基本面面俱到

  6. zxl

    强哥,现在想集成sentry,在MiddlewareErrorHandler中间件处理中 err != nil 时,使用sentry.CaptureException(err)发送异常到sentry,带的堆栈信息是MiddlewareErrorHandler发送异常的地方,如何在发送sentry信息时带上真正抛出异常地方的堆栈信息呢?代码如下

    // 测试sentry消息发送
    func (s *sTest) SentryTest(ctx context.Context) error {
        panic("middle-ddddddfffffgggg")

        return nil
    }

    // 返回处理中间件
    func (s *sMiddleware) ResponseHandler(r *ghttp.Request) {
           r.Middleware.Next()

          if err != nil {
              g.Dump("进入中间件 异常捕获")

              // 这里带的堆栈信息是ResponseHandler的,但是我需要的堆栈信息是SentryTest方法的
              sentry.CaptureException(err)
          }

    }

    1. zxl

      郭强强哥麻烦帮我看一下,谢谢啦

  7. MySQL事务处理与异常处理
    // 开启事务
    	db := g.DB()
    	if tx, err := db.Begin(ctx); err == nil {
    		userId, err := service.User().Create(ctx, UserEntity)
    		if err != nil {
    			tx.Rollback()
    			return nil, gerror.NewCodef(gcode.CodeUnknown, "failed to create user: %v", err)
    		}
    
    		UserRoleEntity := entity.UserRoles{
    			UserId: userId,
    			RoleId: req.RoleId,
    		}
    		userRoleId, err := service.UserRoles().Create(ctx, UserRoleEntity)
            // 比如我在这里测试主动抛出异常,这个时候直接抛出了,下面的回滚也不会生效了,怎么处理啊?感觉还是python的try...except ...好用,golang这个怎么使用才好啊?
            panic("test")
    		if err != nil {
    			tx.Rollback()
    			return nil, gerror.NewCodef(gcode.CodeUnknown, "failed to create userRole: %v", err)
    		}
    		tx.Commit()
    		res = &v1.CreateUserRes{
    			Result: map[string]interface{}{
    				"id":           userId,
    				"user_role_id": userRoleId,
    				"user":         UserEntity,
    			},
    		}
    	} else {
    		tx.Rollback()
    		g.Log().Error(ctx, "开启事务失败")
    		return nil, gerror.NewCodef(gcode.CodeUnknown, "新建用户失败: %v", err)
    	}