Rack your brain to help you understand the essence of the method and choose the correct receiver type

Permalink to this article – https://ift.tt/pQlZaj0

Although the Go language does not support classic object-oriented syntax elements, such as: classes, objects, inheritance, etc., the Go language also has methods. Compared with functions, methods in Go language only have one more parameter in the declaration form, which Go calls the receiver parameter , and the receiver parameter is the link between the method and the type.

So what exactly is the Go method? What exactly does it have to do with functions? How should we choose the type of the receiver parameter? Is it a value type or a pointer type?

This article will help you deeply understand the essence of the Go method through the content of the book “The Road to Go Language Improvement: Programming Ideas, Methods and Skills from Novices to Experts” , and give the principles of receiver parameter type selection, so that you can not Confused again.

img{512x368}


1. What is a method in Go

The general declaration form of a Go method is as follows:

 func (receiver T/*T) MethodName(参数列表) (返回值列表) { // 方法体}

The T in the above method declaration is called the base type of the receiver. Through the receiver, the above method is bound to the type T. In other words: the above method is a method of type T, and we can call this method with an instance of type T or *T, as in the following pseudocode:

 var t T t.MethodName(参数列表) var pt *T = &t pt.MethodName(参数列表)

The Go method has the following characteristics:

1) Whether the first letter of the method name is capitalized determines whether the method is an export method;

2) The method definition should be placed in the same package as the type definition. From this we can infer that methods cannot be added for native types (such as int, float64, map, etc.), but only for custom types (example code is as follows).

 // 错误的做法func (i int) String() string { // 编译器错误:cannot define new methods on non-local type int return fmt.Sprintf("%d", i) } // 正确的做法type MyInt int func (i MyInt) String() string { return fmt.Sprintf("%d", int(i)) }

Similarly, it can be deduced that methods cannot be defined across Go packages for custom types in other packages.

3) Each method can only have one receiver parameter, and does not support multiple receiver parameter lists or variable-length receiver parameters. A method can only bind to one base type, and Go language does not support methods that bind multiple types at the same time.

4) The base type of the receiver parameter cannot itself be a pointer type or an interface type. The following example shows this:

 type MyInt *int func (r MyInt) String() string { // 编译器错误:invalid receiver type MyInt (MyInt is a pointer type) return fmt.Sprintf("%d", *(*int)(r)) } type MyReader io.Reader func (r MyReader) Read(p []byte) (int, error) { // 编译器错误:invalid receiver type MyReader (MyReader is an interface type) return r.Read(p) }

Compared with other mainstream programming languages, Go language has only one more receiver from functions to methods, which greatly reduces the threshold for Gophers to learn methods. But even so, Gophers are still confused when they grasp the essence of the method and how to choose the type of receiver. In this section, I will focus on these confusions.

2. The essence of the method

As mentioned earlier: Go language does not have classes, and methods and types are linked by receivers. We can define methods for any non-built-in primitive type, such as the following type T:

 type T struct { a int } func (t T) Get() int { return ta } func (t *T) Set(a int) int { ta = a return ta }

When a C++ object calls a method, the compiler will automatically pass in the this pointer to the object itself as the first parameter of the method. For Go, the same is true for the receiver. We pass the receiver as the first parameter to the parameter list of the method. The method of type T in the above example can be equivalently converted to the following ordinary function:

 func Get(t T) int { return ta } func Set(t *T, a int) int { ta = a return ta }

This transformed function is the prototype of the method . It’s just that in Go, this equivalence conversion is done automatically by the Go compiler when compiling and generating code. A new concept is provided in the Go language specification that allows us to understand the equivalence conversion above more fully.

The general usage of a Go method is as follows:

 var t T t.Get() t.Set(1)

We can replace the above method call with the following equivalent:

 var t T T.Get(t) (*T).Set(&t, 1)

This way of calling a method directly with the type name T is called a method expression . Type T can only call methods in the method set of T; similarly *T can only call methods in the method set of *T (for the method set, we will explain in detail in the next section). We see that the method expression is somewhat similar to the static method of the class in C++. The static method takes an object instance of the C++ class as the first parameter when used. When the method expression of Go language (Method Expression) is used, it also takes the instance represented by the receiver parameter as the first parameter.

This way of calling a method via a method expression is exactly the same as the method-to-function equivalence conversion we did earlier. This is the essence of a Go method: an ordinary function that takes as its first argument an instance of the type to which the method is bound .

A method expression embodies the essence of a Go method: its own type is an ordinary function. We can even assign it as an rvalue to a variable of type function:

 var t T f1 := (*T).Set // f1的类型,也是T类型Set方法的原型:func (t *T, int)int f2 := T.Get // f2的类型,也是T类型Get方法的原型:func(t T)int f1(&t, 3) fmt.Println(f2(t))

3. Correctly select the receiver parameter type

With the above analysis of the essence of Go methods, it is much simpler to understand the receiver and choose the correct receiver type when defining the method. Let’s take a look at the “equivalent transformation formulas” for methods and functions:

 func (t T) M1() <=> M1(t T) func (t *T) M2() <=> M2(t *T)

We see: the receiver parameter type of the M1 method is T, and the receiver parameter type of the M2 method is *T.

1) When the type of the receiver parameter is T, the receiver of the value type is selected

When we choose T as the receiver parameter type, the M1 method of T is equivalent to M1(t T). We know that the parameters of the Go function are passed by value copy, that is to say, the t in the M1 function body is a copy of the T type instance, so that any modification to the parameter t in the implementation of the M1 function will only affect the copy, and Will not affect the original T type instance.

2) When the type of the receiver parameter is *T, the receiver of the pointer type is selected

When we choose *T as the receiver parameter type, the M2 method of T is equivalent to M2(t *T). The t we pass to the M2 function is the address of an instance of type T, so that any modification to the parameter t in the body of the M2 function will be reflected in the original instance of type T.

We use the following example to demonstrate the impact of choosing different receiver types on instances of the original type:

 // chapter4/sources/method_nature_1.go type T struct { a int } func (t T) M1() { ta = 10 } func (t *T) M2() { ta = 11 } func main() { var t T // ta = 0 println(ta) t.M1() println(ta) t.M2() println(ta) }

Run the program:

 $ go run method_nature_1.go 0 0 11

In this example, both M1 and M2 methods have modified the field a, but M1 (using the value type receiver) only modifies the copy of the instance, and has no effect on the original instance, so after M1 is called, the value of the output ta is still is 0; and M2 (using pointer type receiver) modifies the instance itself, so after M2 is called, the value of ta becomes 11.

Many Go beginners still have the following doubts: Can an instance of T type only call methods whose receiver is of type T, but cannot call methods whose receiver is of type *T? the answer is negative. Whether it is an instance of type T or an instance of type *T, you can call either the method whose receiver is of type T or the method whose receiver is of type *T. The following example demonstrates this:

 // chapter4/sources/method_nature_2.go package main type T struct { a int } func (t T) M1() { } func (t *T) M2() { ta = 11 } func main() { var t T t.M1() // ok t.M2() // <=> (&t).M2() var pt = &T{} pt.M1() // <=> (*pt).M1() pt.M2() // ok }

Through the example, we can see that it is no problem for the T type instance t to call the M2 method whose receiver type is *T, and it is also possible for the *T type instance pt to call the M1 method whose receiver type is T. In fact, this is all Go syntax sugar, and the Go compiler automatically does the conversion for us when compiling and generating code.

At this point, we can draw preliminary conclusions about the receiver type selection:

  • If you want to modify the type instance, select the *T type for the receiver;
  • If there is no need to modify the type instance, you can choose T type or *T type for the receiver; but considering that when the Go method is called, the receiver is passed into the method in the form of a value copy. If the size of the type is large, passing it in as a value will cause a large loss. In this case, it is better to select *T as the receiver type.

In fact, there is another important factor in the choice of the receiver type, that is, whether the type needs to implement a certain interface, we will continue to look down.

An innovation of the Go language is that the implementation relationship between custom types and interfaces is loosely coupled: if the method set of a custom type T is a superset of the method set of an interface type, then the type T is said to implement The interface, and the variable of type T can be assigned to the variable of the interface type, that is, the method set we say determines the interface implementation.

Method Set is an important concept in Go language. Methods are used when assigning values ​​to interface type variables, using structure embedding/interface embedding, type aliases and method expressions, etc. Collections, which act like “glue” to implicitly glue custom types and interfaces together.

To determine whether a custom type implements an interface type, we first need to identify the method set of the custom type and the method set of the interface type. But sometimes they are not so obvious, especially when there are struct embeddings, interface embeddings, and type aliases.

Here we implement a utility function that can easily output a method collection of a custom type or interface type.

 // chapter4/sources/method_set_utils.go func DumpMethodSet(i interface{}) { v := reflect.TypeOf(i) elemTyp := v.Elem() n := elemTyp.NumMethod() if n == 0 { fmt.Printf("%s's method set is empty!\n", elemTyp) return } fmt.Printf("%s's method set:\n", elemTyp) for j := 0; j < n; j++ { fmt.Println("-", elemTyp.Method(j).Name) } fmt.Printf("\n") }

Next, we use this utility function to output the method set of the interface type and custom type in the example at the beginning of this section:

 // chapter4/sources/method_set_2.go type Interface interface { M1() M2() } type T struct{} func (t T) M1() {} func (t *T) M2() {} func main() { var t T var pt *T DumpMethodSet(&t) DumpMethodSet(&pt) DumpMethodSet((*Interface)(nil)) }

Run the above code:

 $ go run method_set_2.go method_set_utils.go main.T's method set: - M1 *main.T's method set: - M1 - M2 main.Interface's method set: - M1 - M2

In the above output results, the respective method sets of T, *T and Interface are clear at a glance. We see that the method set of type T only contains M1, which cannot be a superset of the method set of Interface type, so this is the reason why the compiler in the opening example thinks that the variable t cannot be assigned to a variable of Interface type. In the output result, we also see that the method set of type *T is [M1, M2]. *T type does not directly implement M1, but M1 still appears in the method set of *T type. This is in line with the statement in the Go language specification: for a custom type T of non-interface type, its method set is composed of all methods whose receiver is of type T; and the method set of type *T includes all receivers of type T and *T. method. Because of this, pt can be successfully assigned to the Interface type variable.

At this point, we have completely clarified the third factor that needs to be considered when choosing a type for the receiver: whether to support assigning an instance of type T to an interface type variable. If we need support, we need to implement all the methods in the interface type method collection whose receiver is T type .

4. Summary

This article introduces the definition and usage of Go language methods in detail, and tells you the essence of Go methods and the three principles of receiver parameter type selection through examples. Keeping these three principles in mind, the receiver of the method will no longer be troubled. is you. If you want to learn more about the essence of Go language programming, I recommend you to read my new book “The Road to Go Language Improvement: Programming Ideas, Methods and Skills from Novices to Experts” .

The source code covered in this article can be downloaded here – https://ift.tt/NoL1s5G.

Gopher Daily Archive Repository – https://ift.tt/CZ9DzlF

my contact information:

  • Weibo: https://ift.tt/0vUj1Qi
  • WeChat public account: iamtonybai
  • Blog: tonybai.com
  • github: https://ift.tt/7eofT6z
  • “Gopher Tribe” Planet of Knowledge: https://ift.tt/iCrVKAd

Business cooperation methods: writing, publishing books, training, online courses, partnership entrepreneurship, consulting, advertising cooperation.

© 2022, bigwhite . All rights reserved.

This article is reprinted from https://tonybai.com/2022/05/17/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type/
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment