DEV Community

Michele Caci
Michele Caci

Posted on

TIL: How to listen to different servers in Go (using select)

Today I learned how to listen to different servers in Go by refactoring an existing codebase of mine. The goal of this refactoring was to correctly use select statement instead of creating an "artificial" one.

This is the starting point: I'm omitting most of the code to best highlight the "artificial" select that was present at the beginning of my refactoring.

func Start(/* options here*/) error {
    errChan := make(chan error)
    go func() {
        c := make(chan os.Signal, 1)
        signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
        errChan <- fmt.Errorf("%s", <-c)
    }()
    // [...]
    go startHTTPSrv(errChan)
    // [...]
    go startGRPCSrv(errChan)
    // [...]
    return <-errChan
}

func startHTTPSrv(errChan chan<-error) { // the param list is simplified to focus only on the err chan
    errChan <- http.ListenAndServe(/* http.ListenAndServe params */)
}

func startGRPCSrv(errChan chan<-error) { // the param list is simplified to focus only on the err chan
    errChan <- gRPCServer.Serve(/* gRPCServer.Serve params */)
}
Enter fullscreen mode Exit fullscreen mode

Since the same errChan is used everywhere, any of the goroutines that ends up returning an error, be it the HTTP server, the GRPC one or the SIGINT/SIGTERM listener, will make the Start func exit.

This is actually the behaviour of a select statement; which is why I started transforming it and this is how it looks now.

func Start(o *Opts) error {
    select {
    case err := <-startHTTPSrv(newSrvData(o.HTTPAddr)):
        return err
    case err := <-startGRPCSrv(newSrvData(o.GRPCAddr)):
        return err
    case err := <-handleSigTerm():
        return err
    }
}

func startHTTPSrv(/* params */)  <-chan error{ // the param list is simplified to focus only on the err chan
    // [...]
    errChan := make(chan error)
    go func() {
        errChan <- http.ListenAndServe(/* http.ListenAndServe params */)
    }()
    return errChan
}

func startGRPCSrv((/* params */) <-chan error { // the param list is simplified to focus only on the err chan
    // [...]
    errChan := make(chan error)
    go func() {
        // [...]
        errChan <- gRPCServer.Serve(/* gRPCServer.Serve params */)
    }()
    return errChan
}

func handleSigTerm() <-chan error {
    // [...]
    errChan := make(chan error)
    go func() {
        c := make(chan os.Signal, 1)
        signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
        errChan <- fmt.Errorf("%s", <-c)
    }()
    return errChan
}
Enter fullscreen mode Exit fullscreen mode

At this point, the usage of the select statement is able to show the clear distinction between the different goroutines run and wait for the first one to exit to return from Start.

Besides the change in the Start func, the other functions have also been changed to return a receiver chan of errors instead of a plain error in order to be comfortably used inside the select statement.

There are two positive aspects in this refactor:

  1. It gives a good showcase for the usage of the select statement to listen to different processes/servers.
  2. It shows a way to transition from an "artificial" select statement to an idiomatic one.

I hope this was helpful, thanks a lot for your time reading it!

p.s. Here be dragons! A.k.a the github reference for the commit I did with the complete change that inspired this TIL.

Top comments (0)