I quite new to go. I need some help.
I guess I am facing a goroutine overhead. I was wondering if I can overcome the go-routine overhead. Here is my code.
package main
import (
"log"
"sync"
"time"
)
// This function will be replaced by receiving websocket messages.
func msg(i int) int {
return i
}
// This function will be replaced by an action, in response of the messeage received.
func action(i int, wg *sync.WaitGroup) {
defer wg.Done() // decrease the counter by 1 when the task is done.
log.Println(i, "th action start")
time.Sleep(500 * time.Millisecond)
log.Println(i, "th action end")
}
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
var wg sync.WaitGroup
// I want to remove this part, without facing the overhead.
// wg.Add(1)
// go action(-1, &wg)
// time.Sleep(1 * time.Second)
// I want to remove this part, without facing the overhead.
for i := 0; i < 10; i++ {
time.Sleep(100 * time.Millisecond)
msg := msg(i)
log.Println(msg, "th message received")
wg.Add(1) // increment the counter by 1
go action(msg, &wg)
}
wg.Wait()
}
You can see the result(namely, Result1) below. There is 0.05s gap between the 0th message received and 0th action start. So, I guess I am facing a go-routine overhead. Once I unblock the code
wg.Add(1)
go action(-1, &wg)
time.Sleep(1 * time.Second)
, you can see the result(namely, Result2) below. There is only 0.0003s gap between the 0th message received and 0th action start. But, I would like to remove this part, because it calls action unnecessarily. Any suggestions would be appreciated.
Result 1
2025/08/31 17:12:15.915057 0 th message received
2025/08/31 17:12:15.968358 0 th action start
2025/08/31 17:12:16.069100 1 th message received
2025/08/31 17:12:16.069100 1 th action start
2025/08/31 17:12:16.169469 2 th message received
2025/08/31 17:12:16.169469 2 th action start
2025/08/31 17:12:16.270079 3 th message received
2025/08/31 17:12:16.270079 3 th action start
...
Result 2
2025/08/31 17:15:36.098222 -1 th action start
2025/08/31 17:15:36.653260 -1 th action end
2025/08/31 17:15:37.199560 0 th message received
2025/08/31 17:15:37.199866 0 th action start
2025/08/31 17:15:37.300656 1 th message received
2025/08/31 17:15:37.300656 1 th action start
2025/08/31 17:15:37.401045 2 th message received
2025/08/31 17:15:37.401562 2 th action start
2025/08/31 17:15:37.501994 3 th message received
2025/08/31 17:15:37.502418 3 th action start
...
That ~50 ms gap before the first action is not real goroutine overhead; it is a cold start of the Go runtime.
The first go action(...) has to spin up an OS thread, initialize the scheduler, bring up timers and the netpoller, touch memory pages, etc.
Once that initialization is complete, new goroutines start almost instantly, so only the very first one shows the delay. Your go action(-1, …) just forces this warm-up earlier so the real work appears to start without lag.
To avoid that 50 ms delay without calling a fake action, you can keep a pool of worker goroutines alive that wait on a channel for work so the first message is picked up immediately. From your code, something like this:
package main
import (
"log"
"runtime"
"sync"
"time"
)
func action(i int) {
log.Println(i, "th action start")
time.Sleep(500 * time.Millisecond)
log.Println(i, "th action end")
}
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
workers := runtime.NumCPU() // or whatever
tasks := make(chan int)
var wg sync.WaitGroup
// Start workers so they stay hot and wait for tasks
for w := 0; w < workers; w++ {
go func() {
for i := range tasks {
action(i)
wg.Done()
}
}()
}
for i := 0; i < 10; i++ {
time.Sleep(100 * time.Millisecond)
log.Println(i, "th message received")
wg.Add(1)
tasks <- i
}
close(tasks) // workers exit
wg.Wait()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With