The problem

A goroutine capturing a slice by value gets a snapshot at startup and never sees updates from the main thread.

The pattern

// Pass pointer so goroutine sees future updates
go s.schedulePolling(ctx, taskType, &s.locationEntities)
 
// Main thread replaces the slice periodically
s.locationEntities = locationEntitiesFromRefresh
 
// Inside the ticker loop — dereference each tick
entities := *entitiesPtr  // snapshot of current list
publishPollingMessages(entities)

Why it’s safe

A Go slice is a 24-byte header (pointer + length + capacity). Dereferencing copies the header, creating a snapshot pointing to the current underlying array.

Safety comes from complete replacement, not in-place mutation:

// ✅ New array allocated — old snapshot unaffected
s.locationEntities = []Entity{X, Y, Z}
 
// ❌ Race condition — modifies the same array the goroutine is reading
s.locationEntities[0] = newEntity

The old array lives until the goroutine’s current tick finishes. The next tick picks up the new pointer.