Concurrency Basics in Go: A Beginner's Journey to Efficient Programming


Concurrency allows programs to execute multiple tasks at the same time, which makes them faster and more efficient. In Go, concurrency is built into the language and is relatively easy to use. Go uses goroutines for concurrent execution and channels for communication between them.
In this article, we'll explain the basic concepts of concurrency in Go, including:
Goroutines: What they are and how to use them.
Channels: How to communicate between goroutines.
Buffered vs. Unbuffered Channels: What’s the difference?
The Select Statement: Handling multiple channels.
1. Goroutines: Spawning, Behavior, and Lifecycle
A goroutine is a lightweight thread of execution. You can think of it as a small worker that performs a task in the background while the main program continues its execution. Goroutines are managed by Go's runtime, which handles scheduling and running them efficiently.
Example: Spawning a Goroutine
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
// Step 1: Start a new Goroutine
go sayHello() // This will run sayHello() in the background
// Step 2: Main function keeps running
time.Sleep(1 * time.Second) // Give the Goroutine time to run
fmt.Println("Hello from Main!")
}
Explanation:
go sayHello()
: This is how you spawn a new goroutine. Thego
keyword before a function call launches the function as a goroutine.time.Sleep(1 * time.Second)
: Since the main function will finish quickly, we usetime.Sleep()
to give the goroutine time to execute.In the output, you might see "Hello from Goroutine!" and "Hello from Main!" printed, though the order may vary because they run concurrently.
Output:
Hello from Goroutine!
Hello from Main!
Goroutines are very fast and lightweight, which allows you to spawn thousands of them without affecting performance significantly.
2. Channels: Creating, Sending, Receiving
A channel is a Go feature that allows goroutines to communicate with each other. A channel can be thought of as a "pipe" through which data can be sent and received.
Example: Basic Channel Communication
package main
import (
"fmt"
)
func sendMessage(ch chan string) {
ch <- "Hello from Goroutine!" // Send data into the channel
}
func main() {
// Step 1: Create a channel
messageChannel := make(chan string)
// Step 2: Start a goroutine that sends a message
go sendMessage(messageChannel)
// Step 3: Receive the message from the channel
message := <-messageChannel
fmt.Println(message)
}
Explanation:
make(chan string)
: This creates a channel of typestring
.ch <- "Hello from Goroutine!"
: Sends data into the channel. The goroutine is pushing the string into the channel.message := <-messageChannel
: The main function receives the message from the channel and stores it in themessage
variable.Finally, it prints the message.
Output:
Hello from Goroutine!
3. Buffered vs. Unbuffered Channels
Channels in Go can either be unbuffered or buffered. The main difference between them is how they handle data flow.
Unbuffered Channels: These channels do not have storage. When one goroutine sends data, it waits until another goroutine receives it. The sending and receiving happen synchronously.
Buffered Channels: These channels have a buffer, meaning they can hold a certain number of messages before they block. The sender doesn’t block until the buffer is full, and the receiver doesn’t block until the buffer is empty.
Example: Unbuffered Channel
package main
import (
"fmt"
)
func main() {
// Unbuffered channel
ch := make(chan string)
// Start a goroutine to send data
go func() {
ch <- "Hello from Goroutine!"
}()
// Main function waits to receive data
msg := <-ch
fmt.Println(msg)
}
Explanation:
- Unbuffered channel: The channel
ch
will not hold any data. Themain
function will block atmsg := <-ch
until the goroutine sends a message.
Output:
Hello from Goroutine!
Example: Buffered Channel
package main
import (
"fmt"
)
func main() {
// Buffered channel with a capacity of 2
ch := make(chan string, 2)
// Send data into the buffered channel
ch <- "Message 1"
ch <- "Message 2"
// Main function receives data
msg1 := <-ch
msg2 := <-ch
fmt.Println(msg1)
fmt.Println(msg2)
}
Explanation:
make(chan string, 2)
: This creates a buffered channel with a capacity of 2. It can hold up to 2 messages before blocking.We send 2 messages into the channel and receive them one by one.
Output:
Message 1
Message 2
As you can see, with buffered channels, the main function does not block until the messages are received, and the sender can continue to send data without waiting.
4. Select Statement for Multiplexing Channels
The select
statement allows a Go program to wait on multiple channel operations. It’s like a "switch" for channels. If there are multiple channels ready, select
will choose one to execute. It’s used to handle situations where you want to perform operations on multiple channels concurrently.
Example: Using Select
package main
import (
"fmt"
"time"
)
func sendMessage(ch chan string, message string) {
time.Sleep(2 * time.Second)
ch <- message
}
func main() {
// Create two channels
ch1 := make(chan string)
ch2 := make(chan string)
// Start two goroutines
go sendMessage(ch1, "Hello from channel 1!")
go sendMessage(ch2, "Hello from channel 2!")
// Use select to wait for messages from either channel
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
}
}
Explanation:
select
: Waits for one of the channels to be ready for communication. It’s like a "choose one" for channels.In the program,
sendMessage()
sends messages intoch1
andch2
, andselect
picks one of them when it’s ready.
Output (varies based on which channel finishes first):
Received: Hello from channel 1!
Conclusion
In this article, we learned the basics of concurrency in Go, including:
Goroutines: Spawning concurrent tasks using the
go
keyword.Channels: Sending and receiving messages between goroutines.
Buffered and Unbuffered Channels: Understanding how data flow works with channels.
Select Statement: Multiplexing multiple channels and making decisions based on their state.
Concurrency is a powerful feature in Go, and mastering it will allow you to build highly efficient and concurrent programs. Keep practicing these concepts, and soon you’ll be comfortable writing concurrent Go programs!
Happy coding! 🚀
Subscribe to my newsletter
Read articles from Shivam Dubey directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
