14 Rational Use of Interface Types

14 Rational Use of Interface Types #

Hello, I am Haolin. Today, we are going to talk about the related content of interfaces.

Introduction: Basics of using interfaces #

In the context of Go language, when we talk about “interfaces”, we are referring to interface types. Unlike other data types, interface types cannot be instantiated.

Specifically, we cannot create a value of an interface type by calling the new function or using a literal representation.

For a given interface type, if there are no data types that can serve as its implementations, then a value of that interface cannot exist.

As I have shown before, we can declare interface types using the type and interface keywords.

The type literal of an interface type resembles the struct type, as they both use curly braces to encapsulate core information. The difference is that struct types encapsulate field declarations, while interface types encapsulate method definitions.

Here, it’s important to note that the methods declared in an interface type represent the method set of that interface. The method set of an interface is its complete set of features.

For any data type, if its method set fully includes all the features (i.e., all the methods) of an interface, then it is definitely an implementation type of that interface. For example:

type Pet interface {
    SetName(name string)
    Name() string
    Category() string
}

I declared an interface type called Pet, which includes 3 method definitions with the names SetName, Name, and Category. These 3 methods together form the method set of the interface type Pet.

If a data type’s method set includes all 3 methods, it is definitely an implementation type of the Pet interface. This is a non-intrusive way of implementing an interface. This approach is also known as “Duck typing”. You can read more about it on the Baidu Encyclopedia page (Chinese).

By the way, how do we determine if a method implementation of a data type matches a method in an interface type?

There are two necessary and sufficient conditions: the signatures of the two methods must be identical, and the names of the two methods must be exactly the same. Clearly, this is more strict than checking if a function matches a function type.

If you have read the final example provided in the previous article, then you would know that even though the struct type Cat is not an implementation type of the Pet interface, its pointer type *Cat is an implementation type.

If you are not sure why, let me explain. I have moved the declaration of the Cat type to the demo31.go file and simplified it for better clarity. By the way, since “Cat” and “Pet” sound too similar, I renamed Cat to Dog.

The type Dog I declared has 3 methods. Two of them are value methods, namely Name and Category, and the other one is a pointer method called SetName.

This means that the method set of the Dog type itself only includes 2 methods, that is, all the value methods. On the other hand, the method set of its pointer type *Dog includes all 3 methods, meaning it has all the value methods and pointer methods associated with the Dog type. Since these 3 methods are the implementations of a method in the Pet interface, the *Dog type becomes an implementation type of the Pet interface.

dog := Dog{"little pig"}
var pet Pet = &dog

For this reason, I can declare and initialize a variable of type Dog, then assign its pointer value to a variable of type Pet.

There are a few terms that you should remember. For a variable of an interface type, such as the variable pet mentioned above, the value assigned to it is called its concrete value (also known as dynamic value), and the type of that value is called its concrete type (also known as dynamic type).

For example, when we assign the result of the address expression &dog to the variable pet, the result value becomes the dynamic value of the variable pet, while its type, *Dog, becomes the dynamic type of that variable.

The term dynamic type is in contrast to the static type. For the variable pet, its static type is always Pet and will never change, but its dynamic type can change depending on the dynamic value we assign to it.

For instance, only when I assign a value of type *Dog to the variable pet, the dynamic type of that variable becomes *Dog. If there is another implementation type of the Pet interface called *Fish, and I assign a value of that type to pet, then the dynamic type of pet will become *Fish.

Furthermore, the dynamic type of an interface variable does not exist until we assign an actual value to it.

You need to understand what dynamic value, dynamic type, and static type mean for an interface variable. I will explain more in-depth knowledge based on these concepts in the following sections.

Okay, now I will ask a series of questions on the topic of “how to use Go interfaces effectively”. Please think about these questions along with me.

So, today’s question is: What happens when we assign a value to an interface variable?

To highlight the question, I simplified the declaration of the Pet interface.

type Pet interface {
    Name() string
    Category() string
}

I removed the SetName method from the Pet interface. This change makes the Dog type become an implementation type of the Pet interface. You can find the code for this question in the demo32.go file.

