"Go Language Bible" Study Notes Chapter 6 Method

"Go Language Bible" Study Notes Chapter 6 Method


table of Contents

  1. Method declaration
  2. Pointer-based method
  3. Expand types by embedding structures
  4. Method value and method expression
  5. Example: Bit array
  6. Package

Note: To study the notes of "Go Language Bible", click to download the PDF , it is recommended to read the book.
Go language learning notes for beginners, copy the contents of the book, the big guys do not spray when you read them, and you will summarize them into your own reading notes when you are familiar with it.


  1. Since the early 1990s, object-oriented programming (OOP) has become the programming paradigm that dominates the engineering and education circles, so almost all large-scale applied languages ​​afterwards include support for OOP, and the go language is no exception.
  2. Although there is no clear definition of OOP accepted by the public, from our understanding, an object is actually a simple value or a variable. This object will contain some methods, and a method is one and one Functions associated with special types. An object-oriented program uses methods to express its attributes and corresponding operations, so that users who use the object do not need to directly manipulate the object, but instead use methods to do these things.
  3. In the earlier chapters, we have used some methods provided by the standard library, such as the Seconds method of time.Duration:
    Insert picture description here
  4. And in section 2.5, we defined our own method, the String method of Celsius type:
    5.
  5. In this chapter, the first aspect of OOP programming, we will show you how to effectively define and use methods. We will cover the two key points of OOP programming, encapsulation and combination.

1. Method declaration

  1. When a function is declared, put a variable before its name, which is a method. This additional parameter will attach the function to this type, which is equivalent to defining an exclusive method for this type.
  2. Let's write an example of our first method, this example is under package geometry:
  3. gopl.io/ch6/geometry
    Insert picture description here
  4. The additional parameter p in the above code is called the receiver of the method. The legacy of the early object-oriented language calls a method called "sending a message to an object".
  5. In the Go language, we do not use this or self as the receiver like other languages; we can choose the name of the receiver arbitrarily. Since the name of the receiver is often used, it is a good idea to keep it consistent and short when passed between methods. The suggestion here is to use the first letter of its type, for example, the first letter p of Point is used here.
  6. In the process of method invocation, the receiver parameter usually appears before the method name. This is the same as the method declaration, where the receiver parameter comes before the method name. Here is an example:
    7.
  7. As you can see, the two function calls above are both Distance, but there is no conflict. The first Distance call actually uses the package-level function geometry.Distance, while the second uses the Point just declared, and calls the Point.Distance method declared under the Point class.
  8. This expression of p.Distance is called a selector, because it will select the appropriate Distance method corresponding to the object p to execute. The selector can also be used to select a field of type struct, such as pX. Since methods and fields are in the same namespace, if we declare an X method here, the compiler will report an error, because there will be ambiguities when calling pX (Annotation: This is really strange here).
  9. Because each type has its own method namespace, when we use the name Distance, different Distance calls point to the Distance method in different types. Let us define a Path type, this Path represents a collection of line segments, and also define a method called Distance for this Path.
    Insert picture description here
  10. Path is a named slice type, not a struct type like Point, but we can still define methods for it. In terms of being able to define methods for any type, Go is different from many other object-oriented languages. Therefore, in the Go language, it is very convenient for us to define some additional behaviors for some simple numbers, strings, slices, and maps. The method can be declared to any type, as long as it is not a pointer or an interface.
  11. The two Distance methods have different types. There is no relationship between the two methods, although the Distance method of Path will internally call the Point.Distance method to calculate the length of each line segment connecting adjacent points.
  12. Let's call a new method to calculate the circumference of a triangle:
    Insert picture description here
  13. In the above two method calls to the Distance name, the compiler will determine which function is called based on the method name and receiver. In the first example, the type in the path[i-1] array is Point, so the Point.Distance method is called; in the second example, the type of perim is Path, so Distance calls Path.Distance.
  14. For a given type, its internal methods must have a unique method name, but different types can have the same method name. For example, here, Point and Path both have methods named Distance; so we don’t need to Add the type name before the method name to eliminate ambiguity, such as PathDistance. Here we have seen some of the advantages of methods over functions: Method names can be short. This benefit will be magnified when we call outside the package, because we can use this short name, but can omit the package name. Here is an example:
    Insert picture description here
  15. Annotation: If we want to use the method to calculate the distance of perim, we also need to write the package name of the whole geometry and its function name, but because the variable Path defines a distance method that can be used directly, we can write perim directly. Distance(). It is equivalent to typing a lot of words less, the author should mean this. Because calling functions outside the package in Go need to bring the package name, which is still quite troublesome.

