在 Golang 中,是否可能以"defer"的方式捕获并运行 Ctrl+C 信号(SIGINT) 的清理功能?

283

我希望捕获从控制台发送的Ctrl+C (SIGINT) 信号,并打印出一些部分运行总数。

11个回答

342
您可以使用os/signal包来处理传入的信号。Ctrl+CSIGINT,因此您可以使用它来捕获 os.Interrupt
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func(){
    for sig := range c {
        // sig is a ^C, handle it
    }
}()

你可以自己决定如何结束程序并打印信息。


30
除了 for sig := range g { 外,你还可以使用 <-sigchan ,就像这个先前的答案中所示:https://dev59.com/MGoy5IYBdhLWcg3wq_wk - Denys Séguret
5
如果你真的打算在收到第一个信号后终止程序,那么可以使用该循环来捕获所有信号。如果你决定终止程序,该循环也能帮助你捕获所有信号。 - Lily Ballard
111
请注意:要使程序正常工作,必须实际构建该程序。如果您在控制台上通过“go run”运行程序并通过^C发送SIGTERM信号,则该信号将被写入通道,并且程序会做出响应,但似乎意外地退出了循环。这是因为SIGTERM信号也发送给了“go run”!(这给我带来了很大的困惑!) - William Pursell
9
请注意,为了使goroutine获得处理信号的处理器时间,主goroutine必须在适当的位置(如果有主循环,则在其中)调用阻塞操作或调用runtime.Gosched - misterbee
有没有办法可以显示程序当前所在的行堆栈?在我的情况下,是一些IO读取,我不知道它停留在哪一行... - James Tan
显示剩余7条评论

146

这个有效:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time" // or "runtime"
)

func cleanup() {
    fmt.Println("cleanup")
}

func main() {
    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-c
        cleanup()
        os.Exit(1)
    }()

    for {
        fmt.Println("sleeping...")
        time.Sleep(10 * time.Second) // or runtime.Gosched() or similar per @misterbee
    }
}

请在操场结帐


1
对于其他读者:请查看@adamonduty的答案,了解为什么要捕获os.Interrupt和syscall.SIGTERM。最好在这个答案中包含他的解释,特别是因为他比你早几个月发布了答案。 - Chris
1
你为什么要使用非阻塞通道?这是必要的吗? - Awn
4
@Barry 为什么缓冲区大小是2而不是1? - pdeva
3
这里是来自文档的一段节选:"signal包不会阻塞向c发送信号:调用者必须确保c有足够的缓冲空间以跟上预期的信号速率。对于仅用于通知一个信号值的通道,大小为1的缓冲区就足够了。" - bmdelacruz
1
这段代码可以运行,但我有一个问题。如果我将 os.Exit(1) 改为 os.Exit(0),然后运行 echo $?,退出码是 1 而不是 0。 - jimouris

31

补充其他答案的观点,如果你确实想捕获SIGTERM(kill命令发送的默认信号),你可以使用syscall.SIGTERM代替os.Interrupt。请注意,syscall接口是特定于系统的,可能不适用于所有地方(例如在Windows上)。但它可以很好地捕获两者:

c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
....

2
os.Kill怎么样? - Awn
3
很棒的问题! os.Kill 对应 syscall.Kill,它是一种可以发送但无法捕获的信号。它相当于命令kill -9 <pid>。如果你想捕获 kill <pid> 并优雅地关闭进程,你需要使用 syscall.SIGTERM - adamlamar
1
为什么要将其缓冲大小设置为2?(或者根本不缓冲?) - Mike Graf
1
答案在signal.Notify文档中:signal包不会阻塞对c的发送:调用者必须确保c具有足够的缓冲区空间以跟上预期的信号速率。一个无缓冲通道可能根本捕获不到任何信号。通过为每个注册信号估计一个插槽,可以捕获大多数信号,但不能保证。 - adamlamar

23

在发布时,接受的答案中存在一两个小错别字,因此这里是经过整理的版本。在这个例子中,我将在收到Ctrl+C时停止CPU分析器。

// capture ctrl+c and stop CPU profiler                            
c := make(chan os.Signal, 1)                                       
signal.Notify(c, os.Interrupt)                                     
go func() {                                                        
  for sig := range c {                                             
    log.Printf("captured %v, stopping profiler and exiting..", sig)
    pprof.StopCPUProfile()                                         
    os.Exit(1)                                                     
  }                                                                
}()    

4
请注意,为了让 goroutine 获得处理信号的处理器时间,主 goroutine 必须在适当的位置调用阻塞操作或调用 runtime.Gosched(如果程序有主循环,则在其中调用)来让出 CPU 时间。 - misterbee
1
@misterbee:这似乎并不必要。例如,尝试使用go build编译并在本地执行以下程序:https://go.dev/play/p/uVda7C4bZbe。我认为Go运行时会公平地分配每个活动的goroutine处理器时间来运行,并且示例程序似乎证实了这一点。您能分享一下您评论的基础是什么吗? - nishanthshanmugham