Now, I declare and initialize a variable of type Dog named dog, and its name field is set to "little pig". Then, I assign this variable to a variable of type Pet named pet. Finally, I change the name field of dog to "monster" by calling the SetName method.

dog := Dog{"little pig"}
var pet Pet = dog
dog.SetName("monster")

Therefore, the specific question I want to ask is: After the execution of the above code, what is the value of the name field in the variable pet?

The typical answer to this question is: The value of the name field in the variable pet is still "little pig".

Problem Analysis #

First of all, since the SetName method of dog is a pointer method, the receiver it holds is a copy of the pointer value pointing to dog, and therefore setting the name field of the receiver is a modification of the dog variable. So when dog.SetName("monster") is executed, the value of the name field in dog will definitely be "monster". If you understand this, be careful of the trap ahead.

Why did the name field value of dog change, but not that of pet? Here is a general rule you need to know: when we assign one variable to another variable, the latter is not assigned the exact value held by the former, but a copy of that value.

For example, if I declare and initialize a variable of Dog type, dog1, with a name of "little pig", and then I modify the name field value of dog1 after assigning dog1 to dog2, what is the value of the name field in dog2?

dog1 := Dog{"little pig"}
dog2 := dog1
dog1.name = "monster"

This question is almost the same as the previous one, except that it does not involve interface types. In this case, the name field of dog2 will still be "little pig". This is another demonstration of the general rule I just told you.

Once you know this general rule, you can indeed get the previous question right. However, if I ask you why, and you only give this reason, then I can only say that you have only answered half of it.

So what’s the other half? This requires us to talk about the storage method and structure of interface type values. As I mentioned earlier, an interface type itself cannot be valued. Before we give it an actual value, its value must be nil, which is its zero value.

Conversely, once it is assigned a value of some implementing type, its value is no longer nil. However, be aware that even if we assign the value of dog to pet as before, the value of pet is different from the value of dog. This is not just a difference between a copy and the original value.

When we assign a value to an interface variable, its dynamic type and dynamic value are stored together in a special data structure.

Strictly speaking, the value of such a variable is actually an instance of this special data structure, not the actual value we assign to it. That’s why I said that the value of pet is definitely different from the value of dog, both in terms of the content they store and the structure of the storage. However, we can think that the value of pet at this time contains a copy of the value of dog.

Let’s call this special data structure iface. In the Go language’s runtime package, it is actually called this name.

An instance of iface contains two pointers, one pointing to type information and the other pointing to dynamic value. The type information here is carried by an instance of another special data structure, which includes the type of the dynamic value and the methods that make it implement the interface and how to call them, and so on.

In short, when an interface variable is assigned a dynamic value, the stored value is a more complex value that contains a copy of the dynamic value. Do you understand?

Knowledge Expansion #

Question 1: Under what circumstances is the value of an interface variable really nil

At first glance, this question seems like a non-question. For a reference type variable, whether its value is nil depends entirely on what we assign to it, right? Let’s take a look at some code:

var dog1 *Dog
fmt.Println("The first dog is nil. [wrap1]")
dog2 := dog1
fmt.Println("The second dog is nil. [wrap1]")
var pet Pet = dog2
if pet == nil {
   fmt.Println("The pet is nil. [wrap1]")
} else {
   fmt.Println("The pet is not nil. [wrap1]")
}

In this code snippet in the demo33.go file, I first declare a variable dog1 of type *Dog and do not initialize it. What is the value of this variable? Obviously, it is nil. Then I assign this value to dog2, and the value of dog2 at this point will also be nil, right?

Now here’s the question: What will be the value of the variable pet when we assign dog2 to it? Is it nil?

If you truly understand the knowledge I explained in the previous question, especially the part about assigning values to interface variables and their underlying data structure, then this question is not difficult to answer. You can think about it first and then continue reading.

When we assign the value of dog2 to the variable pet, the value of dog2 will be copied first. However, since its value is nil in this case, there is no need to make a copy.

Then, Go will wrap the copy of the value of dog2 with an instance of the special data structure called iface, which is nil in this case.

Although the wrapped dynamic value is nil, the value of pet will not be nil, because the dynamic value is only a part of the value of pet.

