Golang | Fatal 和 panic 的区别

488 阅读10分钟

在使用 Go 语言开发程序时,我们可能都会注意到,log.Fatal()panic() 都可以让程序退出。但是它们之间的差异是什么?或者说,我们什么时候应该使用 log.Fatal(),什么时候应该使用 panic() 呢?

log.Fatal

log.Fatal() 方法在 log 包下,源代码如下:

// Fatal is equivalent to [Print] followed by a call to [os.Exit](1).
func Fatal(v ...any) {
    std.Output(2, fmt.Sprint(v...))
    os.Exit(1)
}

Fatal 等效于 [Print],后跟对 [os.Exit](1) 的调用。那么重点在于 os.Exit 方法中,程序是如何响应和退出的。

// Exit causes the current program to exit with the given status code.
// Conventionally, code zero indicates success, non-zero an error.
// The program terminates immediately; deferred functions are not run.
//
// For portability, the status code should be in the range [0125].
func Exit(code int) {
    if code == 0 && testlog.PanicOnExit0() {
        // We were told to panic on calls to os.Exit(0).
        // This is used to fail tests that make an early
        // unexpected call to os.Exit(0).
        panic("unexpected call to os.Exit(0) during test")
    }

    // Inform the runtime that os.Exit is being called. If -race is
    // enabled, this will give race detector a chance to fail the
    // program (racy programs do not have the right to finish
    // successfully). If coverage is enabled, then this call will
    // enable us to write out a coverage data file.
    runtime_beforeExit(code)

    syscall.Exit(code)
}

Exit 使当前程序以给定的状态代码退出。传统上,代码零表示成功,非零表示错误。程序立即终止;defer 方法不运行。我们需要关注的有两点:① 应用程序立马退出;② defer 函数不会执行。那么,总结 log.Fatal 函数就是:

  1. 打印输出内容;
  2. 应用程序立马退出;
  3. defer 函数不会执行;

panic

panic 函数在 builtin.go 文件中,属于 Go 语言预先声明的标识符。这里记录的项目实际上并不包含在内置包中,但是这里的描述允许 godoc 为语言的特殊标识符提供文档。

// The panic built-in function stops normal execution of the current
// goroutine. When a function F calls panic, normal execution of F stops
// immediately. Any functions whose execution was deferred by F are run in
// the usual way, and then F returns to its caller. To the caller G, the
// invocation of F then behaves like a call to panic, terminating G's
// execution and running any deferred functions. This continues until all
// functions in the executing goroutine have stopped, in reverse order. At
// that point, the program is terminated with a non-zero exit code. This
// termination sequence is called panicking and can be controlled by the
// built-in function recover.
//
// Starting in Go 1.21, calling panic with a nil interface value or an
// untyped nil causes a run-time error (a different panic).
// The GODEBUG setting panicnil=1 disables the run-time error.
func panic(v any)

panic 是内置函数,会停止当前 goroutine 的正常执行。当函数 F 调用 panic 时,会立即停止 F 的正常执行。F 中任何 defer 函数都会以正常运行,然后返回给 F 的调用者。对于调用者 G,对 F 的调用就像对 panic 的调用一样,终止 G 的执行并运行任何 defer 函数。这种机制会一直持续,直到正在执行的 goroutine 中的所有函数都以相反的顺序停止。此时,程序以非零退出代码终止。此终止序列称为panicking,可以通过内置函数 recover 进行控制。

从 Go 1.21 开始,使用 nil 接口值或无类型的 nil 调用 panic 会导致运行时错误 (不同的 panic )。可以通过 GODEBUG 设置 panicnil = 1 禁用传输 nil 导致 panic 在运行时的错误。

注意几点:

  1. 函数立即停止执行,而不是应用程序;
  2. defer 函数正常执行;
  3. 返回给调用者 caller;
  4. 调用者 caller 也等同于调用了 panic,defer 函数也会执行,返回给 caller 的调用者;
  5. 递归重复第 ④ 步,直到最上层函数;
  6. 若中途有 recover 函数捕获,可以正常执行;若未被捕获,应用程序将停止。

