DEV Community

Michael Umeokoli
Michael Umeokoli

Posted on

Handling Concurrency With Goroutines and Channels in Golang

GOROUTINES AND CONCURRENCY

Concurrency in programming is the ability of a computer program to execute multiple instructions/tasks at a time. With concurrency long-running tasks do not hold up other tasks in the program, so instead of blocking, long-running tasks can run separately while the rest of the program continues. In summary concurrency is when a task does not have to wait until another task finish before running. This ensures speedy and efficient execution of programs.

Different programming languages have different methods of handling concurrency Go handles it with goroutines, A goroutine is a lightweight execution thread in the Go programming language and a function that executes concurrently with the main program flow. They have less overhead on your program than traditional threads (which can also handle concurrency) and are therefore the popular option in Go programming.

CREATING A GOROUTINE

Go routines are normal functions but called with the “go” keyword, basically any function can become a goroutine,

Example:

func helloWorld(str string) {
    for i := 0; i < 6; i++ {
    fmt.Println(str, ":", i)
    }
}

func main() {
    go helloWorld("Hello World")
    time.Sleep(1 * time.Second)
}
Enter fullscreen mode Exit fullscreen mode

Here we have a program containing three goroutines , the main function which is automatically a routine and the two helloWorld functions called inside the main function using the go keyword. The helloWorld goroutine prints out “Hello World” and “hello world” five times each.

Notice the time.Sleep(1 * time.Second)in the “main” function, it delays the function for one second because without it the “main” function would not wait for our “helloWorld” goroutine to finish execution before going to the next line and finishing the program.

WHAT ARE CHANNELS AND WHAT ARE THEY USED FOR?

Channels are like pipelines between goroutines, they provide a way for goroutines to communicate effectively among themselves, Channels are a way of sending data of a particular type from one data type to another.

We create channels using the make method with the type chan followed by the data type you want the channel to send as arguments in the make() method;

var channel = make(chan int)
Enter fullscreen mode Exit fullscreen mode

This is an example program of channels in use;

package main

import (
    "fmt"

func sendInteger(myIntChannel chan int){
    for i:=1; i<6; i++ {
      myIntChannel <- i // sending value
    }
  }

func main() {
    myIntChannel := make(chan int)
    go sendInteger(myIntChannel) 
for i:=1; i<6; i++ {
  message := <-myIntChannel //receiving value
      fmt.Println(message) 
    }
}
Enter fullscreen mode Exit fullscreen mode

SENDING AND RECEIVING VALUES WITH CHANNELS

We create the channel myIntChannel in the main function and then pass it into the sendInteger goroutine where we use the channel to send numbers 1-5 with the channel on the left side of the special left-pointing arrow (<-) and the value we want to send on the right side of the arrow. We then receive the value sent from the sendInteger goroutine in the main function but this time the channel is on the right side of the arrow and the values sent are printed out with the two functions running concurrently.

One thing to note when using channels to send and receive data is ‘blocking’ i.e blocking the program, statements like message := <-myIntChannel that receive data through channels will block until they have received the data and statements to send data will also block until receivers are ready.

DIRECTION IN CHANNELS

Channels can be directed i.e specified to either send or receive data, we can do this using the <- arrow and the chan keyword in the arguments of the function to be used.

func sendInteger(myIntChannel chan <- int){ //send-only channel---the left arrow on the right side of the "chan" keyword in the function's arguments specifies a send only channel

    for i:=1; i<=50; i++ {
     myIntChannel <- i // sending value
    }
    
}

func printer(myIntChannel <- chan int)  { //receive-only channel---the left arrow on the left side of the "chan" keyword in the function's arguments specifies a receive only channel
    for i:=1; i<=50; i++ {
        message := <-myIntChannel //receiving value
        fmt.Println(message) 
    }
}

func main() {
    myIntChannel := make(chan int)
    go sendInteger(myIntChannel) 
    go printer(myIntChannel)
    time.Sleep(1 * time.Second)
}
Enter fullscreen mode Exit fullscreen mode

SELECT STATEMENTS

Select statements are almost identical to the switch statement in Go, they both serve the purpose of conditional execution of statements but select statements are more specific to channels, they help execute actions when a condition is met by a channel.

Example:

func sendValuesFast(myStringChannel1 chan string){
      time.Sleep(5 * time.Millisecond)
      myStringChannel1 <- "Fast" // sending value
}

func sendValuesSlow(myStringChannel2 chan string){
     time.Sleep(30 * time.Millisecond)
      myStringChannel2 <- "Slow" // sending value
    
}

func main() {
   myStringChannel1 := make(chan string)
   myStringChannel2 := make(chan string)

    go sendValuesFast(myStringChannel1) 
    go sendValuesSlow(myStringChannel2)

    select{
    case res:= <- myStringChannel1:
          fmt.Println("sendValuesFast finished first",res)
    case res:= <- myStringChannel2:
        fmt.Println("sendValuesSlow finished first",res)
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example we have two channels in the sendValuesFast and sendValuesSlow goroutines, we use a time.Sleep() delay to myStringChannel2 so the myStringChannel completes first and the first case in the select statement is met and it’s action executed.

BUFFERED CHANNELS

So far we’ve been using what we call unbuffered channels and we said earlier that they block until data is sent or received on the channel, this is because unbuffered channels have no memory space for data being sent through it hence they have to wait till there is a statement to receive it before sending again.

Buffered channels on the other hand are created with memory allocations in the make() method and will only block if the channel is full (when sending) or if the channel is empty (when receiving). It allows you to store the amount of data specified on creation for example channel:=make(chan int, 5) creates a channel that can store 5 integers and if a 6th is sent the channel will block until the messages in the channel are read.

func bufferFunction(bufferChannel chan int)  {
    for i := 1; i<=6; i++ { 
        bufferChannel <- i
        time.Sleep(1 * time.Second)
        fmt.Println("Channel sent", i)
    }
}

func main()  {
   bufferChannel:= make(chan int,5)
   bufferFunction(bufferChannel)
}
Enter fullscreen mode Exit fullscreen mode

Here we have an example buffered channel with space for 5 integers and we use a for loop to try and send the numbers 1-6 , we do this without a receiving statement and so when the loop tries to send the 6th integer the channel blocks and the program finishes.

This article was written to help me better understand goroutines and channels, SPOILER ALERT: i still dont understand them!!😂😭( just kidding😁,or am I? ).

Top comments (0)