13 Usage Demystified of Structs and Their Methods

13 Usage Demystified of Structs and Their Methods #

We all know that a struct type represents a tangible data structure. A struct type can contain several fields, each of which typically needs to have a specific name and type.

Introduction: Basic Knowledge of Struct Types #

Of course, a struct type can also have no fields. This doesn’t mean it’s meaningless, because we can associate some methods with the type. Here, you can think of methods as a special version of functions.

Functions are independent program entities. We can declare functions with names, or without names, and we can treat them as ordinary values that can be passed around. We can abstract functions with the same signature into independent function types, which represent a set of inputs and outputs (or a class of logical components).

Methods are different. They need to have names and cannot be treated as values. Most importantly, they must belong to a specific type. The type to which a method belongs is indicated by the receiver declaration in its declaration.

The receiver declaration is the content enclosed in parentheses between the keyword func and the method name. It must include the exact name and type literals.

The type of the receiver is actually the type to which the current method belongs, and the name of the receiver is used to refer to the current value of the type it belongs to in the current method.

Let’s take an example to see.

// AnimalCategory represents the basic classification in animal taxonomy.
type AnimalCategory struct {
	kingdom string // 界。
	phylum  string // 门。
	class   string // 纲。
	order   string // 目。
	family  string // 科。
	genus   string // 属。
	species string // 种。
}

func (ac AnimalCategory) String() string {
	return fmt.Sprintf("%s%s%s%s%s%s%s",
		ac.kingdom, ac.phylum, ac.class, ac.order,
		ac.family, ac.genus, ac.species)
}

The struct type AnimalCategory represents the basic classification in animal taxonomy, with seven fields of type string representing each level of classification.

There is a method named String that can be seen from its receiver declaration that it belongs to the AnimalCategory type.

Through the receiver name ac of this method, we can refer to any field of the current value in it, or call any method of the current value (including the String method itself).

This String method’s function is to provide a string representation of the current value, and the levels of classification will be arranged in descending order. When using it, we can do it like this:

category := AnimalCategory{species: "cat"}
fmt.Printf("The animal category: %s\n", category)

Here, I initialized a value of type AnimalCategory using a literal and assigned it to the variable category. To avoid being overly wordy, I only specified the string value "cat" for the species field, which represents the most specific classification “species”.

In Go, we can customize the string representation of a type by writing a method named String for it. This String method does not need any parameter declaration, but it needs a result declaration of type string.

Because of this, when I call the fmt.Printf function, using the placeholder %s and the category value itself can print out the string representation of the latter without explicitly calling its String method.

fmt.Printf will find it automatically. At this time, the printed content will be The animal category: cat. Obviously, the String method of category successfully refers to all fields of the current value.

The type to which a method belongs is not limited to struct types, but it must be a custom data type and cannot be any interface type.

All the methods associated with a data type together constitute the method set of that type. Methods with the same name cannot appear in the same method set. Also, if they belong to a struct type, their names cannot be the same as any field name in that type.

We can regard a field in a struct type as one of its attributes or a piece of data, and regard a method belonging to it as an ability or operation attached to the data. Encapsulating attributes and their abilities (or data and their operations) together is a core principle of object-oriented programming.

Go language incorporates many excellent features of object-oriented programming and also recommends this approach of encapsulation. In this regard, Go language actually supports object-oriented programming, but it chooses to discard some features and rules that are prone to confusion for developers in practical application.

Now, let’s focus on the field declarations of struct types. Let’s look at the code below:

type Animal struct {
	scientificName	string // 学名。
	AnimalCategory	// 动物基本分类。
}

I declared a struct type called Animal. It has two fields. One is a field of type string named scientificName, which represents the scientific name of the animal. The other field declaration only contains AnimalCategory, which is the name of the struct type I wrote before. What does this mean?

So, our question today is: What does the field declaration AnimalCategory represent in the Animal type?

Broadly speaking, if a field declaration of a struct type contains only a type name, what does the field represent?

The typical answer to this question is: The field declaration AnimalCategory represents an embedded field of the Animal type. According to the Go language specification, if a field declaration only contains the type name of the field and no field name, it is an embedded field, also known as an anonymous field. We can refer to this field by appending a dot and the type of the embedded field after the variable name of this type. In other words, the type of the embedded field is both a type and a name.

Problem Analysis #

When it comes to embedded fields in a struct, the Animal type has a method called Category that is defined as follows:

func (a Animal) Category() string {
    return a.AnimalCategory.String()
}

The Category method has a receiver of type Animal and it is named a. In this method, I select the embedded field of a using the expression a.AnimalCategory, and then I call the String method of this field.

