Introduction
My business partner Ed asked me what would happen if a struct and an embedded field both implemented the same interface. We asked ourselves two questions:
- Would the compiler throw an error because we now had two implementations of the interface?
- If the compiler accepted the type declaration, how does the compiler determine which implementation to use for interface calls?
Methods
Go has both functions and methods. In Go, a method is a function that is declared with a receiver. A receiver is a value or a pointer of a named or struct type. All the methods for a given type belong to the type’s method set.
Let’s declare a struct type and a method for that type:
Name string
Email string
}
func (u User) Notify() error
First we declare a struct type named User and then we declare a method named Notify with a receiver that accepts a value of type User. To call the Notify method we need a value or pointer of type User:
// with a value receiver.
bill := User{"Bill", "bill@email.com"}
bill.Notify()
// Pointer of type User can also be used to call a method
// with a value receiver.
jill := &User{"Jill", "jill@email.com"}
jill.Notify()
In the case where we are using a pointer, Go adjusts and dereferences the pointer so the call can be made. Be aware that when the receiver is not a pointer, the method is operating against a copy of the receiver value.
We can change the Notify method to use a pointer for the receiver:
Once again, we can call the Notify method like we did before:
// with a pointer receiver.
bill := User{"Bill", "bill@email.com"}
bill.Notify()
// Pointer of type User can be used to call the method
// with a pointer receiver.
jill := &User{"Jill", "jill@email.com"}
jill.Notify()
If you are unsure about when to use a value or a pointer for the receiver, the Go wiki has a great set of rules that you can follow. The Go wiki also contains a paragraph about the conventions the community follows for naming receivers.
Interfaces
Interfaces in Go are special and provide an incredible amount of flexibility and abstraction for our programs. They are a way of specifying that values and pointers of a particular type can behave in a specific way. From a language perspective, an interface is a type that specifies a method set and all the methods for an interface type are considered to be the interface.
Let’s declare an interface in Go:
Notify() error
}
Here we declare an interface called Notifier with a single method called Notify. It is a convention in Go to name interfaces with an -er suffix when the interface contains only one method. This is not a hard rule but something we should honor, especially when the interface and method name have the same signature and meaning.
We can specify as many methods as we want for our interfaces. In the standard library you will be hard pressed to find many interfaces with more than two methods.
Implementing Interfaces
Go is unique when it comes to how we implement the interfaces we want our types to support. Go does not require us to explicitly state that our types implement an interface. If every method that belongs to an interface’s method set is implemented by our type, then our type is said to implement the interface.
Let’s continue with our example by creating a function that accepts any value or pointer of a type that implements the Notifier interface:
return notify.Notify()
}
The SendNotification function calls the Notify method that is implemented by the value or pointer that is passed into the function. This function can be used to execute the specific behavior for any value or pointer of a given type that implements the interface.
Let’s implement the Notify method for our User type and call the SendNotification function passing a value of type User:
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
func main() {
user := User{
Name: "janet jones",
Email: "janet@email.com",
}
SendNotification(user)
}
// Output:
cannot use user (type User) as type Notifier in function argument:
User does not implement Notifier (Notify method has pointer receiver)
http://play.golang.org/p/VieiPRDGVu
Why does the compiler not consider our value to be of a type that implements the interface? The rules for determining interface compliance are based on the receiver for those methods and how the interface call is being made. Here are the rules in the spec for how the compiler determines if the value or pointer for our type implements the interface:
- The method set of the corresponding pointer type *T is the set of all methods with receiver *T or T
- The method set of any other type T consists of all methods with receiver type T.
Since those are the only two rules in the spec for interface compliance, I have derived this rule that applies to our example:
- The method set of the corresponding type T does not consists of any methods with receiver type *T.
user := &User{
Name: "janet jones",
Email: "janet@email.com",
}
SendNotification(user)
}
// Output:
User: Sending User Email To janet jones<janet@email.com>
http://play.golang.org/p/3NNiS4dMrK
Embedded Types
Struct types have the ability to contain anonymous or embedded fields. This is also called embedding a type. When we embed a type into a struct, the name of the type acts as the field name for what is then an embedded field.
Let’s declare a new type and embed our User type into it:
User
Level string
}
We have declared a new type called Admin and embedded the User type within the struct declaration. This is not inheritance but composition. There is no relationship between the User and the Admin type.
Let’s change main to create a value of the Admin type and pass the address of this value to the SendNotification function:
admin := &Admin{
User: User{
Name: "john smith",
Email: "john@email.com",
},
Level: "super",
}
SendNotification(admin)
}
// Output
User: Sending User Email To john smith<john@email.com>
http://play.golang.org/p/2jZMCGEfxW
Sure enough, we are able to call the SendNotification function with a pointer of type Admin. Thanks to composition, the Admin type now implements the interface through the promotion of the methods from the embedded User type.
If the Admin type now contains the fields and methods of the User type, then where are they in relationship to the struct?
"When we embed a type, the methods of that type become methods of the outer type, but when they are invoked, the receiver of the method is the inner type, not the outer one." - Effective Go
Since the name of the embedded type acts as the field name and the embedded type exists as an inner type, we can then make the following method call:
// Output
User: Sending User Email To john smith<john@email.com>
http://play.golang.org/p/_huNeKVmXS
Here we are accessing the field and method set of the inner type through the use of the type’s name. However, these fields and methods are also promoted to the outer type:
// Output
User: Sending User Email To john smith<john@email.com>
http://play.golang.org/p/v4ro-KHiKJ
So calling the Notify method using the outer type, calls the implementation of the inner type’s method.
These are the rules for inner type method set promotion in Go:
Given a struct type S and a type named T, promoted methods are included in the method set of the struct as follows:
- If S contains an anonymous field T, the method sets of S and *S both include promoted methods with receiver T.
- The method set of *S also includes promoted methods with receiver *T.
- If S contains an anonymous field *T, the method sets of S and *S both include promoted methods with receiver T or *T.
Since those are the only three rules in the spec for method promotion, I have derived this rule for one other case:
- If S contains an anonymous field T, the method set of S does not include promoted methods with receiver *T.
Answering The Questions
Now we can finalize the sample program that will provide the answers for the two questions we asked in the beginning of the post. Let’s implement the Notifier interface for the Admin type:
log.Printf("Admin: Sending Admin Email To %s<%s>\n",
a.Name,
a.Email)
return nil
}
The implementation of the interface by the Admin type displays a message on behalf of an admin. This will help us determine which implementation gets called when we use a pointer of the Admin type to make the function call to SendNotification.
Now let’s create a value of the Admin type and pass the address of that value to the SendNotification function and see what happens:
admin := &Admin{
User: User{
Name: "john smith",
Email: "john@email.com",
},
Level: "super",
}
SendNotification(admin)
}
// Output
Admin: Sending Admin Email To john smith<john@email.com>
http://play.golang.org/p/NkDioPJs04
As expected, the Admin type’s implementation of the interface is called by the SendNotification function. So now what happens when we call the Notify method using the outer type:
// Output
Admin: Sending Admin Email To john smith<john@email.com>
http://play.golang.org/p/RG50rxC0d7
We get the output for the Admin type’s implementation. The User type’s implementation is no longer promoted to the outer type:
So now we have the knowledge we need to answer the questions:
- Would the compiler throw an error because we now had two implementations of the interface?
- If the compiler accepted the type declaration, how does the compiler determine which implementation to use for interface calls?
Conclusion
The way methods, interfaces, and embedded types work together is something that makes Go very unique. These features help us create powerful constructs to achieve the same ends as object oriented code without all the complexity. With the language features that we talked about in this post, we can build abstracted and scalable frameworks with a minimal amount of code and confusion.
The more I learn about the details of the language and the compiler, the more I come to appreciate how orthogonal the language is. Small features that work together and allow us to be creative and use the language in ways not even the language designers thought or dreamed about. I recommend to take the time to learn the language features so you can do more with less and be both creative and productive at the same time.