对级联保存映射的结构的空括号初始化

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.


Update (clarification):

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