By the way, at this point, the dynamic type of pet exists and is *Dog. We can verify this with the fmt.Printf function and the %T placeholder, or the TypeOf function in the reflect package can also accomplish the same.

Looking at it from another perspective, we assign nil to pet, but the value of pet is not nil.

This seems strange, doesn’t it? Actually, it is not. In Go, we call the value represented by the nil literal an untyped nil. This is the real nil, because its type is also nil. Although the value of dog2 is the true nil, when we assign this variable to pet, Go considers its type and value together.

That is to say, at this point, Go recognizes that the value assigned to pet is a *Dog type nil. Then, Go will wrap it with an instance of iface, and the result of the wrapping will definitely not be nil.

As long as we assign a typed nil to an interface variable, the value of this variable will definitely not be the true nil. Therefore, when we use the equality operator == to check whether pet is equal to the literal nil, the answer will definitely be false.

So, how can we make the value of an interface variable truly nil? Either only declare it without initialization, or directly assign the literal nil to it.

Question 2: How to implement interface composition?

Interface composition, also known as embedding interfaces, is simpler than embedding fields between struct types because it does not involve method “shadowing”. If there are conflicting methods with the same name between the composed interfaces, compilation will fail, even if the signatures of the conflicting methods are different. Therefore, interface composition cannot lead to the occurrence of “shadowing” phenomenon. The embedding of interface types is very similar to that of struct types. We just need to directly write the name of one interface type into the member list of another interface type. For example:

type Animal interface {
    ScientificName() string
    Category() string
}

type Pet interface {
    Animal
    Name() string
}

The interface type Pet contains two members, one is the Animal representing another interface type and the other is the definition of the Name method. They are both included in the curly braces of the type declaration of Pet and each occupies a separate line. At this point, all the methods contained in the Animal interface become methods of the Pet interface.

The Go language team encourages us to declare smaller interface types and recommends us to use this composition of interface types to expand programs and increase program flexibility.

This is because compared to large interfaces with many methods, small interfaces can express a certain ability or class of characteristics more focused, and are also easier to be combined together.

An example of this is the ReadWriteCloser interface and the ReadWriter interface in the Go language standard library package io. They are both composed of several small interfaces. Taking the io.ReadWriteCloser interface as an example, it is composed of the io.Reader, io.Writer, and io.Closer interfaces.

All three interfaces only contain one method, which is a typical small interface. Each of them represents only one ability, namely reading, writing, and closing. It is usually easy to implement these small interface types. And once we implement them at the same time, it means that we have implemented their combined interface io.ReadWriteCloser.

Even if we only implement io.Reader and io.Writer, it is equivalent to implementing the io.ReadWriter interface, because the latter is composed of the former two interfaces. As you can see, the interfaces in the io package together form an interface matrix. They are both interrelated and independent.

I wrote a small example in the demo34.go file that demonstrates the advantages of interface composition. You can take a look. In short, using interface composition and small interfaces can make your program framework more stable and flexible.

Summary

Okay, let’s summarize briefly.

Interfaces in Go are commonly used to represent certain abilities or classes of characteristics. First, we need to understand what the dynamic value, dynamic type, and static type of an interface variable represent. These are the basis for using interface variables correctly. When we assign a value to an interface variable, the interface variable will hold a copy of the value, not the value itself.

More importantly, the value of an interface variable is not the same as the copy of the value, which can be called the dynamic value. It will contain two pointers, one pointing to the dynamic value and one pointing to the type information.

Based on this, even if we assign a variable of a certain implementation type with a value of nil to an interface variable, the value of the interface variable cannot be truly nil. Although its dynamic value will be nil in this case, its dynamic type does exist.

Please remember that unless we only declare without initializing, or explicitly assign it with nil, the value of an interface variable will not be nil.

The next question is relatively easy, it is about program design. It is always beneficial to use small interfaces and interface composition, as we can form an interface matrix and build a flexible program framework. If we also use the embedding technique between struct types when implementing interfaces, then interface composition can play a greater role.

Thinking question

If we assign a variable of a certain implementation type with a value of nil to an interface variable, can we still call the methods of that interface on this interface variable? If so, what are the considerations? If not, why not?

Click here to view the detailed code for the Go language column article.