By the way, in Go language, an expression on the right side of an identifier that represents a variable, followed by a dot, and then followed by a field name or method name is called a selector expression. It is used to select a field or method of the variable.

This is what the Go language specification says and it is analogous to saying “referencing a field of a struct” or “calling a method of a struct”. I will use both of these terminologies interchangeably in the future.

In fact, embedding one struct type into another has more meanings than that. The method set of an embedded field is unconditionally merged into the method set of the enclosing type. For example, consider the following code:

animal := Animal{
    scientificName: "American Shorthair",
    AnimalCategory: category,
}
fmt.Printf("The animal: %s\n", animal)

Here, I declare a variable of type Animal named animal and initialize it. I assign the string value "American Shorthair" to its field scientificName and assign the variable category that was declared earlier to its embedded field AnimalCategory.

Later, I use the fmt.Printf function and the %s placeholder to attempt to print the string representation of animal, which is equivalent to calling the String method of animal. Although we have not yet written the String method for the Animal type, it is still valid. This is because in this case, the String method of the embedded field AnimalCategory is treated as a method call on animal.

Now, what if I also write a String method for the Animal type? Which one will be called?

The answer is that the String method of animal will be called. In this case, we say that the String method of the embedded field AnimalCategory has been “overridden”. Note that if the names are the same, regardless of whether the two methods have the same signature, the method of the enclosing type will always “override” the method of the embedded field.

Similarly, since we can directly access the fields of an embedded field just like we access the fields of the enclosing type, if there are two fields with the same name in these two struct types, then the field in the embedded field will definitely be “overridden”. This is similar to the “shadowing” phenomenon that may occur between variables with the same name that we mentioned earlier.

Because the fields and methods of the embedded field can be “grafted” onto the enclosing type, even in the case where one of the two members with the same name is a field and the other is a method, this “overriding” phenomenon still occurs.

However, even if it is overridden, we can still select the fields or methods of the embedded field using a chained selector expression, just like what I did in the Category method. This “overriding” actually brings some benefits. Let’s take a look at the implementation of the String method for the Animal type:

func (a Animal) String() string {
    return fmt.Sprintf("%s (category: %s)",
        a.scientificName, a.AnimalCategory)
}

Here, we incorporate the result of calling the String method of the embedded field into the result of the method with the same name of the Animal type. This technique of wrapping the result of the same-named method at each level is common and useful, and can be considered as a convention.

- (Embedded fields in a struct type)

Finally, I want to mention the issue of multiple levels of embedding. That is, the embedded field itself may also have embedded fields. Please take a look at the Cat type that I declared:

type Cat struct {
    name string
    Animal
}

func (cat Cat) String() string {
    return fmt.Sprintf("%s (category: %s, name: %q)",
        cat.scientificName, cat.Animal.AnimalCategory, cat.name)
}

The struct type Cat has an embedded field Animal, and the Animal type also has an embedded field AnimalCategory.

In this case, the “overriding” is based on the level of embedding, and the deeper the level of embedding, the more likely the field or method will be “overridden”.

For example, when we call the String method on a value of the Cat type, if this type indeed has a String method, then the String methods of the embedded fields Animal and AnimalCategory will both be “overridden”.

If this type does not have a String method, then the String method of the embedded field Animal will be called, and the String method of its embedded field AnimalCategory will still be “overridden”.

Only when both the Cat type and the Animal type do not have a String method, will the String method of AnimalCategory be called.

Finally, if multiple embedded fields with the same name exist at the same level, choosing this name from the value of the enclosing type will cause a compilation error because the compiler cannot determine which member is being selected.

All of the examples about embedded fields can be found in demo29.go, hope it helps you.

Knowledge Expansion #

Question 1: Does Go use embedded fields to implement inheritance?

Let me emphasize that Go does not have the concept of inheritance. What it does is to implement composition between types using embedded fields. The specific reasons and philosophy behind this can be found in the Why is there no type inheritance? section of the Go Language FAQ on the official Go website.

In simple terms, inheritance in object-oriented programming sacrifices code simplicity for extensibility, and this extensibility is achieved through an intrusive way.

Composition between types is achieved in a non-declarative manner. We do not need to explicitly declare that a certain type implements an interface or that one type inherits from another.

Moreover, type composition is non-intrusive. It does not break encapsulation or increase coupling between types.

All we need to do is to embed the type as a field and effortlessly enjoy all the capabilities and properties of the embedded field. If there are any issues with the embedded field, we can adjust and optimize them through “wrapping” or “shielding”.

Furthermore, type composition is flexible. We can easily “graft” the attributes and abilities of one type onto another type by using embedded fields.

In this way, the embedded type naturally implements the interfaces implemented by the embedded field. Additionally, composition is more concise and clear compared to inheritance. Go allows us to achieve powerful types by embedding multiple fields without the complexity of multiple inheritance and the associated management costs.

Composition between interface types is also possible. In Go, composition between interface types is even more common. We often use it to extend the behavior defined by an interface or to mark the characteristics of an interface. I will cover this in more detail in the next article.

Among the many Go engineers I have interviewed, many of them claim that “Go uses embedded fields to implement inheritance” and firmly believe it.

Either they are still viewing Go from the perspective and philosophy of other programming languages, or they have been misled by certain “Go tutorials”. Whenever this happens, I can’t help but correct them on the spot and suggest that they take a look at the answers on the official website.

Question 2: What do value methods and pointer methods mean, and what is the difference between them?

We all know that the receiver type of a method must be a custom data type and cannot be an interface type or a pointer type of an interface. The so-called value methods are methods where the receiver type is a non-pointer custom data type.

For example, all the methods we declared for the AnimalCategory, Animal, and Cat types in our previous discussion are value methods. Let’s take Cat as an example. The receiver type of its String method is Cat, a non-pointer type. So what does a pointer type mean? Please take a look at this method:

func (cat *Cat) SetName(name string) {
    cat.name = name
}

The receiver type of the SetName method is *Cat. Adding an asterisk * in front of Cat indicates the pointer type of Cat.

In this case, Cat can be referred to as the underlying type of *Cat. You can think of the value of this pointer type as representing a pointer to some underlying type value.

We can use a dereference operator * placed before such a pointer value to form a dereference expression, which retrieves the underlying type value that the pointer value points to. Alternatively, we can use an address operator & placed before an addressable underlying type value to form an address expression, which retrieves a pointer value of that underlying type value.

The so-called pointer methods are methods where the receiver type is the aforementioned pointer type.

So what are the differences between value methods and pointer methods? Here are the differences:

  1. For value methods, the receiver is a copy of the original value the method belongs to. Generally, any modifications to this copy within the method will not be reflected in the original value, unless the type itself is an alias type of a reference type (such as a slice or map).

    For pointer methods, the receiver is a copy of the pointer value to the underlying type value. Any modifications to the value pointed to by this copy within the method will always be reflected in the original value.

  2. The method set of a custom data type will only contain all of its value methods, while the method set of its pointer type includes all methods of the former, including value methods and pointer methods.

Strictly speaking, we can only call its value methods on a basic type value. However, Go language will automatically interpret it as calling its pointer methods, allowing us to call its pointer methods on such a value. #

For example, on a variable cat of type Cat, the reason why we can modify the name of the cat using cat.SetName("monster") is because Go language automatically interprets it as (&cat).SetName("monster"), that is: first take the pointer value of cat, then call the SetName method on that pointer value.

  1. Later you will learn that the methods in a type’s method set and the interface types it can implement are closely related. If a basic type and its pointer type have different method sets, then the number of interface types they can implement will also be different, unless both numbers are zero.

For example, although a pointer type can implement a certain interface type, its basic type may not necessarily be able to be used as an implementation type for that interface.

I have put a small example that demonstrates the difference between value methods and pointer methods in the file demo30.go. You can refer to that.

Summary

Embedded fields in a struct type can be confusing for Go language beginners, so in this article, I focused on explaining the writing method, basic characteristics and rules, as well as the deeper meaning of embedded fields. After understanding the composition and construction methods of struct types and their methods, these knowledge should be your key understanding.

An embedded field is a field in its declaration that has only a type and no name, and it can bring new properties and capabilities to the embedded type in a natural way. In general, we can directly refer to their fields and methods with simple selector expressions.

However, we need to be careful about the possibility of “shadowing”, especially when there are multiple embedded fields or multiple levels of embedding. The “shadowing” phenomenon may cause your actual reference to be different from your expectation.

Also, you must clearly understand the differences between value methods and pointer methods, including what these two methods can and cannot do, and what aspects of their parent types they affect. This involves value modification, method sets, and interface implementation.

Finally, I must emphasize again that embedded fields are a way to achieve composition between types and have nothing to do with inheritance. Although Go language supports object-oriented programming, it does not have the concept of “inheritance”.

Exercises

  1. Can we embed a pointer type of a certain type in a struct type? If so, what are the considerations?
  2. What does the literal struct{} represent? What is its use?

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