从这一点来看,panic 的行为类似于 Java 中的 Exception,如果没有捕获 Exception,那么就会导致应用程序退出(在 Java 中是当前线程退出)。

验证过程

package main

import "log"

func fatal() {
    defer func() {
        log.Print("3333")
    }()
    log.Fatal("4444")
}

func main() {
    log.Print("1111")
    defer func() { log.Print("2222") }()
    fatal()
    log.Print("9999")
}

// 2024/07/21 21:54:12 1111
// 2024/07/21 21:54:12 4444
//
// Process finished with the exit code 1

从运行结果可以看出,程序是异常退出的。且两个函数中的 defer 函数也并没有执行。

package main

import "log"

func localPanic() {
    defer func() {
        log.Print("3333")
    }()
    panic("4444")
}

func main() {
    log.Print("1111")
    defer func() { log.Print("2222") }()
    localPanic()
    log.Print("9999")
}

// 2024/07/21 21:55:20 1111
// 2024/07/21 21:55:20 3333
// 2024/07/21 21:55:20 2222
// panic: 4444
// 
// goroutine 1 [running]:
// main.localPanic()
//        C:/xxx/main.go:9 +0x3e
// main.main()
//        C:/xxx/main.go:15 +0x9e
//
// Process finished with the exit code 2

从测试程序中,可以看出 panic 执行后,defer 函数被正常执行。从 exit code 也可以看出,程序属于非正常退出。

web 框架的处理

Spring MVC

Java 中最常用的框架是 Spring,与之同时使用的 web 框架是 Spring MVC。

Spring MVC 框架设计时考虑到了异常处理的灵活性和鲁棒性,它并不会因为一个Exception未被捕获而在整个应用程序层面引发崩溃。相反,Spring MVC 提供了一种机制来优雅地处理异常,确保只有引发异常的请求受到影响,而其他请求和整个应用程序可以继续正常运行。

异常处理器(Exception Handler)

Spring MVC 支持异常处理器,允许开发者定义特定的控制器方法来专门处理异常。这些方法通常通过@ControllerAdvice注解标记,其中可以包含@ExceptionHandler注解来指定处理哪种类型的异常。当一个控制器方法抛出异常时,Spring会查找与之匹配的异常处理器并调用它。

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = { IllegalArgumentException.class })
    public ResponseEntity<ObjecthandleIllegalArgumentException(IllegalArgumentException ex) {
        return new ResponseEntity<>(new ErrorMessage("Invalid argument"), HttpStatus.BAD_REQUEST);
    }
}

在这个例子中,当任何控制器抛出IllegalArgumentException时,上述方法将被调用来处理异常,返回一个适当的HTTP响应给客户端,而不是让整个应用程序崩溃。

默认的异常处理

即使没有自定义异常处理器,Spring MVC 也有默认的异常处理机制。它会生成一个错误页面或错误响应,通常包含 HTTP 状态码和简单的错误消息,这取决于应用的配置。

Spring MVC 框架设计的核心理念之一就是保证应用上下文的稳定性和健壮性。这意味着即使单个请求处理失败,也不会影响到其他请求或系统的整体健康。这通过隔离每个请求的处理上下文以及使用线程池来运行请求处理任务得以实现。

Gin

Go 语言中的 Gin 框架,在设计时同样遵循了处理异常时的优雅降级原则,不会因为单一请求的错误处理而使整个应用程序崩溃。Gin 框架提供了几种机制来处理请求中的异常,确保应用程序的稳定性和健壮性。

错误处理中间件

Gin 提供了一个内置的恢复中间件,可以通过调用 gin.Recovery() 来启用。这个中间件会在请求处理过程中捕获任何 panic,并将其转换为 HTTP 错误响应。这样,即使某个请求处理函数中出现了未处理的 panic,Gin 也会捕获它并发送错误响应给客户端,而不会导致整个程序崩溃。

router := gin.Default()
router.Use(gin.Recovery())

router.GET("/"func(c *gin.Context) {
    // 请求处理逻辑
})

// 启动服务器
router.Run(":8080")

在使用 gin.Default 函数构建 GinEngine 时,已经使用了默认提供的错误处理中间件。

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

