Engineering Blog

                            

Not being aware of the possible problem of type embedding

Concept of embedded field

A struct field without a name is known as embedded field. For example :-

type Employee struct {
    Address  <- this is called embedded field
}
type Address struct {
    City string
}

In the Employee struct, the Address type is declared without an associated name; hence, it’s an embedded field.

But this can sometimes lead to unexpected behaviors if we don’t understand all the implications of type embedding.

Note that city is available from two different paths: either from the promoted one using Employee.City or from the nominal one via Address, Employee.Address.City. Both relate to the same field.

Let’s see what’s wrong

Now that we’ve reminded ourselves what embedded types are, let’s look at an example of a wrong usage. In the following, we implement a struct that holds some in-memory data, and we want to protect it against concurrent accesses using a mutex:

type InMem struct {
    sync.Mutex
    m map[string]int
}
func New() *InMem {
    return &InMem{m: make(map[string]int)}
}

We decided to make the map unexported so that clients can’t interact with it directly but only via exported methods. Meanwhile, the mutex field is embedded. Therefore, we can implement a Get method this way:

func (i *InMem) Get(key string) (int, bool) {
    i.Lock()
    v, contains := i.m[key]
    i.Unlock()
    return v, contains
}

100 Go Mistakes and How to Avoid Them

Because the mutex is embedded, we can directly access the Lock and Unlock methods from the i receiver.We mentioned that such an example is a wrong usage of type embedding. What’s the reason for this? Since sync.Mutex is an embedded type, the Lock and Unlock methods will be promoted. Therefore, both methods become visible to external clients using InMem:

m := inmem.New()
m.Lock() // ?? This doesn't make sense 

This promotion is probably not desired. A mutex is, in most cases, something that we want to encapsulate within a struct and make invisible to external clients. Therefore, we shouldn’t make it an embedded field in this case.

What needs to be done

We want to write a custom logger that contains an io.WriteCloser and exposes two methods, Write and Close. If io.WriteCloser wasn’t embedded, we would need to write it like so:


type Logger struct {
    writeCloser io.WriteCloser
}

func (l Logger) Write(p []byte) (int, error) {
    return l.writeCloser.Write(p)
}

func (l Logger) Close() error {
    return l.writeCloser.Close()
}

func main() {
    l := Logger{writeCloser: os.Stdout}
    _, _ = l.Write([]byte("foo"))
    _ = l.Close()
}
Previous Post
Next Post