When Go structure nested structure pointer, the pointer structure must be initialized before it can be assigned a value

When I am coding, I met a error:
“runtime error: invalid memory address or nil pointer dereference”


It’s strange, encountering a “runtime error: invalid memory address or nil pointer dereference” usually means accessing memory pointed to by a nil pointer.

func (c *ThriftGeneric) Init(Conf *config.Config) error {
    // Waiting for server reflection
    p, err := generic.NewThriftFileProvider(Conf.IDLPath)
    if err != nil {
        return err
    }

    fmt.Println(Conf)

    c.Provider = p
    // The error occurs here due to nil pointer
    c.Conf = Conf

    g, err := generic.JSONThriftGeneric(p)
    if err != nil {
        return err
    }
    c.Generic = g

    if err := c.BuildClientOptions(); err != nil {
        return err
    }

    cli, err := genericclient.NewClient(Conf.Service, c.Generic, c.ClientOpts...)
    if err != nil {
        return err
    }
    c.Client = cli
    return nil
}

Why does it throw an error here? It’s because c.Conf is nil, even though it was working fine before. Later, I found out that I had refactored the code once:

Originally:

type ThriftGeneric struct {
    Client      genericclient.Client
    Generic     generic.Generic
    Conf        *config.Config
    ClientOpts  []client.Option
    CallOptions []callopt.Option
    Resp        interface{}
    Provider generic.DescriptorProvider
}

func NewThriftGeneric() Client {
    return &ThriftGeneric{}
}

After refactoring:

type GenericClientBase struct {
    Client      genericclient.Client
    Generic     generic.Generic
    Conf        *config.Config
    ClientOpts  []client.Option
    CallOptions []callopt.Option
    Resp        interface{}
}

type ThriftGeneric struct {
    *GenericClientBase
    Provider generic.DescriptorProvider
}

func NewThriftGeneric() Client {
    // Upon inspection, something seemed off here
    // I realized GenericClientBase wasn't initialized...
    return &ThriftGeneric{}
}

After the modification, everything worked fine again. Since c.GenericClientBase.Conf and c.Conf are the same, directly assigning the latter without any change is sufficient.

func NewThriftGeneric() Client {
    return &ThriftGeneric{
        GenericClientBase: &GenericClientBase{},
    }
}

But this is also correct:

type ThriftGeneric struct {
    GenericClientBase
    Provider generic.DescriptorProvider
}

func NewThriftGeneric() Client {
    return &ThriftGeneric{}
}

This approach directly nests the GenericClientBase structure instead of using pointers. This way, when a ThriftGeneric instance is created, the initialization of GenericClientBase occurs automatically without additional steps.

My question is, how does the compiler handle the above two situations? If both are used, will the final memory layout of ThriftGeneric be the same? Which one is more recommended?

I think it’s not because c.Conf is nil, but because c is nil. assigning a value to a nil pointer doesn’t cause panic, but accessing a field of nil pointer to struct does. Check this out.


When you create a struct by doing StructName{}, any field that you don’t define inside the {...} block will be assigned with zero value. Zero value of int is 0, zero value of pointer is nil, zero value for struct is a struct with all its field assigned zero value.

So, in the code below:

type GenericClientBase struct {
    Client      genericclient.Client
    Generic     generic.Generic
    Conf        *config.Config
    ClientOpts  []client.Option
    CallOptions []callopt.Option
    Resp        interface{}
}

type ThriftGeneric struct {
    *GenericClientBase
    Provider generic.DescriptorProvider
}

func NewThriftGeneric() Client {
    // Upon inspection, something seemed off here
    // I realized GenericClientBase wasn't initialized...
    return &ThriftGeneric{}
}

The &ThriftGeneric{} part is equivalent to

&ThriftGeneric{
    GenericBaseClient: nil, 
    Provider: generic.DescriptorProvider{
        ... // all field in the generic.DescriptorProvider assigned with their zero value
    },
}

If you do this, the GenericBaseClient is not allocated yet, it points to nothing (nil). Accessing its field will cause a panic. Here is the proof: The Go Playground


In the second example:

func NewThriftGeneric() Client {
    return &ThriftGeneric{
        GenericClientBase: &GenericClientBase{},
    }
}

the GenericClientBase is allocated. So accessing its field doesn’t panic.


In the third example:

type ThriftGeneric struct {
    GenericClientBase
    Provider generic.DescriptorProvider
}

func NewThriftGeneric() Client {
    return &ThriftGeneric{}
}

this works because as you said GenericClientBase is initialized. the &ThriftGeneric{} code is basically equivalent to this code:

&ThriftGeneric{
    GenericClientBase: GenericClientBase{
        Client:      genericclient.Client{},.
        Generic:     generic.Generic{},
        Conf:        nil,
        ClientOpts:  nil,
        CallOptions: nil,
        Resp:        nil,
    },
    Provider: generic.DescriptorProvider{},
}

This works, because, well, the .GenericClientBase is allocated when you write &ThriftGeneric. That’s why accessing its field doesn’t panic. The address of GenericClientBase is the same as the address of ThriftGeneric, it’s already allocated.


The memory layout between the second and third are different.

  • The second example, the memory layout of ThriftGeneric consist of: 8 byte (the size of pointer) + whatever the size of generic.DescriptorProvider + some padding if necessary.
  • The third example, the memory layout of ThriftGeneric consist of: the size of GenericClientBase + whatever the size of generic.DescriptorProvider + some padding if necessary.

Which one is more recommended?

It doesn’t matter that much. I usually go with the second one (embedding the struct), because it is less likely causing bug. But, really, it doesn’t matter if you use pointer as long as you do it correctly. Using pointer might also create another performance issue with the GC and memory allocation, but it’s so small that you won’t even notice it. Especially when your ThriftGeneric is only allocated once. If your ThriftGeneric is allocated a million time, it does make a difference.