I tried to find this function in Go's standard library and many other cache libraries that resemble to Java's ConcurrentMap.computeIfAbsent. I found sync.Map in the standard library which looks like what I'm looking for. I’d like to use sync.Map as a concurrent map. The problem is that the following function does not provide a defer computation as Java's ConcurrentMap does.
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
LoadOrStore returns the existing value for the key if present. Otherwise, it stores and returns the given value. The loaded result is true if the value was loaded, false if stored.
It’s atomic, but the function doesn’t make sense to me. I don’t understand its purpose since I'm new to the language. I don’t know why the second param is a value not a function. Since the param will be evaluated eagerly, I have no idea how is this function useful when the value is recomputed every time anyway.
It would be much more useful IMO if the function signature is like
func (m *Map) LoadOrStore(key interface{}, f func() interface{}) (actual interface{}, loaded bool)
I mean why do we need a map when we have a value already? Pretty sure I miss something.
Let me elaborate that a bit. I don't get the reason why the function takes a value as parameter instead of a function and I'm pretty sure that I miss something. I want to get a value out of a map in a concurrent manner. When a key doesn't exist, I want to compute the value and put it into the map atomically. I know I shouldn't compare it with the ConcurrentMap in Java since the language and paradigm are different, but it could be useful to say where I'm from and it could benefit anyone who is learning Go to get a little bit of better understanding on this difference.
Here is the similar function in Java ConcurrentMap.
computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
If the specified key is not already associated with a value (or is mapped to null), attempts to compute its value using the given mapping function and enters it into this map unless null.
The question is why LoadOrStore
function takes a value instead of a function? Any insight on why the API is designed this way would be appreciated. Also Is there a way to accomplish the same thing as I do in Java computeIfAbsent
without using an explicit lock around the map?
Updated I found that it's very easy to modify the sync.Map.LoadOrStore to take a function instead of a value.
https://play.golang.org/p/VBIaS8ZV38o
Not sure that it will work as expected though.
The purpose of sync.Map
is not to provide a way to defer the value calculation, but to provide a general map[interface{}]interface{}
safe for concurrent use by multiple goroutines. The map
type in go is not safe for concurrent use without additional / explicit synchronization.
This is clearly stated in the doc of sync.Map
:
Map is like a Go map[interface{}]interface{} but is safe for concurrent use by multiple goroutines without additional locking or coordination. Loads, stores, and deletes run in amortized constant time.
The Map type is specialized. Most code should use a plain Go map instead, with separate locking or coordination, for better type safety and to make it easier to maintain other invariants along with the map content.
The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.
As to why sync.Map
has a
LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
method instead of:
LoadOrComputeAndStore(key interface{}, f func() interface{}) (actual interface{}, loaded bool)
That question is opinion based and therefore off-topic for Stackoverflow. But for one reason: the former is easier to use (you don't have to use a function literal when you already have the value) and the latter is not always needed (even though in some cases would be really useful).
Note that we can easily create our custom sync map that has this "feature":
type MyMap struct {
sync.Map
}
func (m *MyMap) LoadOrComputeAndStore(key interface{}, f func() interface{}) (actual interface{}, loaded bool) {
actual, loaded = m.Load(key)
if loaded {
return
}
return m.LoadOrStore(key, f())
}
Example using it:
m := &MyMap{Map: sync.Map{}}
f := func() interface{} {
fmt.Println("calculating...")
return "myvalue"
}
key := "mykey"
fmt.Println(m.LoadOrComputeAndStore(key, f))
fmt.Println(m.LoadOrComputeAndStore(key, f))
Output (try it on the Go Playground):
calculating...
myvalue false
myvalue true
As seen in the output, f()
is only called once. First call to m.LoadOrComputeAndStore()
calls f()
, and reports loaded=false
(as key
was not yet in the map). Second call to m.LoadOrComputeAndStore()
does not call f()
, and reports loaded=true
.
Note that this is an "easy and sufficient" way to achieve what you want. But it is not "bullet proof", meaning that the implementation of LoadOrComputeAndStore()
does not guarantee that f()
may only be called once. This is because it first loads the value using Map.Load()
. If it finds that it's not in the map, then it calls f()
, during which a concurrent goroutine may do the same. So f()
may be called multiple times if f()
takes long to return and the map is used by multiple goroutines. If guarantee is needed to avoid multiple f()
calls, then you should use an explicit sync.RWMutex
in the map.
So to sum it up: the above example implementation should be sufficient in most cases, namely when f()
does not have a side effect, and you only need LoadOrComputeAndStore()
to avoid having to call f()
due to its long execution time. If f()
does have a side effect which must not be repeated, then this implementation is insufficient and manual locking is required.