在这个示例中,如果 / 路由的处理函数中出现 panic,Gin 的恢复中间件会捕获它,并且发送一个错误响应给客户端,同时记录错误信息。

自定义错误处理

除了内置的恢复中间件,Gin 还允许开发者自定义错误处理函数,以便在发生错误时执行特定的逻辑。我们可以定义自己的中间件来处理特定类型的错误,或者使用 Gin 提供的 c.Error() 函数来创建错误响应。

router.GET("/"func(c *gin.Context) {
    defer func() {
        if r := recover(); r != nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError,
                gin.H{"error""An unexpected error occurred"},
            )
        }
    }()

    // 请求处理逻辑
})

在这个程序中,deferrecover 语句用于捕获可能发生的 panic,并在发生时终止当前请求处理并发送错误响应。

Gin 框架的设计目标之一是在遇到问题时能够优雅地处理错误,而不会导致整个应用程序退出。通过内置的恢复中间件和自定义错误处理逻辑,Gin 能够确保即使在处理请求时遇到错误,其他请求和应用程序的其余部分也能继续正常运行。这种设计对于生产环境中的高可用性和稳定性至关重要。

不处理错误

Gin 框架从设计上,给我们提供了错误处理的方式,但是如果不处理错误,会发生什么呢?

为了避免使用自带的 gin.Recovery() 中间件,我们使用 gin.New() 来新建 engine 对象。

engine := gin.New()
engine.GET("/hello"func(context *gin.Context) {
    panic("test panic")
    context.JSON(200, gin.H{
        "message""hello world",
    })
})

// 直接运行 engine 服务
err := engine.Run()

启动之后,访问界面会提示无法访问,服务端输出:

[GIN-debug] Listening and serving HTTP on :8080
2024/07/21 21:52:22 http: panic serving [::1]:5461: test panic
goroutine 6 [running]:
net/http.(*conn).serve.func1()
        C:/Program Files/Go/src/net/http/server.go:1898 +0xbe
panic({0x377d80?, 0x4a2d70?})
        C:/Program Files/Go/src/runtime/panic.go:770 +0x132
main.main.func1(0xc0000a0100?)
        C:/Users/Real/GoProjects/go_study/gin_framework/quick_start/main.go:12 +0x25
github.com/gin-gonic/gin.(*Context).Next(...)

同时我们再访问其他 path 时,是可以正常访问的。证明 Gin 框架对于不同的请求处理,是做了隔离与默认的错误处理的。

如果在处理请求的过程中发生错误或 panic,而没有使用如 gin.Recovery() 这样的中间件,那么处理该请求的 Goroutine 会终止,但不会影响其他正在运行的 Goroutines。Gin 框架的设计确保了即使在异常情况下,服务器的稳定性也不会受到严重影响。这里是 Gin 框架利用 Go 语言的 Goroutines 和 channel 机制来实现高效的并发处理,从而能够隔离请求处理。

engine := gin.New()
engine.GET("/hello"func(context *gin.Context) {
    log.Fatal("test panic")
    context.JSON(200, gin.H{
        "message""hello world",
    })
})

engine.GET("/world"func(context *gin.Context) {
    context.JSON(200, gin.H{
        "message""hello world",
    })
})

如果是在接收请求时触发 log.Fatal(),那么程序将直接退出。

[GIN-debug] GET    /hello                    --> main.main.func1 (1 handlers)
[GIN-debug] GET    /world                    --> main.main.func2 (1 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
2024/07/21 22:09:21 test panic

因此,在实际开发中,如果希望程序停止,那么使用 log.Fatal。而 web 服务程序一般不会有这种需求,且成熟的框架中,通常会做好请求隔离与错误处理,保证服务程序的健壮性。因此 web 开发中,一般使用 panic 就好了。

总结

  • 在 Go 语言中,panic 的退出有特殊的 pranking 序列,可以被 recovery 捕获,defer 函数也会执行;
  • log.Fatal 由 Print 和 os.Exit 组成,会直接退出,不可被捕获处理,defer 函数不会执行;
  • 在 Gin 框架中,可以处理 panic,但不能处理 log.Fatal,线上需要谨慎使用。