Async to Goroutines
Async to Goroutines
Introduction
In this lesson, you'll learn about async to goroutines in Go. Coming from C#, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C#, you're familiar with async to goroutines.
Go has its own approach to async to goroutines, which we'll explore step by step.
The Go Way
Let's see how Go handles this concept. Here's a typical example:
package main
import (
"fmt"
"sync"
)
func fetch(url string, wg *sync.WaitGroup, results chan<- string) {
defer wg.Done()
// ... do HTTP request ...
results <- "result from " + url
}
func main() {
var wg sync.WaitGroup
results := make(chan string, 2)
wg.Add(2)
go fetch("https://api1.example.com", &wg, results)
go fetch("https://api2.example.com", &wg, results)
wg.Wait()
close(results)
for r := range results {
fmt.Println(r)
}
}Comparing to C#
Here's how you might have written similar code in C#:
// C# async/await — cooperative concurrency
async Task<string> FetchAsync(string url) {
return await httpClient.GetStringAsync(url);
}
// Run concurrently
var t1 = FetchAsync("https://api1.example.com");
var t2 = FetchAsync("https://api2.example.com");
var results = await Task.WhenAll(t1, t2);You may be used to different syntax or behavior.
Go goroutines are lightweight threads launched with the 'go' keyword — not async/await
You may be used to different syntax or behavior.
Channels (chan) pass data between goroutines — typed message passing
You may be used to different syntax or behavior.
sync.WaitGroup replaces Task.WhenAll for waiting on multiple goroutines
You may be used to different syntax or behavior.
C# async/await is cooperative (single-threaded by default); goroutines are truly parallel
You may be used to different syntax or behavior.
Go's concurrency model is CSP (Communicating Sequential Processes) — channels over shared memory
Step-by-Step Breakdown
1. Goroutines — go keyword
A goroutine is a lightweight managed thread. Launch any function call as a goroutine with the 'go' keyword. The main function does not wait for goroutines to finish.
// C# — fire-and-forget task
Task.Run(() => DoWork());
// Or async call
await DoWorkAsync();// Launch goroutine — fire and forget
go doWork()
// With an anonymous function
go func() {
fmt.Println("running in goroutine")
}()
// main() must wait or goroutine is killed with it
// Use WaitGroup or channel to synchronize2. Channels for Communication
Channels are typed conduits for passing data between goroutines. Send with <- and receive with <-. They replace shared mutable state.
// C# — shared variable with lock
private string _result;
private readonly object _lock = new();
// ... write from task, read after await ...ch := make(chan string) // unbuffered channel
// Sender goroutine
go func() {
ch <- "hello" // blocks until receiver is ready
}()
// Receiver (main goroutine)
msg := <-ch // blocks until sender sends
fmt.Println(msg)3. Buffered Channels and select
Buffered channels allow sending without an immediate receiver. select chooses from multiple channel operations — like a switch for channels.
// C# — Task.WhenAny
var completed = await Task.WhenAny(task1, task2);ch1 := make(chan string)
ch2 := make(chan string)
// select — wait on whichever is ready first
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}4. sync.WaitGroup and Mutex
WaitGroup waits for a set of goroutines to finish (like Task.WhenAll). Mutex protects shared data when channels are not practical.
var tasks = new[] { Task.Run(Work1), Task.Run(Work2) };
await Task.WhenAll(tasks);var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done() // signal done when goroutine exits
fmt.Println("worker", id)
}(i)
}
wg.Wait() // blocks until all goroutines call Done()
// Mutex for shared state
var mu sync.Mutex
mu.Lock()
sharedCounter++
mu.Unlock()Common Mistakes
When coming from C#, developers often make these mistakes:
- Go goroutines are lightweight threads launched with the 'go' keyword — not async/await
- Channels (chan) pass data between goroutines — typed message passing
- sync.WaitGroup replaces Task.WhenAll for waiting on multiple goroutines
Key Takeaways
- 'go func()' launches a goroutine — cheaper than threads, no async keyword needed
- Channels (chan) pass typed values between goroutines safely
- sync.WaitGroup replaces Task.WhenAll for join points
- Goroutines are truly parallel; C# async/await is single-threaded cooperative by default