2. Pointer-based method

  1. When a function is called, every parameter value is copied. If a function needs to update a variable, or one of the parameters of the function is too large, we hope to avoid this default copy. In this case, we will Need to use pointers. Corresponding to the method we use to update the receiver's object, when the receiver variable itself is relatively large, we can use its pointer instead of the object to declare the method, as follows:
    Insert picture description here
  2. The name of this method is (*Point).ScaleBy. The parentheses here are required; without the parentheses, the expression might be interpreted as *(Point.ScaleBy).
  3. In real programs, it is generally agreed that if the Point class has a pointer as a receiver method, then all Point methods must have a pointer receiver, even those functions that do not need this pointer receiver. We broke this agreement here just to show the similarities and differences between the two methods.
  4. Only the type (Point) and the pointer to them (*Point) are the two receivers that may appear in the receiver declaration. In addition, in order to avoid ambiguity, when declaring a method, if a type name itself is a pointer, it is not allowed to appear in the receiver, such as the following example:
    Insert picture description here
  5. To call the pointer type method (*Point).ScaleBy, just provide a Point type pointer, as shown below.
    Insert picture description here
  6. Or like this:
    Insert picture description here
  7. Or like this:
    Insert picture description here
  8. However, the latter two methods are a bit clumsy. Fortunately, the go language itself can help us in such places. If the receiver p is a Point type variable, and its method requires a Point pointer as the receiver, we can use the following short notation:
    Insert picture description here
  9. The compiler will implicitly help us use &p to call the ScaleBy method. This shorthand method only applies to "variables", including fields in struct such as pX, and elements in array and slice such as perim[0]. We cannot call pointer methods through a receiver whose address cannot be obtained. For example, the memory address of a temporary variable cannot be obtained:
    Insert picture description here
  10. But we can use a receiver like *Point to call the Point method, because we can find this variable by address, just use the dereference symbol * to get the variable. The compiler will also implicitly insert the * operator here, so the following two ways of writing are equivalent:
    Insert picture description here
  11. Here are a few examples that may confuse you, so let's summarize: In every legal method call expression, that is, any of the following three cases is possible:
  12. No matter the actual parameters of the receiver are the same as the formal parameters of the receiver, for example, both are of type T or both are of type *T:
    Insert picture description here
  13. Or the receiver parameter is of type T, but the receiver actual parameter is of type *T. In this case, the compiler will implicitly take the address of the variable for us:
    Insert picture description here
  14. Or the receiver parameter is type *T, and the actual parameter is type T. The compiler will implicitly dereference us and get the actual variable pointed to by the pointer:
    Insert picture description here
  15. If all methods of type T use the T type itself as the receiver (instead of *T), then it is safe to copy an instance of this type; calling any of its methods will also produce a copy of the value. For example, this type of time.Duration will be all copied when its method is called, including when it is passed into the function as a parameter. But if a method uses a pointer as a receiver, you need to avoid copying it, because this may destroy the internal immutability of the type. For example, if you copy the bytes.Buffer object, it may cause the original object and the copied object to be just aliases, but in fact the objects they point to are the same. Immediately modifying the copied variable may have unexpected results.

1. Nil is also a legal receiver type

  1. Just as some functions allow nil pointers as parameters, methods can theoretically use nil pointers as their receivers, especially when nil is a valid zero value for the object, such as map or slice. In the following simple int linked list example, nil represents an empty linked list:
    Insert picture description here
  2. When you define a type that allows nil as a receiver value, it is necessary to point out the meaning of the nil variable in the comment before the type, as we did in the example above.
  3. The following is a part of the Values ​​type definition in the net/url package.
  4. net/url
    Insert picture description here
  5. This definition exposes a variable of the map type to the outside, and provides some methods to easily manipulate the map. The value field of this map is a slice of string, so this Values ​​is a multi-dimensional map. When the client uses this variable, it can use some of the operations inherent in map (make, slice, m[key], etc.), or use the operation methods provided here, or use both, both are possible:
  6. gopl.io/ch6/urlvalues
    Insert picture description here
  7. In the last call to Get, the behavior of the nil receiver is the behavior of an empty map. We can equivalently write this operation as Value(nil).Get("item"), but if you write nil.Get("item") directly, it will not be compiled, because the nil literal compiler cannot judge The type of preparation. So in contrast, the last call of m.Add will generate a panic because it tries to update an empty map.
  8. Since url.Values ​​is a map type and indirectly refers to its key/value pair, any update or deletion of elements in this map by url.Values.Add is visible to the caller. In fact, just like in ordinary functions, although the internal value can be manipulated by reference, the original value will not be affected when the method wants to modify the reference itself, such as setting it to nil, or making this reference point to another The caller will not be affected. (Annotation: Because the variable that stores the memory address is passed in, you can change this variable without affecting the original variable. Think about the C language, it's almost the same)

3. Expand the type by embedding the structure

  1. Take a look at this type of ColoredPoint:
  2. gopl.io/ch6/coloredpoint
    Insert picture description here
  3. We can completely define ColoredPoint as a struct with three fields, but we embed the Point type in ColoredPoint to provide X and Y fields. As we saw in Section 4.4, inlining allows us to get a syntactic shorthand when defining ColoredPoint, and to include all the fields of the Point type, and then define some of our own. If we want, we can directly think that the embedded field is the field of ColoredPoint itself, and there is no need to specify the Point when calling, such as the following.
    4.
  4. We have a similar usage for the methods in Point. We can use the ColoredPoint type as a receiver to call the methods in Point, even if these methods are not declared in ColoredPoint:
    Insert picture description here
  5. The methods of the Point class have also been introduced into ColoredPoint. In this way, embedding allows us to define complex types with particularly many fields. We can group fields by small types first, then define methods for small types, and then combine them.
  6. Readers who are familiar with class-based object-oriented languages ​​may tend to regard Point as a base class, and ColoredPoint as a subclass or inherited class, or to regard ColoredPoint as an "is a" Point type. But this is a wrong understanding. Please note the call to the Distance method in the above example. One of the parameters of Distance is the Point type, but q is not a Point class, so even though q has the built-in type of Point, we must also explicitly select it. If you try to pass q directly, you will see the following error:
    Insert picture description here
  7. A ColoredPoint is not a Point, but it "has a" Point, and it has the Distance and ScaleBy methods introduced from the Point class. If you like to consider the problem from the perspective of implementation, the embedded field will instruct the compiler to generate additional wrapper methods to delegate to the declared method, which is equivalent to the following form:
    Insert picture description here
  8. When Point.Distance is called by the first wrapper method, its receiver value is p.Point, not p. Of course, in the methods of the Point class, you can't access any fields of ColoredPoint.
  9. The anonymous field embedded in the type may also be a pointer to a named type, in which case the fields and methods will be indirectly introduced into the current type. (Annotation: Access needs to be obtained through the object pointed to by the pointer). Adding this level of indirect relationship allows us to share a common structure and dynamically change the relationship between objects. The following declaration of ColoredPoint embeds a *Point pointer.
    Insert picture description here
  10. A struct type may also have multiple anonymous fields. We define ColoredPoint as follows:
    11.
  11. Then this type of value will have all the methods of the Point and RGBA types, as well as the methods directly defined in ColoredPoint. When the compiler parses a selector to a method, such as p.ScaleBy, it will first find the ScaleBy method directly defined in this type, then find the method introduced by the embedded fields of ColoredPoint, and then find Point and RGBA The method introduced by the embedded field, and then it has been recursively looking down. If the selector is ambiguous, the compiler will report an error. For example, you have two methods with the same name in the same level.
  12. Methods can only be defined on named types (like Point) or pointers to types, but thanks to inlining, sometimes we have the means to define methods for anonymous struct types.
  13. Below is a little trick. This example shows a simple cache, which is implemented using two package-level variables, a mutex (§9.2) and the cache it operates on:
    Insert picture description here
  14. The following version is functionally consistent, but the variables of the two package-level bars are placed in the cache struct group:
    Insert picture description here
  15. We gave the new variable a more expressive name: cache. Because the sync.Mutex field is also embedded in this struct, its Lock and Unlock methods are also introduced into this anonymous structure, which allows us to lock and unlock it with a simple and clear syntax.

4. Method values ​​and method expressions

  1. We often choose a method and execute it in the same expression, such as the common p.Distance() form. In fact, it is possible to divide it into two steps to execute. p.Distance is called "selector", and the selector returns a method "value" -> a function that binds the method (Point.Distance) to a specific receiver variable. This function can be called without specifying its receiver; that is, you don't need to specify the receiver when you call it, just pass in the function's parameters:
    Insert picture description here
  2. If the API of a package requires a function value, and the caller wants to operate a method bound to an object, the method "value" will be very useful (=_=really bypass). For example, the function of time.AfterFunc in the following example is to execute a function after the specified delay time. And this function operates on a Rocket object r
    3.
  3. It can be shorter if you pass the method "value" directly to AfterFunc:
    Insert picture description here
  4. Annotation: The anonymous function in the above example is omitted.
  5. Related to method "value" is method expression. When calling a method, compared with calling an ordinary function, we must use selector (p.Distance) syntax to specify the receiver of the method.
  6. When T is a type, the method expression may be written as Tf or (*T).f, which will return a function "value". This function uses its first parameter as a receiver, so the usual ( Annotation: Do not write the selector) to call it:
    Insert picture description here
  7. When you decide which function of the same type to call based on a variable, method expressions are very useful. You can call different methods of the receiver according to your choice. In the following example, the variable op represents the addition or subtraction method of the Point type, and the Path.TranslateBy method will call the corresponding method for each Point in the Path array:
    Insert picture description here

5. Example: Bit array

  1. Sets in Go language are generally expressed in the form of map[T]bool, and T represents the element type. Although it is very flexible to represent the collection with the map type, we can represent it in a better form. For example, in the field of data flow analysis, the collection element is usually a non-negative integer, the collection will contain many elements, and the collection will often perform union and intersection operations. In this case, the bit array will perform better than the map. (Annotation: Here is another example. For example, when we execute an http download task, we divide the file into many blocks according to a 16kb block. A global variable is needed to identify which blocks have been downloaded. In this case, the bit array is also used)

  2. A bit array is usually represented by an unsigned number or slice or called a "word", and each bit of each element represents a value in the set. When the i-th bit of the set is set, we say that this set contains element i. The following program shows a simple bit array type, and implements three functions to operate on this bit array:

  3. gopl.io/ch6/intset
    Insert picture description here

  4. Because each word has 64 binary bits, in order to locate the bit of x, we use the quotient of x/64 as the subscript of the word, and use the value obtained by x%64 as the position of the bit in the word . In the UnionWith method, the bit-bit "or" logical operation symbol | is used to complete the OR calculation of 64 elements at a time. (In Exercise 6.5, we will also use this 64-bit word example in the program.)

  5. The current implementation still lacks many necessary features. We will list some of them as exercises after this section. But if there is a method that is missing, our bit array may be more difficult to mix: print IntSet as a string. Here we implement it, let us add a String method to the above example, similar to what we did in section 2.5:

    // String方法以字符串"{1 2 3}"的形式返回集中
    func (s *IntSet) String() string {
          
          
    	var buf bytes.Buffer
    	buf.WriteByte('{')
    	for i, word := range s.words {
          
          
    		if word == 0 {
          
          
    			continue
    		}
    		for j := 0; j < 64; j++ {
          
          
    			if word&(1<<uint(j)) != 0 {
          
          
    				if buf.Len() > len("{") {
          
          
    					buf.WriteByte(' ')
    				}
    				fmt.Fprintf(&buf, "%d", 64*i+j)
    			}
    		}
    	}
    	buf.WriteByte('}')
    	return buf.String()
    }
    
  6. Here pay attention to the String method, is it very similar to the intsToString method in Section 3.5.4; bytes.Buffer is often used in the String method. When you define a String method for a complex type, the fmt package will treat the value of this type specially, so that these types can look more friendly when printed, instead of printing the original value directly. fmt will directly call the user-defined String method. This mechanism relies on interfaces and type assertions, which we will introduce in detail in Chapter 7.

  7. Now we can directly use the IntSet defined above in actual combat:
    Insert picture description here

  8. Note here: The String and Has two methods we declare both use the pointer type *IntSet as the receiver, but in fact, for these two types, it is not necessary to declare the receiver as a pointer type. But this is not the case for the other two functions, because the other two functions operate on the s.words object. If you don't declare the receiver as a pointer object, then the actual operation is to copy the object instead of the original object. Therefore, because our String method is defined on the IntSet pointer, when our variable is of the IntSet type instead of the IntSet pointer, there may be the following unexpected situations:
    Insert picture description here

  9. In the first Println, we print a pointer to *IntSet, this type of pointer does have a custom String method. In the second Println, we directly call the String() method of the x variable; in this case, the compiler will implicitly insert the & operator before x, so that we still call the String method of the IntSet pointer quite far away. In the third Println, because the IntSet type does not have a String method, the Println method will directly understand and print in the original way. So in this case, the ampersand must not be forgotten. In our scenario, it might be more appropriate to bind the String method to the IntSet object instead of the IntSet pointer, but this also requires specific analysis of specific issues.


6. Packaging

  1. If a variable or method of an object is invisible to the caller, it is generally defined as "encapsulation". Encapsulation is sometimes called information hiding, and it is also the most critical aspect of object-oriented programming.
  2. The Go language has only one means of controlling visibility: identifiers with uppercase first letters are exported from the package in which they are defined, not with lowercase letters. This way of restricting members in a package also applies to structs or methods of a type. So if we want to encapsulate an object, we must define it as a struct.
  3. This is why IntSet is defined as a struct type in the previous section, although it has only one field:
    Insert picture description here
  4. Of course, we can also define IntSet as a slice type, although in this way we need to replace the s.words used in all methods in the code with *s:
    Insert picture description here
  5. Although this version of IntSet is essentially the same, it can also allow other packages to read and edit the slice directly. In other words, the relative expression of *s will appear in all packages, and s.words only needs to appear in the package that defines IntSet (Annotation: So the latter is recommended).
  6. This name-based approach makes the smallest packaging unit in the language package, rather than the type like other languages. A field of struct type has visibility to all code in the same package, no matter whether your code is written in a function or a method.
  7. Encapsulation provides three advantages. First of all, because the caller cannot directly modify the variable value of the object, it only needs to pay attention to a small number of statements and only need to understand the possible values ​​of a small number of variables.
  8. Second, hiding the implementation details can prevent the caller from relying on the specific implementations that may change, so that the programmer who designs the package can get more freedom without destroying the external API.
  9. Consider the bytes.Buffer type as an example. This type is very commonly used when superimposing short strings, so some pre-optimization can be done when designing, such as reserving some space in advance to avoid repeated memory allocation. And because Buffer is a struct type, these extra spaces can be saved with additional byte arrays and placed in a field starting with a lowercase letter. In this way, the external caller can only see the performance improvement, but will not get this additional variable. Buffer and its growth algorithm are listed here, a little bit simplified for simplicity:
    Insert picture description here
  10. The third and most important advantage of encapsulation is that it prevents external callers from arbitrarily modifying the internal values ​​of the object. Because internal variables of an object can only be modified by functions in the same package, the author of the package can make these functions ensure the immutability of some values ​​inside the object. For example, the following Counter type allows the caller to increase the value of the counter variable, and allows the value to be reset to 0, but it is not allowed to set this value arbitrarily (Annotation: because it is not accessible at all):
    Insert picture description here
  11. Functions only used to access or modify internal variables are called setters or getters. Examples are as follows, such as some functions corresponding to the Logger type in the log package. When naming a getter method, we usually omit the preceding Get prefix. This preference for simplicity can also be extended to various types of prefixes such as Fetch, Find, or Lookup.
    Insert picture description here
  12. Go's coding style does not prohibit direct export of fields. Of course, once the export is carried out, there is no way to remove the export while ensuring API compatibility. Therefore, the initial choice must be carefully considered and the guarantee of some invariants within the package must be considered, and possible future changes , And whether the caller's code quality will deteriorate due to a little modification of the package.
  13. Packaging is not always ideal. Although encapsulation is necessary in some situations, sometimes we also need to expose some internal content, such as: time.Duration exposes its performance as an int64 number in nanoseconds, so that we can use general numerical operations to compare time , You can even define this type of constant:
    Insert picture description here
  14. Another example is to compare IntSet with geometry.Path at the beginning of this chapter. Path is defined as a slice type, which allows it to call the literal method of slice to iteratively traverse its internal points with range; at this point, IntSet has no way for you to do this.
  15. The two types are decisively different: The essence of geometry.Path is a sequence of coordinate points, no more and no less, we can foresee that no additional fields will be added to him in the future, so Path is exposed in the geometry package As a slice. In contrast, IntSet just uses a slice of []uint64 here. This type can also be represented by the []uint type, or we can even use other completely different things that take up smaller memory space to represent this collection, so we may also need additional fields to record the elements in this type Number. It is for these reasons that we make IntSet transparent to the caller.
  16. In this chapter, we learned how to combine methods with named types and how to call these methods. Although methods are essential to OOP programming, they are only half the sky in OOP programming. In order to complete OOP, we also need interfaces. Interfaces in Go will be introduced in the next chapter.

Guess you like

Origin blog.csdn.net/weixin_41910694/article/details/106165442