Concurrent Golang Applications With Goroutines And Channels

One of the highlights of the Go programming language is its ability to handle concurrency with minimal effort using what are called goroutines and channels.  The beauty here versus other programming languages is that you don’t end up in a callback mess or locking hell.  In fact, you can even create far more goroutines in Go than you can in a language such as Java with the same hardware.

We’re going to see an example where we have an application that starts several worker goroutines and shares a channel for accessing data.

By default, every Go application has one goroutine.  This is known as the main goroutine that the main function operates on.  In our example we’re going to use the main goroutine to spin up a few goroutines that we’ll refer to as workers.  After the workers are available, we’ll perform a simple task where the main goroutine feeds data into a channel.  The plan here is that each worker will work with the data in that channel until the feeder closes the channel, at which point the workers will terminate.

Create a file somewhere in your $GOPATH called main.go which keep all of our project code.  Open this file and include the following code:

The above boilerplate code includes our imports, the main function, the worker function that will represent every goroutine, and two global variables.

In normal circumstances, after the main function reaches the end of the function, the application will terminate, even if there are goroutines active.  To prevent this we’re going to use a WaitGroup which will allow us to wait until each goroutine says that they are done.  To avoid locking, we can synchronize with a channel that we’ll call data in the application.

So let’s take a look at our worker function:

So what is happening in the above?

Remember, we’re going to be using a WaitGroup, so we need to be able to tell the main application that the goroutines have finished.  This is done through the defer function which executes when our worker function returns or finishes.  The worker function will actually run forever in a loop until we tell it to stop.  Every iteration will look at the channel and if it is not closed, the data will be printed.  If the channel had been closed, we will terminate the worker.

Since this is a basic example, we’re just going to put the worker to sleep after every cycle.  We’re doing this because our worker is so simple that the loop will take sub-milliseconds to finish.

Now let’s take a look at how the channel is populated and the workers are deployed.

In the above main function we are initializing the channel as an unbuffered channel.  Next we are starting several worker goroutines via a loop and for each goroutine started we increase the count of the WaitGroup so that way we keep track of all the workers.

Once the workers have been started we loop a bunch of times, adding data to the channel.  This loop will finish almost instantly, much faster than a loop iteration in our worker.  Remember we are sleeping each of our workers for one second every cycle.

After the channel has our values we close it.  Since it is closed, each worker will terminate after the channel becomes empty.  Finally, we tell the main function to wait.

The full code to this project can be seen below:

There is something you should note when it comes to goroutines and channels.  Let’s say we tried to add data to the channel before the workers had started.  This would result in deadlock errors because the channel would get clogged.  You need at least one goroutine reading from it to prevent it from blowing up.

Conclusion

You just saw a simple example of concurrency in a Golang application that uses goroutines and channels.  The Go programming language makes this incredibly simple in comparison to other development technologies because it’s fast, you can easily spin up hundreds of thousands of goroutines, and you don’t have to have messy callback code or worry about locking.

Want to see this put to good use?  Check out a previous article I wrote titled, Create a Real Time Chat App with Golang, Angular, and Websockets, which uses concurrency for managing incoming and outgoing data via websockets.

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.

  • Dave Mazzoni

    Nic, Nice example. I rewrote it to be more idiomatic (I didn’t like using global variables) even though your example is clear as a tutorial.
    Here’s my version if you care. In my comments I emphasize the “RIGHT” way to pass WaitGroup only because I passed values the first time (i.e., it’s more of a reminder to ME, not criticism of your example):

    // A simple example using channels, multiple goroutines (workers) and the
    // RIGHT way to use sync.WaitGroup to synchronize all routines.
    // If a pointer to the WaitGroup were not used, it would update a copy in
    // the worker function and function main would PANIC since it wasn’t updated.
    package main

    import (
    “fmt”
    “strconv”
    “sync”
    “time”
    )

    func main() {
    fmt.Println(“Starting the application…”)
    wg := sync.WaitGroup{}
    data := make(chan string)

    }

    // worker reads a value from a channel and prints it
    // NOTE: a pointer to WaitGroup MUST be used so the value in main is changed,
    // not the local copy.
    func worker(workerN int, data chan string, wg *sync.WaitGroup) {
    fmt.Println(“Goroutine”, workerN, “is now starting…”)
    defer func() {
    fmt.Println(“defered destruction of worker”, workerN)
    wg.Done()
    }()
    for {
    val, ok := <-data
    if !ok {
    fmt.Println(“Channel closed, exiting for(ever) loop”)
    break
    }
    fmt.Println(“Worker:”, workerN, “:”, val)
    time.Sleep(time.Millisecond * 100)
    }
    }

    • Great alternative to global variables. Your approach is probably better than mine even though neither are wrong.

      Please re-add your code and use pre / code tags for formatting.

      • Dave Mazzoni


        // A simple example using channels, multiple goroutines (workers) and the
        // RIGHT way to use sync.WaitGroup to synchronize all routines.
        // If a pointer to the WaitGroup were not used, it would update a copy in
        // the worker function and function main would PANIC since it wasn't updated.
        package main

        import (
        "fmt"
        "strconv"
        "sync"
        "time"
        )

        func main() {
        fmt.Println("Starting the application...")
        wg := sync.WaitGroup{}
        data := make(chan string)

        }

        // worker reads a value from a channel and prints it
        // NOTE: a pointer to WaitGroup MUST be used so the value in main is changed,
        // not the local copy.
        func worker(workerN int, data chan string, wg *sync.WaitGroup) {
        fmt.Println("Goroutine", workerN, "is now starting...")
        defer func() {
        fmt.Println("defered destruction of worker", workerN)
        wg.Done()
        }()
        for {
        val, ok := <-data
        if !ok {
        fmt.Println("Channel closed, exiting for(ever) loop")
        break
        }
        fmt.Println("Worker:", workerN, ":", val)
        time.Sleep(time.Millisecond * 100)
        }
        }

        • My assumption is that you mixed up the pre / code tags. Disqus isn’t as friendly as I’d like it to be. However, this application is short so what you’ve pasted is fine in my opinion.

          Thanks for sharing it 🙂

      • Dave Mazzoni

        Nic, it still didn’t work.