I am learning Go by writing a simple program that concurrently downloads sensor data files from a few http servers. The sensor data files on the servers are refreshed at regular intervals (30 seconds or 2 minutes, depends on the 'origin'). Downloading the data can take from 100ms to 10 seconds. So I read some configurations for each server (OriginContext). Then I start a controller for each OriginContext. Each controller continuously fires a goroutine that does the download etc.
I stripped my code down to a minimal example that somehow/hopefully still shows my intentions. When I run it, there will be two controllers, but somehow when they fire the doStuffThatMayTakeLongTime() methods they all refer to the identical configuration.
So, how did I confuse scopes of variables and pointers in goroutines here?
I am very new to Go and also this is the first time I try to use a language that uses pointers. Well, my shy C/C++ attempts are more than a decade ago... so I assume my confusion is with reference/value/dereference, but I can't see it.
This is the code:
package main
import (
"log"
"time"
)
type OriginContext struct {
Origin string
Offset time.Duration
Interval time.Duration
}
type Controller struct {
originContext *OriginContext
}
func NewController(originContext *OriginContext) (w *Controller) {
log.Printf("Controller starting loop for origin %s.", originContext.Origin)
w = &Controller{originContext}
w.start()
return w
}
func (w *Controller) start() {
log.Println("start() of", w.originContext.Origin)
go func() {
time.Sleep(w.originContext.Offset)
ticker := time.NewTicker(w.originContext.Interval)
go w.doStuffThatMayTakeLongTime() // iteration zero
for {
select {
case <-ticker.C:
go w.doStuffThatMayTakeLongTime()
}
}
}()
}
func (w *Controller) doStuffThatMayTakeLongTime() {
log.Printf("%s doing stuff", w.originContext.Origin)
}
func main() {
contexts := []OriginContext{
{
Origin: "alpha",
Offset: 0 * time.Second,
Interval: 5 * time.Second,
},
{
Origin: "bravo",
Offset: 5 * time.Second,
Interval: 10 * time.Second,
},
}
for _, ctx := range contexts {
log.Printf("Starting Controller %s.", ctx.Origin)
_ = NewController(&ctx)
}
select {}
}
And this is some output:
2015/09/07 14:30:11 Starting Controller alpha.
2015/09/07 14:30:11 Controller starting loop for origin alpha.
2015/09/07 14:30:11 start() of alpha
2015/09/07 14:30:11 Starting Controller bravo.
2015/09/07 14:30:11 Controller starting loop for origin bravo.
2015/09/07 14:30:11 start() of bravo
2015/09/07 14:30:16 bravo doing stuff
2015/09/07 14:30:16 bravo doing stuff
2015/09/07 14:30:26 bravo doing stuff
2015/09/07 14:30:26 bravo doing stuff
There should be alpha and bravo doing stuff, but there is just bravo.
The problem is on these lines:
for _, ctx := range contexts {
log.Printf("Starting Controller %s.", ctx.Origin)
_ = NewController(&ctx)
}
The variable ctx
is reused on every iteration of the loop as described in the language specification. NewController
is passed address of this single variable on every iteration of loop. The program prints the last values stored in this variable (although that's not guaranteed, there's a race on the variable).
There are a few ways to fix this. One way is to change the code to:
for i := range contexts {
log.Printf("Starting Controller %s.", context[i].Origin)
_ = NewController(&context[i])
}
With this change, NewController is passed a pointer to the slice element instead of a pointer to variable in the function.
Another option is to declare a new variable inside the body of the loop:
for _, ctx := range contexts {
ctx := ctx // <-- add this line
log.Printf("Starting Controller %s.", ctx.Origin)
_ = NewController(&ctx)
}
This option allocates a ctx on every iteration through the loop while the first option does not.