-
-
Save pteich/c0bb58b0b7c8af7cc6a689dd0d3d26ef to your computer and use it in GitHub Desktop.
| package main | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "os/signal" | |
| "syscall" | |
| "time" | |
| "golang.org/x/sync/errgroup" | |
| ) | |
| func main() { | |
| ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) | |
| defer done() | |
| g, gctx := errgroup.WithContext(ctx) | |
| // just a ticker every 2s | |
| g.Go(func() error { | |
| ticker := time.NewTicker(2 * time.Second) | |
| i := 0 | |
| for { | |
| i++ | |
| if i > 10 { | |
| return nil | |
| } | |
| select { | |
| case <-ticker.C: | |
| fmt.Println("ticker 2s ticked") | |
| case <-gctx.Done(): | |
| fmt.Println("closing ticker 2s goroutine") | |
| return gctx.Err() | |
| } | |
| } | |
| }) | |
| // just a ticker every 1s | |
| g.Go(func() error { | |
| ticker := time.NewTicker(1 * time.Second) | |
| i := 0 | |
| for { | |
| i++ | |
| if i > 10 { | |
| return nil | |
| } | |
| select { | |
| case <-ticker.C: | |
| fmt.Println("ticker 1s ticked") | |
| case <-gctx.Done(): | |
| fmt.Println("closing ticker 1s goroutine") | |
| return gctx.Err() | |
| } | |
| } | |
| }) | |
| // force a stop after 15s | |
| time.AfterFunc(15*time.Second, func() { | |
| fmt.Println("force finished after 15s") | |
| done() | |
| }) | |
| // wait for all errgroup goroutines | |
| err := g.Wait() | |
| if err != nil { | |
| if errors.Is(err, context.Canceled) { | |
| fmt.Println("context was canceled") | |
| } else { | |
| fmt.Printf("received error: %v\n", err) | |
| } | |
| } else { | |
| fmt.Println("finished clean") | |
| } | |
| } |
/bin/true
Pretty neat 👍
Shouldn't there be a defer done() after line 17?
Shouldn't there be a
defer done()after line 17?
For a real world usage I would suggest this too, agreed.
This is great! Saved me time. thanks!
Correct me if I am wrong. I think line 21 is better to create the go-routine not part of the errgroup.
go func() { signalChannel := make(chan os.Signal, 1) ...
This way, there is no risk of the deadlock between this goroutine and g.wait().
Correct me if I am wrong. I think line 21 is better to create the go-routine not part of the errgroup.
go func() { signalChannel := make(chan os.Signal, 1) ...This way, there is no risk of the deadlock between this goroutine and g.wait().
Yes, with my knowledge today I would probably do it this way. Not only to prevent a deadlock but also b/c it simply does not belong to the errorgroup and handles a higher level controlling. In addition, we now have a signal.NotifyContext that makes creating the initial context easier. But 5 years ago when I created it, I was happy to have a working solution :)
Maybe I create an updated version.
Maybe I create an updated version.
@pteich It would be great!
Currently there is the NotifyContext, which makes it even simpler because you don't have to create a separate goroutine for propagating the signal to context cancellation: https://pkg.go.dev/os/signal#NotifyContext
@embano1 As an even later response: Depends of what you call an error :) I would think a canceled context could be still an error. But good catch to force checking for it. I changed the gist.