Golang Learning Path 7: Object-Oriented Method

Posted by Jewbilee on Tue, 02 Jul 2019 23:58:08 +0200

Golang Learning: Object-Oriented Method

Definition of Method

Struct is more flexible than function. It encapsulates properties and operations. The former is the field in the structure type, while the latter is the method owned by the structure type.
The parenthesis between the key word func and the name Grow and its contents are the recipient declaration. It consists of two parts. The first part is the identifier representing the type of value it depends on. The second part is the name of the type it depends on. The latter indicates dependency, while the former allows code in the method to use the type of value (also known as the current value). The identifier representing the current value may be referred to as the recipient identifier, or simply as the recipient.
The receiver here refers to the type to which it depends. We still take the structural type Person as an example. Following is a statement attached to its method called Grow:

func (person *Person) Grow() {
    person.Age++
} 

As shown above, the parenthesis between the keyword func and the name Grow and its contents are the recipient declaration. It consists of two parts. The first part is the identifier representing the type of value it depends on. The second part is the name of the type it depends on. The latter indicates dependency, while the former allows code in the method to use the type of value (also known as the current value). The identifier representing the current value may be referred to as the recipient identifier, or simply as the recipient. See the following example:

p := Person{"Robert", "Male", 33}
p.Grow()

We can call its method Grow directly on top of the Person-type variable p by applying the call expression. Note that the recipient identifier person of the method Group refers to the value of the variable P. This is also the origin of the word "current value". In the Grow method, we select the field Age of the current value by using the selection expression and increase it by itself. Therefore, after the statement p.Grow() is executed, the person represented by P is one year older (the value of the Ge field of P has changed to 34).
It should be noted that the type in the recipient declaration of the Group method is * Person, not Person. In fact, the former is the latter's type of pointer. This also makes person refer to p's pointer, not to itself.

  • The method always binds the object instance and implicitly takes the instance as the first argument (receiver).

    • You can only define methods for named types in the current package.
    • The parameter receive can be named arbitrarily. If the method has not been used, the parameter name can be omitted.
    • The parameter receive type can be T or * T. Type T cannot be an interface pointer.
    • Method overloading is not supported, and receive is only part of parameter signature.
    • All methods can be invoked by instance value or pointer, and the compiler automatically converts them.
  • There are no constructive and destructive methods, and object instances are usually returned in simple factory mode.

type Queue struct {
    elements []interface{}
}

func NewQueue() *Queue { // Create an object instance.
    return &Queue{make([]interface{}, 10)}
}
func (*Queue) Push(e interface{}) error { // Omit the receiver parameter name.
    panic("not implemented")
}

// func (Queue) Push(e int) error { // Error: method redeclared: Queue.Push
// panic("not implemented")
// }
func (self *Queue) length() int { // Reciver parameter names can be self, this, or other.
    return len(self.elements)
}
  • The method is just a special function, just restore it to know the difference between receiver T and * T.
type Data struct {
    x int
}

func (self Data) ValueTest() { // func ValueTest(self Data);
    fmt.Printf("Value: %p\n", &self)
}
func (self *Data) PointerTest() { // func PointerTest(self *Data);
    fmt.Printf("Pointer: %p\n", self)
}
func main() {
    d := Data{}
    p := &d
    fmt.Printf("Data: %p\n", p)
    d.ValueTest()   // ValueTest(d)
    d.PointerTest() // PointerTest(&d)
    p.ValueTest()   // ValueTest(*p)
    p.PointerTest() // PointerTest(p)
}

Output results:

Data    : 0x1167c0ec
Value   : 0x1167c108
Pointer : 0x1167c0ec
Value   : 0x1167c10c
Pointer : 0x1167c0ec

Result analysis:
From the results above, we can see that when receiver is of type T, it is a copy of Data, and when receiver is of type * T, it refers to the real data. If the receiver of a method is * T, you can call the method on an instance variable of type T, V, without the need for & V to call the method. If the receiver of a method is * T, you can call the method on an instance variable V of type T without the need for & V to call the method. If the receiver of a method is T, you can call the method on a variable P of type * T without the need for * P to call the method. So you don't have to worry about whether you're calling the method of the pointer or not, Go knows what you're going to do. I'm really shocked, that is to say, the design of Go needs you to understand why, but you don't need to do what.

  • Starting with Go version 1.4, multi-level pointer lookup method members are no longer supported.
type X struct{}

func (*X) test() {
    println("X.test")
}
func main() {
    p := &X{}
    p.test()
    // Error: calling method with receiver &p (type **X) requires explicit dereference
    // (&p).test()
}

II. Anonymous Paragraphs

  • Anonymous segment methods can be accessed as field members, and the compiler is responsible for finding them.
type User struct {
    id   int
    name string
}
type Manager struct {
    User
}

func (self *User) ToString() string {
    return fmt.Sprintf("User: %p,%v", self, self)
}

func main() {
    m := Manager{User{1, "Tom"}}
    fmt.Println("Manager: %p\n", &m)
    fmt.Println(m.ToString())
}

Output results:

Manager: %p
 &{{1 Tom}}
User: 0x115b2170,&{1 Tom}
  • Similar reuse capabilities can be acquired and inherited through anonymity. According to the search order of the compiler, override can be implemented by defining the method of the same name in the outer layer.
type User struct {
    id   int
    name string
}
type Manager struct {
    User
    title string
}

