Go routine and context issue

Hi everybody,

I am trying to learn contexts. I wrote the following simple toy example to test the behaviour of
withCancel. I expect the Println("cancelled..") (line 26) to be printed on the stdout before exiting the program when I hit Ctrl-C interrupt. However, it does not. :slight_smile: Can someone explain me simply what I am doing wrong and how should I approach to the problem given my final expected behavior?

I believe somethings with the concept of using contexts are not clear in my mind (yet). I read the effective-go reference, go blog article, and also some other resources, still could not see what is wrong. :exploding_head:

Thanks for your help in advance.

Here is the code piece:

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go infiniteForLoop(ctx)
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, os.Interrupt)
	// Wait for SIGINT.
	<-sig
}

func infiniteForLoop(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("cancelled...")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("looping...")
		}
	}
}

I think I found out what was my issue. It looks like the main() function exits immediately after the signal is received on the sig channel. So this does not leave anytime for the go routine to execute its println().

In this regard, i will need to implement a second channel probably that will block the main() until the channel is filled in the infiniteForLoop() case ctx.Done(). This will also require to call the cancel() function explicity (instead of using with defer). So it will be something around this (see below):

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//defer cancel()
	ok := make(chan bool, 1)

	go infiniteForLoop(ctx, ok)
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, os.Interrupt)
	// Wait for SIGINT.
	<-sig
	fmt.Println("interrupt received...waiting for loop to cancel...")
	cancel()
	<-ok
}

func infiniteForLoop(ctx context.Context, ok chan bool) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("cancelled...", ctx.Err())
			ok <- true
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("looping...")
		}
	}
}

Is this the idiomatic way of doing this or would you suggest better ways? Thanks a lot again!

Hi @unique_gembom,

A simple trick good practice is to use main only for starting the app. Put anything more sophisticated into a separate function, including actions that require deferred cleanup.

func main() {
    // very basic setup only
    err := run()
    if err != nil {
        log.Println(err)
    }
}

func run() error {
    ctx, cancel := ...
    defer cancel()
    // do other stuff
    return nil
}

All deferred calls inside run() can complete before main() can even attempt to exit.

Using a “done” or “ok” channel to signal that a goroutine has finished working is ok.
Alternatively, you can use a sync.WaitGroup or an ErrGroup to wait for multiple goroutines. The latter even allows to have goroutines return errors.

Thanks for the suggestions @christophberger !

Regarding the “done/ok channel” solution, how can I implement such a mechanism for multiple go routines concurrently (possibly parallel)? Something similar to WaitGroup.Add() concept. What is the good practice in this regard? A simple way that comes to my mind is to create a new “ok channel” for each running go routine (e.g. a slice of ok channels??) and wait until all is filled? However this definitely does not look nice/easy to implement… :thinking: :thinking:

I believe there should be a “standard” way of doing these things, since a typical internet server (e.g. chat server) wants to close connections before stopping/killing…

Once again thanks in advance for your valuable time and support!

You can create one single channel and spread it to all goroutines. They all write their ok into the channel, and the receiving goroutine (the main one usually) then counts the oks it receives, until all goroutines have answered. (Example.)

But then, a WaitGroup does the same and is perhaps more clear to the reader, because waiting is, after all, the single purpose of a WaitGroup.


Edit: enhanced example where each goroutine sends its “ID” back when done. Just for fun, to see the non-deterministic sequence in which the goroutines run.