Go Basics 18 - Understand the nature of methods to choose the correct receiver type

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

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

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

T in the above method declaration is called the base type of receiver. Through receiver, the above method is bound to type T. In other words, the above method is a method of type T, which we can call through an instance of type T or *T, as shown in the pseudocode below:

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

Go methods have the following characteristics.

1) Whether the first letter of the method name is capitalized determines whether the method is an exported method.
2) Method definitions should be placed in the same package as type definitions. From this we can deduce: methods cannot be added to native types (such as int, float64, map, etc.), but methods can only be defined for custom types (sample 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))
}

In the same way, 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 multiple receiver parameter lists or variable-length receiver parameters are not supported. A method can only be bound to one base type, and the Go language does not support methods that bind multiple types at the same time.

4) The base type of the receiver parameter itself cannot be a pointer type or 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 only has one more receiver from function to method, which greatly lowers the threshold for Gopher to learn methods. Even so, Gopher still has confusion when grasping the essence of the method and choosing the type of receiver. This article will focus on these confusions.

the nature of method

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

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

When a C++ object calls a method, the compiler will automatically pass in the this pointer pointing to the object itself as the first parameter of the method. For Go, the same is true for receiver. We pass receiver as the first parameter into the parameter list of the method.

The method of type T in the above example can be equivalently converted into the following ordinary function:

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

This converted function is the prototype of the method. It's just that in the Go language, this equivalent conversion is automatically completed by the Go compiler when compiling and generating code. A new concept is provided in the Go language specification, which allows us to more fully understand the above equivalent conversion.
The general usage of Go methods is as follows:

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

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

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

This expression of directly calling a method with the type name T is called a method expression (Method Expression). Type T can only call methods in T's method set (Method Set). Similarly, T can only call methods in T's method set.

This way of calling a method through a method expression is exactly the same as the equivalent conversion from method to function we did before. This is what a Go method is: an ordinary function that takes as its first argument an instance of the type to which the method is bound.

The type of the Go method itself is an ordinary function, and we can even assign it as an rvalue to a variable of function type:

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))

Choose the correct receiver type

With the above analysis of the essence of Go methods, it is much simpler to understand receiver and choose the correct receiver type when defining methods. 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 that 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, select the receiver of value type

When T is selected as the receiver parameter type, the M1 method of T is equivalent to M1(t T). The parameters of the Go function are passed by value copy, which means that t in the M1 function body is a copy of the T type instance. In this way, any modification to the parameter t in the implementation of the M1 function will only affect the copy, not the copy. Affects the original T type instance.

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

When *T is selected 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 the T type instance, so that any modifications to the parameter t in the M2 function body will be reflected in the original T type instance.

The following example demonstrates the impact of selecting different receiver types on instances of the original type:

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

Run this program:

0
0
11

In this example, field a is modified in both the M1 and M2 method bodies, but M1 (using the value type receiver) only modifies a copy of the instance and has no effect on the original instance. Therefore, after M1 is called, the value of the output ta is still is 0. 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 this doubt: Can T type instances 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 a T type instance or a T type instance, you can call the method of the receiver being of type T, or you can call the method of the receiver being of type T.

The following example demonstrates this:

package main

type T struct {
    
    
	a int
}

func (t T) M1() {
    
    

}
func (t *T) M2() {
    
    
	t.a = 11
}
func main() {
    
    
	var t T
	t.M1() // ok
	t.M2() // <=> (&t).M2()
	var pt = &T{
    
    }
	pt.M1() // <=> (*pt).M1()
	pt.M2() // ok
}

We see that it is okay for a T type instance t to call the M2 method with a receiver type of T. Similarly, it is also okay for a T type instance pt to call the M1 method with a receiver type of T. In fact, this is all Go syntactic sugar. The Go compiler automatically converts it for us when compiling and generating code.

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

● 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 calling the Go method, the receiver is passed into the method in the form of value copy 如果类型的size较大,以值形式传入会导致较大损耗,这时选择*T作为receiver类型会更好些.

Cleverly solve difficult problems based on understanding the essence of Go methods

package main

import (
	"fmt"
	"time"
)

type field struct {
    
    
	name string
}

func (p *field) print() {
    
    
	fmt.Println(p.name)
}

func main() {
    
    

	data1 := []*field{
    
    {
    
    "one"}, {
    
    "two"}, {
    
    "three"}}

	for _, v := range data1 {
    
    
		go v.print()
	}
	data2 := []field{
    
    {
    
    "four"}, {
    
    "five"}, {
    
    "six"}}

	for _, v := range data2 {
    
    
		go v.print()
	}
	time.Sleep(3 * time.Second)
}

The running results are as follows (the results may be different due to different goroutine scheduling orders):

one
two
three
six
six
six

Why is the output result of data2 iteration 3 "six" instead of "four" "five" "six"?

Okay, let's analyze it. First, according to the essence of the Go method - an ordinary function with the instance of the type bound to the method as the first parameter, make an equivalent transformation to this program (here we use method expressions), the transformed source code is as follows:

type field struct {
    
    
name string
}
func (p *field) print() {
    
    
fmt.Println(p.name)
}
func main() {
    
    
data1 := []*field{
    
    {
    
    "one"}, {
    
    "two"}, {
    
    "three"}}
for _, v := range data1 {
    
    
go (*field).print(v)
}
data2 := []field{
    
    {
    
    "four"}, {
    
    "five"}, {
    
    "six"}}
for _, v := range data2 {
    
    
go (*field).print(&v)
}
time.Sleep(3 * time.Second)
}

Here we replace the call to the method print of the type field with the form of a method expression. The program output results before and after the replacement are consistent. After the change, do you feel suddenly enlightened? We can clearly see how parameters are bound when starting a new goroutine using the go keyword:

● When iterating data1, since the element type in data1 is a field pointer (*field), v is the element address after assignment. The parameter (v) passed in each time print is called is actually the address of each field element;

● When iterating data2, since the element type in data2 is field (not pointer), you need to get its address and then pass it in. In this way, the &v passed in each time is actually the address of the variable v, not the address of each element in the slice data2.

In Article 19, we learned about several key issues that should be paid attention to when using for range, including loop variable reuse. There is only one v here in the entire for range process, so after the data2 iteration is completed, v is a copy of the element "six".

In this way, once each started child goroutine is scheduled for execution when the main goroutine reaches Sleep, then when the last three goroutines print &v, they will print the value "six" stored in v. The first three sub-goroutines each pass in the address of the elements "one", "two" and "three", and what is printed is "one", "two" and "three".

So how to modify the original program so that it can output ("one", "two", "three", "four", "five", "six") as expected? In fact, you only need to change the receiver type of the field type print
method from *field to field.

The Go language does not provide grammatical support for the classic object-oriented mechanism, but it implements type methods. Methods and types are associated through the receiver on the left side of the method name. Choosing the appropriate receiver type for a type's method is an important part of Gopher's method definition for the type.

Guess you like

Origin blog.csdn.net/hai411741962/article/details/132811280