Skip to main content
Go Language Fundamentals for Beginners to Advanced
CHAPTER 20 Beginner

Channels in Go

Updated: May 17, 2026
5 min read

# CHAPTER 20

Channels in Go

1. Introduction

In Chapter 19, we spawned Goroutines to run concurrently, but we had a major problem: we used time.Sleep to prevent main() from exiting, and the Goroutines couldn't send their results back to main().

"Do not communicate by sharing memory; instead, share memory by communicating." This is the famous proverb of Go. To let Goroutines talk to each other safely, Go provides Channels. Think of a Channel as a pipe that connects two Goroutines. You push data into one end of the pipe, and another Goroutine pulls it out the other end.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Create a Channel using make().
  • Send data into a channel.
  • Receive data from a channel.
  • Understand how channels naturally Block and Synchronize execution.
  • Differentiate between Unbuffered and Buffered channels.

3. Creating a Channel

Channels are typed, meaning a channel can only transport one specific type of data (e.g., only int or only string). You create them using the make() function.
go
12
// Creates a channel that can transport integers
messages := make(chan int)

4. Sending and Receiving Data (The <- Operator)

The arrow operator <- dictates the flow of data.
  • Sending Data: messages <- 5 (Pushes the integer 5 INTO the pipe).
  • Receiving Data: result := <-messages (Pulls data OUT of the pipe and saves it to result).

5. Goroutine Synchronization (Blocking)

Here is the most critical concept of Channels: Unbuffered channels block execution until the exchange is complete.
  • If a Goroutine sends data (ch <- 5), it will freeze and wait until another Goroutine receives the data.
  • If a Goroutine tries to receive (<-ch), it will freeze and wait until data is pushed in.

This completely eliminates the need for time.Sleep! The blocking behavior automatically synchronizes our code.

go
12345678910111213141516171819202122232425
package main
import "fmt"

// This function runs in a separate Goroutine
func calculateSum(a, b int, resultChan chan int) {
    sum := a + b
    fmt.Println("Goroutine: Calculation complete, sending to channel...")
    
    // Push the result into the channel
    resultChan <- sum 
}

func main() {
    resultChan := make(chan int)

    // Spawn the Goroutine
    go calculateSum(10, 20, resultChan)

    fmt.Println("Main: Waiting for the result...")
    
    // Main Goroutine FREEZES here until data arrives in the channel
    finalResult := <-resultChan 
    
    fmt.Println("Main: Received result:", finalResult)
}

*Notice: main() didn't need time.Sleep! It waited naturally at <-resultChan until the data was delivered.*

6. Deadlocks

If you send data into a channel, but there is no other Goroutine alive to receive it, the sender will freeze forever. Go detects this at runtime and crashes the program with a Fatal Error: Deadlock!
go
12345678
func main() {
    ch := make(chan string)
    
    // Attempting to send data in the main thread without a receiver ready
    ch <- "Hello" // FATAL ERROR: all goroutines are asleep - deadlock!
    
    fmt.Println(<-ch)
}

7. Buffered Channels

What if you want a Goroutine to drop off data into the pipe and immediately leave, even if the receiver isn't ready yet? You use a Buffered Channel. You specify a capacity when making the channel.
go
12345678910
// This channel can hold up to 2 strings before it blocks the sender
ch := make(chan string, 2)

ch <- "Message 1" // Doesn't block, room is available
ch <- "Message 2" // Doesn't block, room is available

// ch <- "Message 3" // If we added this, it WOULD block, because capacity is 2!

fmt.Println(<-ch)
fmt.Println(<-ch)

8. Common Mistakes

  • Deadlocks from Single-Threaded Logic: Trying to send and receive from an unbuffered channel inside the exact same function sequentially. It will deadlock instantly. Channels require at least *two* concurrent Goroutines to function.
  • Leaving Channels Open: When you are completely done sending data, you should use close(ch) so receivers know no more data is coming. (Crucial for range loops over channels).

9. Best Practices

  • Prefer Unbuffered Channels: Buffered channels can hide synchronization bugs. Unbuffered channels force you to write clean, tightly synchronized concurrent logic.

10. Exercises

  1. 1. Create a function multiply(a, b int, ch chan int).
  1. 2. In main, create a channel, spawn the Goroutine, and receive/print the multiplied result.

11. MCQs with Answers & Explanations

Question 1

What is a Channel in Go?

Question 2

How do you create an unbuffered channel for strings?

Question 3

Which syntax is used to SEND data into a channel ch?

Question 4

Which syntax is used to RECEIVE data from a channel ch?

Question 5

What happens when a Goroutine attempts to receive data from an empty, unbuffered channel?

Question 6

Because of channel blocking behavior, we no longer need to use what hacky function to keep main() alive?

Question 7

What is a "Deadlock"?

Question 8

What makes a Buffered Channel different?

Q9. Is it possible to loop over data arriving in a channel? a) Yes, using for msg := range ch b) No Answer: a) Yes. *(Note: the loop will only exit if the sender calls close(ch)).*
Question 10

"Do not communicate by sharing memory; instead, share memory by ___."

12. Interview Preparation

Interview Questions:
  1. 1. Explain the difference between an unbuffered channel and a buffered channel.
  1. 2. What causes a Deadlock in Go? Provide an example scenario.
  1. 3. How do channels solve the "race condition" problems found in traditional Thread/Mutex programming?

13. Summary

Channels elevate Go from a simple systems language to a Concurrency powerhouse. By acting as synchronized pipes, they allow Goroutines to pass data safely without the need for complex, bug-prone memory locks. Understanding how channels block execution naturally is the key to writing professional backend services in Go.

14. Next Chapter Recommendation

We know how to spawn a Goroutine and listen to a channel. But what if we spawn 50 Goroutines and need to wait for ALL of them to finish before proceeding? Or what if we need to listen to 3 different channels at the same time? In Chapter 21: Advanced Concurrency Patterns, we will explore WaitGroups and the select statement.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·