How to include goroutine into a context?

1.5k views Asked by At

I'm working on a Go project that require calling an initiation function (initFunction) in a separated goroutine (to ensure this function does not interfere with the rest of the project). initFunction must not take more than 30 seconds, so I thought I would use context.WithTimeout. Lastly, initFunction must be able to notify errors to the caller, so I thought of making an error channel and calling initFunction from an anonymous function, to recieve and report the error.

func RunInitGoRoutine(initFunction func(config string)error) error {

    initErr := make(chan error)
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Seconds)

    go func() {
        <-ctx.Done()  // Line 7
        err := initFunction(config)
        initErr <-err
    }()

    select {
    case res := <-initErr:
        return res.err
    case <-ctx.Done():
        err := errors.New("Deadline")
    return err
    }
}

I'm quite new to Go, so I'm asking for feedbacks about the above code.

  1. I have some doubt about Line 7. I used this to ensure the anonymous function is "included" under ctx and is therefore killed and freed and everything once timeout expires, but I'm not sure I have done the right thing.
  2. Second thing is, I know I should be calling cancel( ) somewhere, but I can't put my finger around where.
  3. Lastly, really any feedback is welcome, being it about efficency, style, correctness or anything.
1

There are 1 answers

0
Marco On

In Go the pratice is to communicate via channels. So best thing is probably to share a channel on your context so others can consume from the channel.

As you are stating you are new to Go, I wrote a whole bunch of articles on Go (Beginner level) https://marcofranssen.nl/categories/golang.

Read from old to new to get familiar with the language.

Regarding the channel specifics you should have a look at this article.

https://marcofranssen.nl/concurrency-in-go

A pratical example of a webserver listening for ctrl+c and then gracefully shutting down the server using channels is described in this blog post.

https://marcofranssen.nl/improved-graceful-shutdown-webserver

In essence we run the server in a background routine

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
      srv.l.Fatal("Could not listen on", zap.String("addr", srv.Addr), zap.Error(err))
    }
  }()

and then we have some code that is blocking the main routine by listening on a channel for the shutdown signal.

quit := make(chan os.Signal, 1)

  signal.Notify(quit, os.Interrupt)
  sig := <-quit
  srv.l.Info("Server is shutting down", zap.String("reason", sig.String()))

  ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  defer cancel()

  srv.SetKeepAlivesEnabled(false)
  if err := srv.Shutdown(ctx); err != nil {
    srv.l.Fatal("Could not gracefully shutdown the server", zap.Error(err))
  }
  srv.l.Info("Server stopped")

This is very similar to your usecase. So running your init in a background routine and then consume the channel waiting for the result of this init routine.

package main

import (
    "fmt"
    "time"
)

type InitResult struct {
    Message string
}

func main() {
    initResult := make(chan InitResult, 0)
    go func(c chan<- InitResult) {
        time.Sleep(5 * time.Second)
        // here we are publishing the result on the channel
        c <- InitResult{Message: "Initialization succeeded"}
    }(initResult)

    fmt.Println("Started initializing")

    // here we have a blocking operation consuming the channel
    res := <-initResult

    fmt.Printf("Init result: %s", res.Message)
}

https://play.golang.org/p/_YGIrdNVZx6

You could also add an error field on the struct so you could do you usual way of error checking.