I have the following data structure. It a chain of a struct that each has map[string]T. Basically I'm serializing a complex yaml file to a data structure. I have a two version that does work but one doesn't and it not clear for me why? Based on my understand Go compiler is very smart so it supposes to figure out where object needs to be allocated.
Please consider a code below.
type UserData struct {
Username string
Password string
}
type Groups struct {
users map[string] UserData
}
type Cluster struct {
Group map[string] Groups
}
type Director struct {
Cluster map[string]Cluster
}
//... I removed other add function.. Same semantic each add X function
// check if a map is nil call make(map[string]T) add a key)
// add a key with struct (where struct hold nil map)
func (c *Cluster) AddGroup(groupName string) Group {
if c.Group == nil {
c.Group = make(map[string]Groups)
}
c.Group[groupName] = Groups {}
group, _ := c.Group[groupName]
return group
}
func (p *Director) AddCluster(clusterName string) Cluster {
if p.Cluster == nil {
p.Cluster = make(map[string]Cluster)
}
p.Cluster[clusterName] = Cluster{}
cluster, _ := p.Cluster[clusterName]
return cluster
// does a compiler here allocate an object on the
// stack for Cluster {} and it goes out of a scope ?
}
if a caller does something like
var p = Director{}
cluster := p.AddCluster("Test Cluster")
cluster.AddGroup("Test Group")
It doesn't work -- First call does create a map and puts a value but the second call doesn't work. The key is never added to the second map.
In a version that does work, I created a constructor function that does(same semantic in each method). For example, version that does work below. (I use here the same semantic I usually do with C++ and other languages)
func NewCluster() *Cluster {
var cluster = Cluster{}
cluster.Group = make(map[string]Groups)
return &cluster
}
func (p *Director) AddCluster(clusterName string) {
if p.Cluster == nil {
p.Cluster = make(map[string]Cluster)
}
p.Cluster[clusterName] = *NewCluster()
}
I guess when you get used to one language compiler magic makes life harder :)
Right, the problem is that your AddXXX
funcs are returning copies/values, where they should be returning pionters if you want your code to work properly. You also have to change the return type of the AddGroup
func to Groups
(and possibly rename the func to reflect the actual type name). I'd also avoid using make
when a literal is shorter, and more idiomatic.
func (c *Cluster) AddGroup(groupName string) *Groups {
if c.Group == nil {
c.Group = map[string]*Groups{} // change type to use pointers
}
group := &Groups{} // create pointer
c.Group[groupName] = group // assign
return group // return
}
func (p *Director) AddCluster(clusterName string) *Cluster {
if p.Cluster == nil {
p.Cluster = map[string]*Cluster{} // change type
}
cluster := &Cluster{} // create ptr var
p.Cluster[clusterName] = cluster
return cluster
}
The problem you have has got nothing to do with where an object is allocated, but everything with what object you're working with. If you call AddCluster
, and aren't returning a pointer, then the call on the returned Cluster
object (AddGroup
), is going to add a group to a completely different object than the one in the map you've created. What you're doing is the C/C++ equivalent of something along these lines:
typedef _cluster_t struct {
int foo;
} cluster;
// and some calls like this:
int add_foo(cluster *c) {
c->foo = 123;
return c->foo;
}
int main ( void ) {
cluster cl;
int i = add_foo(&cl);
i++;
printf("I expect %d == %d
", i, cl.foo);
return 0;
}
Quite clearly, you're incrementing a copy of the int val assigned to cl.foo
. You're not updating both.
A pointer var can be allocated on the stack or heap, that's up to the runtime to decide. Because value vs pointer returns are semantically/functionally different, however. Should the compiler make assumptions based on whatever reason, we'd all be in a world of hurt. Consider the following code in a program that heavily uses concurrency:
type Foo struct {
SomeData map[string]Bar
}
func (f *Foo) AddBar(name string) Bar {
if f.SomeData == nil {
f.SomeData = map[string]Bar{} // this already is not thread-safe!
}
b := Bar{}
f.SomeData[name] = b
return b
}
If b
, the return value, would be "secretly" returned as a pointer, but another routine is also updating that same value, then you'd have a race condition. What's more, there are plenty of legitimate cases where you really don't want a pointer:
type Foo struct {
mu *sync.Mutex
data map[string]*Bar
}
// GetBar - should return a SNAPSHOT of whatever Bar looks like at this moment
func (f *Foo) GetBar(name string) (Bar, error) {
f.mu.Lock()
defer f.mu.Unlock()
if b, ok := f.data[name]; ok {
// return COPY because it's a snapshot!
return *b, nil
}
return Bar{}, ErrBarNotFound
}
TBH, in this kind of GetBar
function, I'd probably return a pointer, but return it like so:
if b, ok := f.data[name]; ok {
cpy := *b // create copy/snapshot
return &cpy, nil
}
// so in case of an error, I can return nil:
return nil, ErrBarNotFound