Many times, you find yourself in a situation where you have to wait for a long task to complete, maybe little to no output available, and you end up hitting that Ctrl-C, pushing a SIGINT into the CLI software.
And it quits.
Without outputting whatever you needed or even printing a little progress. And it’s a little frustrating, even though it’s probably working as expected, taking what SIGINT does in consideration.
So, how do we introduce a classic “do you really want to exit?” confirmation in a Go CLI software?
The code for the program above can be seen here:
type Thing struct {
sync.Mutex
Index int
Length int
}
func (t *Thing) DoTask(i int) {
t.Lock()
t.Index = i
fmt.Printf("Doing thing for index #%d\n", t.Index+1)
time.Sleep(1 * time.Second)
t.Unlock()
}
var t Thing
func main() {
fmt.Println("Hello gophers!")
t.Length = 10
for i := 0; i < t.Length; i++ {
t.DoTask(i)
}
}
We have a Thing
struct which inherits the sync.Mutex
type, allowing it to have a simple locking/unlocking mechanism. The DoTask
method simply locks the struct, sets the current index, sleeps for a while emulating some hard work, then unlocks the struct. Nothing too complicated.
Catching a SIGINT in a Go program involves creating a (buffered) channel of type os.Signal
, then using signal.Notify(c chan<- os.Signal, sig ...os.Signal)
in order to relay the specified signals - in our case, os.Interrupt
- to the channel we created earlier.
func init() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
fmt.Println("Caught SIGINT")
}
}()
}
func main() {
time.Sleep(30 * time.Second)
}
So how do we make it interactive? How do we tell our worker to pause or stop working on a SIGINT? Well, it’s pretty easy now, noting that our Thing
struct already has the locking and unlocking mechanism.
We only have to (ab)use the mutex and lock when catching a signal.
func init() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for {
select {
case <-c:
t.Lock()
fmt.Printf("\n%d/%d tasks, pausing...", t.Index, t.Length)
fmt.Printf("\nReally quit? [y/n] > ")
var choice string
fmt.Fscanf(os.Stdout, "%s", &choice)
if choice == "y" {
fmt.Println("Cleaning up...")
os.Exit(0) // definitely not the best way to exit
}
t.Unlock()
}
}
}()
}
What if we don’t need to get user input, but only exit (gracefully)? Let’s suppose we want to gracefully stop a running worker on a SIGINT, but somehow a goroutine blocks it, and no matter how hard or how many times we send a SIGINT, it will just stand there, doing nothing?
We could count the received signals, and force a not-so-graceful exit.
func init() {
var count int
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt)
go func() {
for {
select {
case <-c:
count++
if count == 2 {
fmt.Println("Forcefully exiting")
os.Exit(1)
}
fmt.Println("Doing something with a signal")
fmt.Println("Press Ctrl-C one more time to exit")
}
}
}()
}
func main() {
time.Sleep(30 * time.Second)
}
So there it is. This is merely an example of how one could catch & use an OS signal. The pretty printing and cleaning up remains an exercise for the reader.