1

I created this simple app to demonstrate the issue I was having.

package main

import (
    "fmt"
    "unsafe"
    "sync"
)

type loc_t struct {
    count       [9999]int64
    Counter     int64
}

func (l loc_t) rampUp (wg *sync.WaitGroup) {
    defer wg.Done()
    l.Counter += 1
}

func main() {
    wg := new(sync.WaitGroup)
    loc := loc_t{}

    fmt.Println(unsafe.Sizeof(loc))
    wg.Add(1)
    go loc.rampUp(wg)
    wg.Wait()
    fmt.Println(loc.Counter)
}

If I run the above I will get a fatal error: newproc: function arguments too large for new goroutine runtime stack: runtime: unexpected return pc for runtime.systemstack called from 0x0

Now the reason for that is the 2k stack size when a go is used to spawn a background task. What's interesting is I'm only passing a pointer the called function. This issue happened to me in production, different struct obviously, everything was working for a year, and then all of sudden it started throwing this error.

icza
  • 389,944
  • 63
  • 907
  • 827
Nathan Thomas
  • 33
  • 1
  • 6
  • 1
    Yes, the method receiver is also passed just like any other parameters. Use a pointer receiver, and it'll be good. – icza Jun 12 '18 at 16:31

2 Answers2

4

Method receivers are passed to method calls, just like any other parameter. So if the method has a non-pointer receiver, the whole struct in your case will be copied. The easiest solution would be to use a pointer receiver, if you can.

If you must use a non-pointer receiver, then you can circumvent this by not launching the method call as the goroutine but another function, possibly a function literal:

go func() {
    loc.rampUp(wg)
}()

If the loc variable may be modified concurrently (before the launched goroutine would get scheduled and copy it for the rampUp() method), you can create a copy of it manually and use that in the goroutine, like this:

loc2 := loc
wg.Add(1)
go func() {
    loc2.rampUp(wg)
}()

These solutions work because launching the new goroutine does not require big initial stack, so the initial stack limit will not get in the way. And the stack size is dynamic, so after the launch it will grow as needed. Details can be read here: Does Go have an "infinite call stack" equivalent?

icza
  • 389,944
  • 63
  • 907
  • 827
1

The issue with the stack size is, obviously, the size of the struct itself. So as your struct grows organically, you may, as I did, cross that 2k stack call size.

The above problem can be fixed by using a pointer to the struct in the function declaration.

func (l *loc_t) rampUp (wg *sync.WaitGroup) {
    defer wg.Done()
    l.Counter += 1
}

This creates a pointer to the struct, so all that goes to the stack is the pointer, instead of an entire copy of the struct.

Obviously this can have other implications including race conditions if you're making the call in several threads at once. But as a solution to an ever growing struct that will suddenly start causing stack overflows, it's a solution.

Anyway, hope this is helpful to someone else out there.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Nathan Thomas
  • 33
  • 1
  • 6