Golang: Comprehensive guide on Golang channels

Golang: Comprehensive guide on Golang channels

Golang: Comprehensive guide on Golang channels To understand channels, we first need to grasp one of the fundamental and pioneering concepts of Go, which is goroutines.

So what are goroutines?

Goroutines are functions or methods that run concurrently with other functions or methods.

Some properties about goroutines

  • They are lightweight threads.
  • main() method is also a goroutine called main Goroutine.
  • A goroutine doesn’t take up much memory, usually just a few kilobytes. Unlike threads, which have a fixed size, a goroutine’s memory usage can be changed as needed.
package main
import (
   "fmt"
   "time"
)
func main() {
   go func() {
      fmt.Println("Hello from goroutine!!")
   }()
   time.Sleep(1 * time.Second)
   fmt.Println("main function")
}

Output:

Hello from goroutine!!!
main function

What are channels?

A channel is a golang data type that allows us to share data between goroutines.

  • The zero value of a channel is nil, this nil channel cannot be used, like maps and slices, channels must be created before use with make.
ch := make(chan int)

There are three main types of channels we can explore:

  • Buffered channels.
  • Unbuffered channels.
  • Unidirectional channels (just write, just read, read and write)

Unbuffered channels

Unbuffered channels, often referred to as synchronous channels, guarantee that data sent by the sender is immediately received by the receiver, eliminating any potential for delays.

  • An unbuffered channel has no capacity which means that only can handle one value at a time.
  • By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.
package main

import (
 "fmt"
)
func main() {
   // Create an unbuffered channel
   ch := make(chan int)
  
   go func() {
        value := 24
        fmt.Printf("Sending %d to the channel...\n", value)
        ch <- value // Send data to the unbuffered channel
        close(ch);
   }()
  
   receivedValue := <-ch // Receive data from the unbuffered channel
   fmt.Printf("Received %d from the channel.\n", receivedValue)
}

Output:

Sending 24 to the channel...
Received 24 from the channel.

Buffered channels

Buffered channels, also known as asynchronous channels, provide more flexibility for communication between senders and receivers.

Buffered channels have a buffer size that’s useful when the sender wants to send data in bursts, but the receiver might handle it on slower rate.

Points to consider about how the buffered channels work:

  • When the buffered channel is empty: Receiving messages is blocked until a message is sent across the channel.
  • When the buffered channel is full: Sending messages in the channel is blocked until at last one channel is received from the channel, thus making space for new messages to be queued on the channel.
package main

import (
 "fmt"
 "time"
)
func main() {
   ch := make(chan int, 2)
   go func() {
      for i := 0; i < 5; i++ {
         ch <- i
         fmt.Println("successfully wrote", i, "to channel")
      }
      close(ch)
   }()
   time.Sleep(time.Second)
   for v := range ch {
      fmt.Println("read value", v, "from channel")
      time.Sleep(time.Second)
   }
}

Unidirectional channels

By default golang create the channel as a bidirectional mode which means that it is allowed to send or receive data.

A channel can also be one-way, only send or recieve data.

ch := make(chan int) // bidirectional channel
sendch := make(chan<- int) // unidirectional channel
var send chan<- int // can only send data to channel
var receive <-chan int // can only receive data from channel

This will throw an error:

package main

import "fmt"
func main() {
   sender := make(chan<- int) // unidirectional channel
   go func() {
      sender <- 10
   }()
  
   fmt.Println(<-sender)
}

error:

invalid operation: cannot receive from send-only channel sender (variable of type chan<- int)

The previous code only contains the sending part without any receiving part, so it will result in an error. Essentially, having just a send channel without receiving is pointless.

Working example:

package main

import (
 "fmt"
)
func sendData(sendCh chan<- int) {
   for i := 1; i <= 2; i++ {
      fmt.Printf("Sending data: %d\n", i)
      sendCh <- i // Send data on the send-only channel
   }
   close(sendCh)
}
func main() {
   sendChannel := make(chan int)
   
   go sendData(sendChannel)
  
   for value := range sendChannel {
      fmt.Printf("Received: %d\n", value)
   }
}

Output:

Sending data: 1
Sending data: 2
Received: 1
Received: 2

Closing channels

Closing a channel indicates that no more values will be sent by the sender. This is handy to let the receivers know that all the expected data has been sent.

  • The sender can close the channel and notify the receivers that there is no data.
  • Only the sender should close a channel, and it should only be closed if there’s just one sender using it.

Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression:

v, ok := <- ch
// ok
// true: channel still open
// false: No value and channel has been closed

On an alternate range loop can be used to receive values from the channel repeatedly until it is closed.

This example will panic as there is no data to receive:

package main

import (
 "fmt"
)
func producer(ch chan int) {
   for i := 0; i < 2; i++ {
      ch <- i
   }
}
func main() {
   ch := make(chan int)
   go producer(ch)
   for {
      v := <-ch // As there is no close(ch), decklock will be generated
                // because there is no data in it.
      fmt.Println("Received ", v)
   }
}

Output:

Received  0
Received  1
fatal error: all goroutines are asleep - deadlock!

Here the maingoroutine, it enters an infinite loop where it continuously tries to receive data from the channel chusing <-ch. However, since there’s no close of channel ch, it will keep waiting for more data from the channel, resulting in a deadlock.

Working example:

package main

import (
 "fmt"
)

func producer(ch chan int) {
   for i := 0; i < 3; i++ {
      ch <- i
   }
   close(ch)
}

func main() {
   ch := make(chan int)
   go producer(ch)
   for {
      v, ok := <-ch
      if ok == false {
         break
      }
      fmt.Println("Received ", v, ok)
   }
   /*
      // Range loop will automatically leave when the channel closes
      for v := range ch {
          fmt.Println("Received ", v)
      }
   */
}

Deadlock

When a Goroutine sends data to a channel, but there is no other Goroutine to receive the data, a Deadlock will occur and an error panic will occur. fatal error: all goroutines are asleep — deadlock

package main

func main() {
    ch := make(chan string)
    ch <- "hello world!"
}
Write a comment
No comments yet.