Backend Development 9 min read

Why Switch from PHP to Go? Mastering Concurrency with WaitGroup and ErrGroup

This article explains why backend developers are moving from PHP to Go, outlines the concurrency challenges of high‑traffic live streaming services, and demonstrates two practical Go patterns—sync.WaitGroup and errgroup.Group—with code examples, while also warning about common closure pitfalls in loops.

Raymond Ops
Raymond Ops
Raymond Ops
Why Switch from PHP to Go? Mastering Concurrency with WaitGroup and ErrGroup

1. Reasons to Choose Go

As a backend developer, I work most with PHP and Go. Although PHP feels comfortable and fast for simple tasks, new projects after 2021 are built with Go because:

PHP cannot meet our high‑concurrency needs – our live‑streaming service requires handling many simultaneous requests, which the traditional php‑fpm model cannot efficiently support.

Go is popular in the industry – major companies such as Tencent, Baidu, Didi and others have migrated from PHP to Go.

Go’s simplicity – the language is concise; after a couple of weeks of learning I could start writing projects.

2. How Go Solves Concurrency Problems

In PHP, fetching multiple pieces of data for a live‑room (version info, basic info, user info, equity info, statistics) must be done sequentially, causing the total latency to be the sum of all operations.

Serial request flow
Serial request flow

With Go, the request time is limited to the longest individual operation because the tasks run concurrently.

Concurrent request flow
Concurrent request flow

Two common Go patterns for this are:

Method 1: sync.WaitGroup

<code>// Request entry
func main() {
    var (
        VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail int
    )
    ctx := context.Background()
    GoNoErr(ctx,
        func() {
            VersionDetail = 1 // version service info
            time.Sleep(1 * time.Second)
            fmt.Println("Execute first task")
        },
        func() {
            LiveDetail = 2 // live basic info
            time.Sleep(2 * time.Second)
            fmt.Println("Execute second task")
        },
        func() {
            UserDetail = 3 // user info
            time.Sleep(3 * time.Second)
            fmt.Println("Execute third task")
        },
        func() {
            EquityDetail = 4 // live equity info
            time.Sleep(4 * time.Second)
            fmt.Println("Execute fourth task")
        },
        func() {
            StatisticsDetail = 5 // live room statistics
            time.Sleep(5 * time.Second)
            fmt.Println("Execute fifth task")
        },
    )
    fmt.Println(VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail)
}

// Concurrent method using WaitGroup
func GoNoErr(ctx context.Context, functions ...func()) {
    var wg sync.WaitGroup
    for _, f := range functions {
        wg.Add(1)
        // launch each function in a goroutine
        go func(function func()) {
            function()
            wg.Done()
        }(f)
    }
    // wait for all to finish
    wg.Wait()
}</code>

Method 2: errgroup.Group

<code>// Request entry
func main() {
    var (
        VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail int
        err error
    )
    ctx := context.Background()
    err = GoErr(ctx,
        func() error {
            VersionDetail = 1 // version service info
            time.Sleep(1 * time.Second)
            fmt.Println("Execute first task")
            return nil
        },
        func() error {
            LiveDetail = 2 // live basic info
            time.Sleep(2 * time.Second)
            fmt.Println("Execute second task")
            return nil
        },
        func() error {
            UserDetail = 3 // user info
            time.Sleep(3 * time.Second)
            fmt.Println("Execute third task")
            return nil
        },
        func() error {
            EquityDetail = 4 // live equity info
            time.Sleep(4 * time.Second)
            fmt.Println("Execute fourth task")
            return nil
        },
        func() error {
            StatisticsDetail = 5 // live room statistics
            time.Sleep(5 * time.Second)
            fmt.Println("Execute fifth task")
            return nil
        },
    )
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail)
}

// Concurrent method using errgroup
func GoErr(ctx context.Context, functions ...func() error) error {
    var eg errgroup.Group
    for i := range functions {
        f := functions[i] // capture loop variable
        eg.Go(func() (err error) {
            err = f()
            if err != nil {
                // log error if needed
            }
            return err
        })
    }
    // wait for all to finish
    return eg.Wait()
}</code>

Both approaches launch a goroutine for each sub‑task and wait for all to finish. When using closures inside a loop, be careful to capture the loop variable correctly; otherwise all goroutines may reference the same variable, leading to unexpected results. Creating a new variable inside the loop (e.g.,

fs := f

) avoids this issue.

These concurrency techniques are fundamental and widely applicable in Go projects.

Backend DevelopmentConcurrencyGoerrgroupWaitGroupPHP Migration
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.