Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Concurrent Golang Applications With Goroutines And Channels

TwitterFacebookRedditLinkedInHacker News

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:

package main

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

var waitGroup sync.WaitGroup
var data chan string

func main() { }

func worker() { }

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:

func worker() {
    fmt.Println("Goroutine worker is now starting...")
    defer func() {
        fmt.Println("Destroying the worker...")
        waitGroup.Done()
    }()
    for {
        value, ok := <-data
        if !ok {
            fmt.Println("The channel is closed!")
            break
        }
        fmt.Println(value)
        time.Sleep(time.Second * 1)
    }
}

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.

func main() {
    fmt.Println("Starting the application...")
    data = make(chan string)

    for i := 0; i < 3; i++ {
        waitGroup.Add(1)
        go worker()
    }

    for i := 0; i < 50; i++ {
        data <- ("Testing " + strconv.Itoa(i))
    }

    close(data)

    waitGroup.Wait()
}

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:

package main

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

var waitGroup sync.WaitGroup
var data chan string

func main() {
    fmt.Println("Starting the application...")
    data = make(chan string)

    for i := 0; i < 3; i++ {
        waitGroup.Add(1)
        go worker()
    }

    for i := 0; i < 50; i++ {
        data <- ("Testing " + strconv.Itoa(i))
    }

    close(data)

    waitGroup.Wait()
}

func worker() {
    fmt.Println("Goroutine worker is now starting...")
    defer func() {
        fmt.Println("Destroying the worker...")
        waitGroup.Done()
    }()
    for {
        value, ok := <-data
        if !ok {
            fmt.Println("The channel is closed!")
            break
        }
        fmt.Println(value)
        time.Sleep(time.Second * 1)
    }
}

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

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