13

以上所有方式似乎在拼接时都可以工作,但是gobyexample的信号页面有一个非常干净和完整的信号捕获示例。值得加入到这个列表中。

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    sigs := make(chan os.Signal, 1)
    done := make(chan bool, 1)

    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        sig := <-sigs
        fmt.Println()
        fmt.Println(sig)
        done <- true
    }()

    fmt.Println("awaiting signal")
    <-done
    fmt.Println("exiting")
}

来源:gobyexample.com/signals


好的,我知道这很古老,但为什么要将其设置为缓冲通道?据我所知,使用非缓冲通道程序是正确的。 - Mike Graf
3
Notify示例代码中可以看出,"我们必须使用带缓冲的通道,否则如果在信号发送时我们没有准备好接收,就有可能会错过信号。" - willscripted
我是否正确理解,使用非缓冲通道会存在错过第二个未处理信号的风险?(而使用1个缓冲通道则会存在错过第三个信号等更多风险?) - Mike Graf
1
哦,这是一个很好的问题,我还没有尝试过。源文档中可能有一些答案。我没有看到什么明显的东西,所以可能需要更仔细地阅读一下。 - willscripted

2

查看示例

当我们运行这个程序时,它会阻塞等待一个信号。通过键入Ctrl+C(终端显示为^C),我们可以发送SIGINT信号,导致程序打印interrupt然后退出。

signal.Notify函数注册给定的channel以接收指定信号的通知。

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {

    sig := make(chan os.Signal, 1)
    done := make(chan bool, 1)

    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        sig := <-sig
        fmt.Println()
        fmt.Println(sig)
        done <- true

        fmt.Println("ctrl+c")
    }()

    fmt.Println("awaiting signal")
    <-done
    fmt.Println("exiting")
}

检测HTTP请求取消



package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {

    mux := http.NewServeMux()
    mux.HandleFunc("/path", func(writer http.ResponseWriter, request *http.Request) {

        time.Sleep(time.Second * 5)

        select {
        case <-time.After(time.Millisecond * 10):

            fmt.Println("started")
            return
        case <-request.Context().Done():
            fmt.Println("canceled")
        }
    })

    http.ListenAndServe(":8000", mux)

}

0

你可以创建一个不同的goroutine来检测syscall.SIGINT和syscall.SIGTERM信号,并使用signal.Notify将它们中继到一个通道中。你可以使用一个通道向那个goroutine发送一个钩子,并将其保存在函数切片中。当通道上检测到关闭信号时,你可以执行切片中的这些函数。这可以用于清理资源、等待正在运行的goroutine完成、持久化数据或打印部分运行总数。

我编写了一个小而简单的实用程序,在关闭时添加和运行钩子。希望它能有所帮助。

https://github.com/ankit-arora/go-utils/blob/master/go-shutdown-hook/shutdown-hook.go

你可以使用“defer”来实现这一点。
优雅地关闭服务器的示例:
srv := &http.Server{}

go_shutdown_hook.ADD(func() {
    log.Println("shutting down server")
    srv.Shutdown(nil)
    log.Println("shutting down server-done")
})

l, err := net.Listen("tcp", ":3090")

log.Println(srv.Serve(l))

go_shutdown_hook.Wait()

0

这是另一个版本,适用于您需要清理一些任务的情况。代码将在它们的方法中留下清理过程。

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"

)



func main() {

    _,done1:=doSomething1()
    _,done2:=doSomething2()

    //do main thread


    println("wait for finish")
    <-done1
    <-done2
    fmt.Print("clean up done, can exit safely")

}

func doSomething1() (error, chan bool) {
    //do something
    done:=make(chan bool)
    c := make(chan os.Signal, 2)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-c
        //cleanup of something1
        done<-true
    }()
    return nil,done
}


func doSomething2() (error, chan bool) {
    //do something
    done:=make(chan bool)
    c := make(chan os.Signal, 2)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-c
        //cleanup of something2
        done<-true
    }()
    return nil,done
}

如果您需要清理主函数,您还需要使用go func()在主线程中捕获信号。


0
从Go 1.16开始,你可以使用`signal.NotifyContext`,例如:
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

-1

Death是一个简单的库,它使用通道和wait group来等待关闭信号。一旦接收到信号,它将调用您想要清理的所有结构体的close方法。


3
需要翻译的内容:So many lines of code and an external library dependecy to do what can be done in four lines of code? (as per accepted answer)用这么多行代码和一个外部库依赖来完成只需要四行代码就能完成的工作?(按照被接受的答案) - Jay
它允许您并行清理所有内容,并在具有标准关闭接口的结构体上自动关闭。 - Ben Aldrich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接