TIL: How to listen to different servers in Go (using select)
Tags: todayilearned, go, select
Published: 2022-04-25 17:48:09

---
title: TIL: How to listen to different servers in Go (using select)
published: true
description: Refactoring to use the select statement correctly in Go
tags: todayilearned,go,select
//cover_image: https://direct_url_to_image.jpg
---
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.

```go
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 */)
}
```

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.

```go
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
}
```

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!](https://github.com/mcaci/briscola-serv/commit/010d0fb5e95fff4d07db03b5eb73193276967aad?diff=split) A.k.a the github reference for the commit I did with the complete change that inspired this TIL.