func (self *User) ToString() string {
    return fmt.Sprintf("User: %p, %v", self, self)
}
func (self *Manager) ToString() string {
    return fmt.Sprintf("Manager: %p, %v", self, self)
}
func main() {
    m := Manager{User{1, "Tom"}, "Administrator"}
    fmt.Println(m.ToString())
    fmt.Println(m.User.ToString())
}

Output results:

Manager: 0x1162fb20, &{{1 Tom} Administrator}
User: 0x1162fb20, &{1 Tom}

Method Set

Each type has a method set associated with it, which affects the interface implementation rules.

  • The type T method set contains all receive T methods.
  • The type * T method set contains all receive T + T methods.
  • If type S contains your name field T, then S method set contains T method.
  • If type S contains anonymous segment * T, then S method set contains T + *T method.
  • Regardless of embedding T or * T, the * S method set always contains T + *T methods.

Calling methods with instance value and pointer (including anonymous segments) is not constrained by method set. The compiler always finds all methods and automatically converts receive arguments.

IV. Expressions

  • According to the different caller, the method can be divided into two forms:
instance.method(args...)  ---> <type>.func(interface, args...)
  • The former is called method value, and the latter is called method expression.
  • Both can be assigned and passed as normal functions. The difference is that method value binds instances, while method expression needs to be explicitly passed.
type User struct {
    id   int
    name string
}

func (self *User) Test() {
    fmt.Printf("%p, %v\n", self, self)
}
func main() {
    u := User{1, "Tom"}
    u.Test()
    mValue := u.Test
    mValue() // Implicit transfer receiver
    mExpression := (*User).Test
    mExpression(&u) // Explicit transfer receiver
}

Output results:

0x11482170, &{1 Tom}
0x11482170, &{1 Tom}
0x11482170, &{1 Tom}
  • Note that method value replicates the receiver.
type User struct {
    id   int
    name string
}

func (self User) Test() {
    fmt.Println(self)
}
func main() {
    u := User{1, "Tom"}
    mValue := u.Test // Replicate receiver immediately because it is not a pointer type and is not affected by subsequent modifications.
    u.id, u.name = 2, "Jack"
    u.Test()
    mValue()
}

Output results:

{2 Jack}
{1 Tom}
  • At the assembly level, method value and closure are implemented in the same way, actually returning FuncVal-type objects.
FuncVal { method_address, receiver_copy }
  • The method expression can be transformed according to the method set, paying attention to the difference of receive type.
type User struct {
    id   int
    name string
}

func (self *User) TestPointer() {
    fmt.Printf("TestPointer: %p, %v\n", self, self)
}
func (self User) TestValue() {
    fmt.Printf("TestValue: %p, %v\n", &self, self)
}
func main() {
    u := User{1, "Tom"}
    fmt.Printf("User: %p, %v\n", &u, u)
    mv := User.TestValue
    mv(u)
    mp := (*User).TestPointer
    mp(&u)
    mp2 := (*User).TestValue // *User Method Set Contains TestValue. 
    mp2(&u)                  // Signature changed to func TestValue(self) *User). 
} // Actually, it's still receiver value copy.

Output results:

User        : 0x116420e0, {1 Tom}
TestValue   : 0x11642140, {1 Tom}
TestPointer : 0x116420e0, &{1 Tom}
TestValue   : 0x11642170, {1 Tom}
  • Reduce the method to a function.
type Data struct{}

func (Data) TestValue()    {}
func (*Data) TestPointer() {}
func main() {
    var p *Data = nil
    p.TestPointer()
    (*Data)(nil).TestPointer() // method value
    (*Data).TestPointer(nil)   // method expression
    // p.TestValue() // invalid memory address or nil pointer dereference
    // (Data)(nil).TestValue() // cannot convert nil to type Data
    // Data.TestValue(nil) // cannot use nil as type Data in function argument
}

5. Examples of supplementary exercises

Add fields and methods to the structure type Person declared in the source file so that the file does not cause any compilation errors and can print Robert moved from Beijing to San Francisco.

package main

import "fmt"

type Person struct {
    Name    string
    Gender  string
    Age     uint8
}

func main() {
    p := Person{"Robert", "Male", 33, "Beijing"}
    oldAddress := p.Move("San Francisco")
    fmt.Printf("%s moved from %s to %s.\n", p.Name, oldAddress, p.Address)
}

Reference code:

package main

import "fmt"

type Person struct {
    Name    string
    Gender  string
    Age     uint8
    Address string
}

func (self *Person) Move(addr string) string {
    a := self.Address
    self.Address = addr
    return a
}
func main() {
    p := Person{"Robert", "Male", 33, "Beijing"}
    oldAddress := p.Move("San Francisco")
    fmt.Printf("%s moved from %s to %s.\n", p.Name, oldAddress, p.Address)
}

VI. SUMMARY

This part mainly introduces the GoObject-Oriented Method, and after learning this part, we can design some basic Go programs. Object-oriented in Go is so simple that there is no private or public keyword, which can be achieved by capitalization (shared at the beginning of capitalization and private at the beginning of lowercase). The method also applies to this principle. Explore these usages carefully, and you will feel more subtle about the design of Go, which allows developers to develop without ignoring the key points, but do not allow you to do redundant, complex actions for it.

VII. References

  1. Go Learning Notes (Rain Traces)
  2. Go Web programming
  3. Go Language Lesson 1

Topics: Programming Go