Async programming in Go

Go support async programming using goroutines (lightweight threads) and channels. It does not have async/await keywords.
It passes message between goroutines using channels. The Go runtime scheduler multiplexes goroutines onto thread pool threads. When a goroutine blocks on I/O, another goroutine runs.
See also: Goroutine creation, Channels.

1. Goroutines — fire-and-forget concurrency

Prefix any function call with go to run it concurrently. main does not wait unless you use a WaitGroup, channel, or sync primitive.

package main

import (
    "fmt"
    "time"
)

func fetchUser() {
    time.Sleep(100 * time.Millisecond) // simulate network I/O
    fmt.Println("user fetched")
}

func main() {
    go fetchUser() // runs concurrently — like an async task
    fmt.Println("main continues")
    time.Sleep(200 * time.Millisecond) // wait so goroutine can finish
}
      

2. Channels — async message passing

Goroutines communicate by sending values on channels. An unbuffered channel blocks until both sender and receiver are ready — built-in synchronization.

package main

import "fmt"

func fetch(ch chan string) {
    ch <- "result from API" // send when done (blocks until main receives)
}

func main() {
    ch := make(chan string)

    go fetch(ch) // start async work

    result := <-ch // main waits for result — like await
    fmt.Println(result)
}
      

3. select — wait on multiple async operations

select waits on one or more channel operations — similar to waiting on multiple futures. Use with time.After for timeouts.

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(50 * time.Millisecond)
        ch1 <- "service-A"
    }()
    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "service-B"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("got", msg)
    case msg := <-ch2:
        fmt.Println("got", msg)
    case <-time.After(100 * time.Millisecond):
        fmt.Println("timeout")
    }
}
      

4. sync.WaitGroup — wait for many goroutines

When main must wait for several concurrent tasks to finish before exiting.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Println("task", id, "done")
        }(i)
    }

    wg.Wait() // block until all goroutines call Done()
    fmt.Println("all finished")
}
      

5. context — cancel async work

context.Context carries deadlines, cancellation signals, and request-scoped values across goroutines. Pass it into long-running or I/O functions so they can stop when the caller cancels.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context) {
    select {
    case <-time.After(2 * time.Second):
        fmt.Println("work finished")
    case <-ctx.Done():
        fmt.Println("cancelled:", ctx.Err())
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()

    go worker(ctx)
    time.Sleep(1 * time.Second)
}
      

Summary: Go achieves async-style concurrency with go + channels + select, not async/await. For HTTP servers, the standard library (net/http) handles each request in its own goroutine automatically.