在 golang 中捕获 panic()

Capturing panic() in golang

我们有一个大型 golang 应用程序,它使用记录器(实际上是自定义记录器)将输出写入定期轮换的日志文件。

但是,当应用程序崩溃或出现 panic() 时,这些消息会转为标准错误。

有什么方法可以覆盖恐慌功能以使用我们的记录器吗?

您可以使用 recover() 从同一个 goroutine 中恢复恐慌。在延迟方法中调用 recover() 时(请记住延迟方法仍将被调用,即使在 panic()ing 时),它将 return 传递给最后一个 panic() 调用的任何内容作为参数(或 nil 当程序没有崩溃时)。

defer func() {
    if x := recover(); x != nil {
        // recovering from a panic; x contains whatever was passed to panic()
        log.Printf("run time panic: %v", x)

        // if you just want to log the panic, panic again
        panic(x)
    }
}()

panic("foo");

但是请注意,您无法从在不同的 goroutine 中触发的恐慌中恢复(感谢 JimB 的提示)。使用单个 recover() 从任何 goroutine 的恐慌中恢复是不可能的。

据我所知,您无法将 panic 的输出从标准错误重定向到您的记录器。您可以做的最好的事情是将标准错误重定向到一个文件,您可以在外部或在程序内部执行此操作。

对于我的 rclone 程序,我重定向了标准错误以将所有内容捕获到一个选项上的文件中,不幸的是,这在跨平台方式中并不是特别容易做到。这是我的做法(参见 redirect*.go 文件)

对于linux/unix

// Log the panic under unix to the log file

//+build unix

package main

import (
    "log"
    "os"
    "syscall"
)

// redirectStderr to the file passed in
func redirectStderr(f *os.File) {
    err := syscall.Dup2(int(f.Fd()), int(os.Stderr.Fd()))
    if err != nil {
        log.Fatalf("Failed to redirect stderr to file: %v", err)
    }
}

和 windows

// Log the panic under windows to the log file
//
// Code from minix, via
//
// http://play.golang.org/p/kLtct7lSUg

//+build windows

package main

import (
    "log"
    "os"
    "syscall"
)

var (
    kernel32         = syscall.MustLoadDLL("kernel32.dll")
    procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
)

func setStdHandle(stdhandle int32, handle syscall.Handle) error {
    r0, _, e1 := syscall.Syscall(procSetStdHandle.Addr(), 2, uintptr(stdhandle), uintptr(handle), 0)
    if r0 == 0 {
        if e1 != 0 {
            return error(e1)
        }
        return syscall.EINVAL
    }
    return nil
}

// redirectStderr to the file passed in
func redirectStderr(f *os.File) {
    err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(f.Fd()))
    if err != nil {
        log.Fatalf("Failed to redirect stderr to file: %v", err)
    }
    // SetStdHandle does not affect prior references to stderr
    os.Stderr = f
}

扩展@nick-craig-wood 的回答: 如果你在 Linux 上,你可以生成一个 logger(1) 实例并将 stderr 重定向到它。这样你就可以将完整的回溯跟踪到系统日志中。这就是 gocryptfs 所做的:

// redirectStdFds redirects stderr and stdout to syslog; stdin to /dev/null
func redirectStdFds() {
    // stderr and stdout
    pr, pw, err := os.Pipe()
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: could not create pipe: %v\n", err)
        return
    }
    tag := fmt.Sprintf("gocryptfs-%d-logger", os.Getpid())
    cmd := exec.Command("logger", "-t", tag)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Stdin = pr
    err = cmd.Start()
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: could not start logger: %v\n", err)
    }
    pr.Close()
    err = syscall.Dup2(int(pw.Fd()), 1)
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: stdout dup error: %v\n", err)
    }
    syscall.Dup2(int(pw.Fd()), 2)
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: stderr dup error: %v\n", err)
    }
    pw.Close()

    // stdin
    nullFd, err := os.Open("/dev/null")
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: could not open /dev/null: %v\n", err)
        return
    }
    err = syscall.Dup2(int(nullFd.Fd()), 0)
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: stdin dup error: %v\n", err)
    }
    nullFd.Close()
}