Introduction
A faced a bug, that is so simple, that it took me sometime to find it. Let’s start with the problem, and explain what was happening.
Problem Description
Let’s take a look at this code ( I knew it’s not well written, but it’s a good minimal example)
type S struct {
Field1 int
}
type X struct {
S *S
}
type Y struct {
sm map[string]*S
}
func main() {
key := "k"
s := &S{Field1: 10}
x := X{S: s}
y := Y{sm: map[string]*S{
key: s,
}}
x.S.Field1 = 20
fmt.Printf("Changing x reflected in y map: %d\n", y.sm[key]) // prints 20
fmt.Println("Caching x struct...")
c := &Cache{}
xJSON, err := json.Marshal(x)
if err != nil {
panic(err)
}
c.Put(key, xJSON)
y.sm[key].Field1 = -100
fmt.Printf("Field in y map: %d\n", y.sm[key].Field1) // prints -100
fmt.Printf("Field in x (live without saving to cache): %d\n", x.S.Field1) // prints -100
v, ok := c.Get(key)
if !ok {
panic("not found")
}
var xFromCache *X
err = json.Unmarshal(v, &xFromCache)
if err != nil {
panic(err)
}
fmt.Println("Reading x from cache...")
fmt.Printf("After reading from cache: %d\n", xFromCache.S.Field1) // prints 20
}
type Entry struct {
key string
value []byte
}
type Cache struct {
mu sync.Mutex
entries []Entry
}
func (c *Cache) Put(key string, value []byte) {
c.mu.Lock()
defer c.mu.Unlock()
c.entries = append(c.entries, Entry{
key: key,
value: value,
})
}
func (c *Cache) Get(key string) ([]byte, bool) {
c.mu.Lock()
defer c.mu.Unlock()
for _, entry := range c.entries {
if entry.key == key {
return entry.value, true
}
}
return []byte{}, false
}
The problem here that after we changed the values we didn’t save the changes to the cache. If we save the Y struct to the cache, the changes will be reflected only for the Y struct itself, but not for the X struct. so when they are stored in the cache, the changes made to the Y struct will not be reflected in the X struct.
Conclusion
As easy as it is, but it’s really important to understand the implications of pointer references and caching. Always ensure that changes are saved to the cache to avoid unexpected behavior. Specially, when the code is big, and caching behavoir is somewhat hidden after abstractions.