C#
GO

C# to Go

10 lessons

Progress0%
1Introduction2Type System3Classes to Structs4Interfaces5Error Handling6Async to Goroutines7Generics8LINQ to Slices9Testing10Packages and Modules
All Mirror Courses
C#
GO
Async to Goroutines
MirrorLesson 6 of 10
Lesson 6

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.

Mirror Card
C#
From C#:

In C#, you're familiar with async to goroutines.

GO
In Go:

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:

GO
Go 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#
C# (What you know)
// 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);
Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

Go goroutines are lightweight threads launched with the 'go' keyword — not async/await

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

Channels (chan) pass data between goroutines — typed message passing

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

sync.WaitGroup replaces Task.WhenAll for waiting on multiple goroutines

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

C# async/await is cooperative (single-threaded by default); goroutines are truly parallel

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

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#
C#
// C# — fire-and-forget task
Task.Run(() => DoWork());

// Or async call
await DoWorkAsync();
GO
Go
// 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 synchronize
Common Pitfall
If main() returns, all goroutines are killed immediately — even if they have not finished. Always use WaitGroup or a channel to wait for goroutines.

2. Channels for Communication

Channels are typed conduits for passing data between goroutines. Send with <- and receive with <-. They replace shared mutable state.

C#
C#
// C# — shared variable with lock
private string _result;
private readonly object _lock = new();
// ... write from task, read after await ...
GO
Go
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)
Rule of Thumb
"Do not communicate by sharing memory; share memory by communicating." — Go Proverb. Prefer channels over mutexes.

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#
C#
// C# — Task.WhenAny
var completed = await Task.WhenAny(task1, task2);
GO
Go
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.

C#
C#
var tasks = new[] { Task.Run(Work1), Task.Run(Work2) };
await Task.WhenAll(tasks);
GO
Go
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()
Rule of Thumb
Use WaitGroup to wait for goroutines, channels to communicate between them, and Mutex only when you truly need shared mutable state.

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
Common Pitfall
Don't assume Go works exactly like C#. While the concepts may be similar, the syntax and behavior can differ significantly.

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
Rule of Thumb
The best way to learn is by doing. Try rewriting some of your C# code in Go to practice these concepts.
PreviousNext