Skip to content

Instantly share code, notes, and snippets.

@pteich
Last active May 30, 2024 02:57
Show Gist options
  • Save pteich/c0bb58b0b7c8af7cc6a689dd0d3d26ef to your computer and use it in GitHub Desktop.
Save pteich/c0bb58b0b7c8af7cc6a689dd0d3d26ef to your computer and use it in GitHub Desktop.
Example for using go's sync.errgroup together with signal detection signal.NotifyContext to stop all running goroutines
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")
}
}
@embano1
Copy link

embano1 commented May 18, 2020

/bin/true

@pantelis-karamolegkos
Copy link

Pretty neat 👍

@pantelis-karamolegkos
Copy link

Shouldn't there be a defer done() after line 17?

@pteich
Copy link
Author

pteich commented May 2, 2022

Shouldn't there be a defer done() after line 17?

For a real world usage I would suggest this too, agreed.

@alifarooq0
Copy link

This is great! Saved me time. thanks!

@jyouturner
Copy link

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().

@pteich
Copy link
Author

pteich commented Dec 14, 2022

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.

@ceferrari
Copy link

Maybe I create an updated version.

@pteich It would be great!

@bjwschaap
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment