Why Switch from PHP to Go? Practical Concurrency Patterns for High‑Load Services

The article explains why a backend team moved from PHP to Go for high‑concurrency live‑streaming services and demonstrates two Go concurrency patterns—sync.WaitGroup and errgroup—with code examples and common pitfalls.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Why Switch from PHP to Go? Practical Concurrency Patterns for High‑Load Services

Reasons for Choosing Go

As a backend developer who previously used PHP extensively, the author found PHP comfortable but unsuitable for high‑concurrency live‑streaming workloads. The main reasons for migrating to Go were:

PHP cannot handle the required concurrency – the standard php‑fpm model creates a new process per request, which is too heavy for real‑time streaming.

Industry trend – major companies such as Tencent, Baidu, Didi, and others have been adopting Go.

Go’s simplicity – the language is easy to pick up, allowing the author to start writing production code after only a couple of weeks.

Concurrency Problem Solved by Go

In a typical PHP implementation, fetching various pieces of data for a live‑room (version info, basic room info, user info, equity info, statistics) is done sequentially, causing the total response time to equal the sum of all sub‑tasks. Go’s goroutine model enables these tasks to run in parallel, reducing the overall latency to the duration of the longest sub‑task.

Method 1: Using sync.WaitGroup

// request entry point
func main() {
    var (
        VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail int
    )
    ctx := context.Background()
    GoNoErr(ctx,
        func() { VersionDetail = 1; time.Sleep(1 * time.Second); fmt.Println("task 1") },
        func() { LiveDetail = 2; time.Sleep(2 * time.Second); fmt.Println("task 2") },
        func() { UserDetail = 3; time.Sleep(3 * time.Second); fmt.Println("task 3") },
        func() { EquityDetail = 4; time.Sleep(4 * time.Second); fmt.Println("task 4") },
        func() { StatisticsDetail = 5; time.Sleep(5 * time.Second); fmt.Println("task 5") },
    )
    fmt.Println(VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail)
}

func GoNoErr(ctx context.Context, functions ...func()) {
    var wg sync.WaitGroup
    for _, f := range functions {
        wg.Add(1)
        go func(fn func()) { fn(); wg.Done() }(f)
    }
    wg.Wait()
}

This approach launches a goroutine for each function and waits for all to finish.

Method 2: Using errgroup Library

// request entry point
func main() {
    var (
        VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail int
        err error
    )
    ctx := context.Background()
    err = GoErr(ctx,
        func() error { VersionDetail = 1; time.Sleep(1 * time.Second); fmt.Println("task 1"); return nil },
        func() error { LiveDetail = 2; time.Sleep(2 * time.Second); fmt.Println("task 2"); return nil },
        func() error { UserDetail = 3; time.Sleep(3 * time.Second); fmt.Println("task 3"); return nil },
        func() error { EquityDetail = 4; time.Sleep(4 * time.Second); fmt.Println("task 4"); return nil },
        func() error { StatisticsDetail = 5; time.Sleep(5 * time.Second); fmt.Println("task 5"); return nil },
    )
    if err != nil { fmt.Println(err); return }
    fmt.Println(VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail)
}

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() error { return f() })
    }
    return eg.Wait()
}

The errgroup variant also propagates the first error encountered, making it suitable for tasks where failure handling matters.

Common Pitfall with Closures

When launching goroutines inside a loop, capturing the loop variable directly leads to all goroutines referencing the same final value. Two safe patterns are shown:

Assign the loop variable to a new local variable before launching the goroutine (as in Method 2).

Use the index‑based loop ( for i := range functions) and capture functions[i].

Incorrect code that captures the loop variable without copying results in unexpected behavior, illustrated by screenshots in the original article.

Conclusion

Both sync.WaitGroup and errgroup provide straightforward ways to split a parent task into multiple concurrent subtasks in Go, dramatically reducing latency for high‑throughput services such as live streaming. Understanding closure capture rules is essential to avoid subtle bugs when spawning goroutines inside loops.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentconcurrencyGoerrgroupsync.WaitGroup
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

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.