Go bottom layer - Reflection and interface ②

Ten problems about interface of deep decryption Go language (I)

The relationship between Go language and duck type

Let's look directly at the definition in Wikipedia:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

  • Translated as:

    • If something looks like a duck, swims like a duck, quacks like a duck, it can be regarded as a duck
  • Duck Typing, duck type, is an object inference strategy for dynamic programming languages

    • It focuses more on how objects can be used
    • Not the type of the object itself
  • Go language as a static language

    • It perfectly supports duck types through interfaces

For example, in the dynamic language python, define a function like:

def hello_world(coder):
    coder.say_hello()
  • When this function is called

    • Any type can be passed in
    • As long as it implements say_ Just use the hello () function
  • If not implemented, errors will occur during operation

  • In static languages such as Java and C + +

    • You must explicitly declare that an interface is implemented
    • After that, it can be used wherever this interface is needed
    • If you call hello_ in the program World function
      • But it introduces a saying that does not implement say at all_ Type of hello()
      • Then it won't pass at the compilation stage
  • This is why static languages are safer than dynamic languages

  • The difference between dynamic language and static language is reflected here

  • Static languages can find type mismatch errors during compilation

    • Unlike dynamic languages, you have to run to that line of code to report an error
  • of course

    • Static language requires programmers to write programs according to regulations in the coding stage
      • Specify the data type for each variable
      • This is to some extent
        • Increased workload
        • It also increases the amount of code
    • Dynamic languages do not have these requirements
      • It can make people focus more on business
        • The code is also shorter
        • It's faster to write
  • Students who write python know this better

  • Go language as a modern static language

    • It has the advantage of late development
  • It introduces the convenience of dynamic language

    • At the same time, static language type checking will be carried out
  • It's very Happy to write

  • Go adopted a compromise approach:

    • Declare that an interface is implemented without requiring type display
    • As long as the relevant methods are implemented
      • The compiler can detect
  • Let's take an example:

package main

import "fmt"

// First define an interface and functions that use this interface as parameters:
type IGreeting interface {
	sayHello()
}

func sayHello(i IGreeting) {
	i.sayHello()
}

// Then define two structures:
type Go struct {
}

func (g Go) sayHello() {
	fmt.Println("Hi, I am Go")
}

type PHP struct {
}

func (p PHP) sayHello() {
	fmt.Println("Hi, I am PHP!")
}

func main() {
	golang := Go{}
	php := PHP{}
	
	sayHello(golang)
	sayHello(php)
}
```
type IGreeting interface {
	sayHello()
}

func sayHello(i IGreeting) {
	i.sayHello()
}

type Go struct {
}

func (g Go) sayHello() {
	fmt.Println("Hi, I am Go")
}

type PHP struct {
}

func (p PHP) sayHello() {
	fmt.Println("Hi, I am PHP!")
}

// Finally, call the sayHello() function in the main function:
func main() {
	golang := Go{}
	php := PHP{}
	
	sayHello(golang)
	sayHello(php)
}

// Program output:
Hi, I am GO!
Hi, I am PHP!
  • In the main function
  • When calling the sayHello() function
    • Passed in golang, php object
    • They do not explicitly declare or implement the IGreeting type
      • Only the sayHello() function specified by the interface is implemented
  • actually
  • When the compiler calls the sayHello() function
    • It implicitly converts golang and PHP objects to IGreeting type
    • This is also the type checking function of static language

By the way, let's mention the characteristics of dynamic language:

The type of variable binding is uncertain

It can only be determined during operation

Functions and methods can receive any type of parameter

And the parameter type is not checked when calling

No interface is required

  • To sum up
  • Duck type is a dynamic language style
    • In this style
      • Effective semantics of an object
      • Not by inheriting from a specific class or implementing a specific interface
      • It is determined by its "collection of current methods and properties"
  • Go as a static language
    • It is realized through the interface Duck type
    • In fact, it is the Go compiler that does the covert conversion work

Difference between value receiver and pointer receiver

method

  • Method can add new behavior to user-defined types

  • It differs from a function in that a method has a receiver

    • Add a receiver to a function
    • Then it becomes a method
  • The receiver can be a value receiver or a pointer receiver.

  • When calling a method

    • Value type
      • You can call the method of the value receiver
      • You can also call the method of the pointer receiver
    • Pointer type
      • You can call the method of the pointer receiver
      • You can also call the method of the value receiver
  • in other words

    • No matter what type of receiver the method is
    • Both values and pointers of this type can be called
    • It is not necessary to strictly conform to the type of receiver

Let's take an example:

package main

import "fmt"

type Person struct {
	age int
}

func (p Person) howOld() int {
	return p.age
}

func (p *Person) growUp() {
	p.age += 1
}

func main() {
	// qcrao is a value type
	qcrao := Person{
		age: 18,
	}
	// A value type calls a method whose receiver is also a value type
	fmt.Println(qcrao.howOld())

	// The value type calls a method whose receiver is a pointer type
	qcrao.growUp()
	fmt.Println(qcrao.howOld())

	// ----------------------
	// A value type calls a method whose receiver is also a value type
	fmt.Println(qcrao.howOld())

	// The value type calls a method whose receiver is a pointer type
	qcrao.growUp()
	fmt.Println(qcrao.howOld())

	// ----------------------
}

// The output result of the above example is:
18
19
100
101
  • After calling the growUp function

    • Whether the caller is a value type or a pointer type
    • Its Age values have changed
  • actually

    • When the receiver types of type and method are different
    • In fact, the compiler did some work behind it
      • Present in a table:
-Value recipientPointer receiver
Value type callerMethod uses a copy of the caller, similar to "pass value"Use the reference of the value to call the method. In the above example, qcrao Growth () is actually (&qcrao) growUp()
Pointer type callerThe pointer is dereferenced as a value. In the above example, stefno Howold() is actually (* stefno) howOld()In fact, it is also "value transfer". The operation in the method will affect the caller. It is similar to pointer parameter transfer, which copies a pointer

Value receiver and pointer receiver

  • As mentioned earlier

    • Whether the receiver type is a value type or a pointer type
      • Can be called by value type or pointer type
      • It actually works through grammar sugar
  • Let's start with the conclusion:

    • Implementing a method in which the receiver is a value type is equivalent to automatically implementing a method in which the receiver is a pointer type
    • The method that implements the receiver is a pointer type will not automatically generate the method that the corresponding receiver is a value type

Take a look at an example and you will fully understand:

package main

import "fmt"

type coder interface {
    code()
    debug()
}

type Gopher struct {
    language string
}

func (p Gopher) code() {
    fmt.Printf("I am coding %s language\n", p.language)
}

func (p *Gopher) debug() {
    fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
    var c coder = &Gopher{"Go"}
    c.code()
    c.debug()
}

// The above code defines an interface coder, which defines two functions:
code()
debug()

// Then a structure Gopher is defined, which implements two methods, a value receiver and a pointer receiver.
// Finally, we call the two defined functions through the variables of interface type in the main function
// Run it and the result is:
I am coding Go language
I am debuging Go language

// But if we change the first statement of the main function:
func main() {
    var c coder = Gopher{"Go"}
    c.code()
    c.debug()
}

// Run and report an error:
./main.go:24:6: cannot use Programmer literal (type Programmer) as type coder in assignment:
    Programmer does not implement coder (debug method has pointer receiver)

  • See the difference between the two codes?

    • The first time is to assign & gopher to coder
    • The second time is to assign Gopher to coder.
  • The second error is

    • Gopher does not implement coder
  • It's obvious

    • Because the Gopher type does not implement the debug method
    • On the surface, * Gopher type does not implement code method
    • But because the Gopher type implements the code method
      • So let * Gopher type have code method automatically
  • Of course, the above statement has a simple explanation:

    • The receiver is a pointer type method
      • It is likely that the recipient's properties will be changed in the method
      • This affects the recipient
    • For the receiver, it is a method of value type
      • There is no impact on the receiver itself in the method
  • Therefore, when implementing a method whose receiver is a value type

    • You can automatically generate a method whose receiver is the corresponding pointer type
    • Because neither will affect the recipient
  • However, when implementing a method whose receiver is a pointer type

    • If a method whose receiver is a value type is automatically generated at this time
    • Originally expected change to receiver (via pointer)
      • Not now
    • Because the value type will produce a copy
    • Does not really affect the caller

Finally, just remember the following:

If you implement a method whose receiver is a value type, you implicitly implement a method whose receiver is a pointer type

When are they used

  • If the recipient of the method is a value type

    • Whether the caller is an object or an object pointer
      • What you modify is a copy of the object
      • Does not affect the caller
  • If the receiver of the method is a pointer type

    • The caller modifies the object itself pointed to by the pointer
  • Reasons for using pointers as recipients of methods:

    • Method can modify the value pointed to by the receiver
    • Avoid copying this value every time a method is called
      • When the type of value is a large structure
      • This will be more efficient
  • Use value receiver or pointer receiver

    • It is not determined by whether the method modifies the caller (that is, the receiver)
    • It should be based on the nature of the type
  • If the type has "primitive essence"

  • In other words, its members are primitive types built in the Go language

    • Such as string, integer value, etc
      • Then define the method of the value receiver type
  • Like built-in reference types

    • Such as slice, map, interface and channel
      • These types are special. When you declare them, you actually create a header
      • For them, it is also a method to directly define the value receiver type
    • In this way, when the function is called
      • These types of header s are copied directly
      • The header itself is designed for replication.
  • If the type has a non primitive nature

    • Cannot be copied safely
      • This type should always be shared
    • Then define the method of the pointer receiver
      • For example, the structure file in the go source code should not be copied
      • There should be only one entity
  • This paragraph is rather convoluted. You can Go to the section 5.3 of Go language practice

What is the difference between iface and eface

  • iface and eface are the underlying structures that describe interfaces in Go
  • The difference is
    • The interface described by iface contains methods
    • eface is an empty interface without any methods: interface {}

From the perspective of source code:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized
}
  • iface internally maintains two pointers

    • tab points to an itab entity
      • It represents the type of interface and the entity type assigned to this interface
    • data points to the specific value of the interface
      • Generally speaking, it is a pointer to heap memory
  • Let's take a closer look at the itab structure:

    • _ The type field describes the type of entity
      • Including memory alignment, size, etc
    • The inter field describes the type of interface
    • The fun field places the method address of the specific data type corresponding to the interface method
      • Implement the dynamic dispatch of interface calling methods
      • Generally, this table will be updated every time a conversion occurs to an interface assignment
      • Or take the cached itab directly
  • Only entity type and interface related methods will be listed here

  • Other methods of entity types do not appear here

    • If you have studied C + +, you can compare the concept of virtual function here
  • In addition, you may wonder why the size of the fun array is 1

  • What if the interface defines multiple methods?

    • In fact, what is stored here is the function pointer of the first method
    • If there are more methods, continue to store in the memory space after it
    • From an assembly point of view
      • These function pointers can be obtained by adding addresses, which has no impact
      • Incidentally, these methods are arranged in the dictionary order of function names

Take another look at the interface type, which describes the interface type:

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}
  • Can see
  • It's packed_ Type type
    • _ type is actually a structure that describes various data types in Go language
    • We note that
    • There is also an mhdr field
      • Represents the list of functions defined by the interface
      • The pkgpath record defines the package name of the interface

Here is a picture of the iface structure:

Next, let's look at the source code of eface:

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
  • Compared with iface
  • eface is simpler
    • Only one is maintained_ type field
      • Represents the specific entity type carried by the empty interface
    • data describes the specific values

Let's take an example:

package main

import "fmt"

type coder interface {
	code()
	debug()
}

type Gopher struct {
	language string
}

func (p Gopher) code() {
	fmt.Printf("I am coding %s language\n", p.language)
}

func (p Gopher) debug() {
	fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
	x := 200
	var any interface{} = x
	fmt.Println(any)

	g := Gopher{"Go"}
	var c coder = g
	fmt.Println(c)
}

// Execute the command and print out the assembly language:
go tool compile -S ./src/main.go

// You can see that two functions are called in the main function:
func convT2E64(t *_type, elem unsafe.Pointer) (e eface)
func convT2I(tab *itab, elem unsafe.Pointer) (i iface)
  • The parameters of the above two functions can be associated with the fields of iface and eface structures:

    • Both functions are
      • Assemble the parameters
      • Form the final interface
  • As a supplement, let's take a final look_ type structure:

type _type struct {
    // Type size
    size       uintptr
    ptrdata    uintptr
    // hash value of type
    hash       uint32
    // Type of flag, related to reflection
    tflag      tflag
    // Memory alignment correlation
    align      uint8
    fieldalign uint8
    // Type number, including bool, slice, struct, etc
    kind       uint8
    alg        *typeAlg
    // gc correlation
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}
  • Various data types of Go language are in_ Based on the type field, some additional fields are added for management:
type arraytype struct {
    typ   _type
    elem  *_type
    slice *_type
    len   uintptr
}

type chantype struct {
    typ  _type
    elem *_type
    dir  uintptr
}

type slicetype struct {
    typ  _type
    elem *_type
}

type structtype struct {
    typ     _type
    pkgPath name
    fields  []structfield
}
  • The structure definition of these data types is the basis of reflection implementation

Dynamic type and dynamic value of interface

  • From the source code, we can see that iface contains two fields:
    • tab is the interface table pointer
      • Point to type information
    • Data is a data pointer
      • Point to specific data
    • They are called dynamic types and dynamic values, respectively
    • Interface values include dynamic types and dynamic values

[Extension 1] compare the interface type with nil

  • The zero value of the interface value means that both the dynamic type and the dynamic value are nil
    • If and only if the values of both parts are nil
    • This interface value will be considered as interface value = = nil

Let's take an example:

package main

import "fmt"

type Coder interface {
    code()
}

type Gopher struct {
    name string
}

func (g Gopher) code() {
    fmt.Printf("%s is coding\n", g.name)
}

func main() {
    var c Coder
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)

    var g *Gopher
    fmt.Println(g == nil)

    c = g
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
}

// Output:
true
c: <nil>, <nil>
true
false
c: *main.Gopher, <nil>
  • in limine
    • The dynamic type and dynamic value of c are nil
    • g is also nil
    • When g is assigned to c
      • The dynamic type of c becomes * main Gopher
    • Only the dynamic value of c is still nil
    • But when c is compared with nil
      • The result is false

[Extension 2] let's take an example and see its output:

package main

import "fmt"

type MyError struct {}

func (i MyError) Error() string {
    return "MyError"
}

func main() {
    err := Process()
    fmt.Println(err)

    fmt.Println(err == nil)
}

func Process() error {
    var err *MyError = nil
    return err
}

// Function running result:
<nil>
false
  • Here, a MyError structure is defined first
    • The Error function is implemented
    • The error interface is implemented
  • The Process function returned an error interface
    • This implies type conversion
  • therefore
    • Although its value is nil
    • In fact, its type is * MyError
    • Finally, when compared with nil, the result is false

[extension 3] how to print the dynamic type and value of the interface?

package main

import (
    "unsafe"
    "fmt"
)

type iface struct {
    itab, data uintptr
}

func main() {
    var a interface{} = nil

    var b interface{} = (*int)(nil)

    x := 5
    var c interface{} = (*int)(&x)

    ia := *(*iface)(unsafe.Pointer(&a))
    ib := *(*iface)(unsafe.Pointer(&b))
    ic := *(*iface)(unsafe.Pointer(&c))

    fmt.Println(ia, ib, ic)

    fmt.Println(*(*int)(unsafe.Pointer(ic.data)))
}

// The operation results are as follows:
{0 0} {17426912 0} {17426912 842350714568}
5
  • An iface structure is directly defined in the code

    • Use two pointers to describe itab and data
    • Then, the contents of a, B and C in memory are forcibly interpreted as our custom iface
    • Finally, you can print out the addresses of dynamic types and dynamic values
  • The address of the dynamic type and dynamic value of a is 0, that is, nil

  • The dynamic type of b is consistent with that of c, both of which are * int

  • The dynamic value of c is 5

The compiler automatically detects whether a type implements an interface

It is often seen that some open source libraries have strange usages like the following:

var _ io.Writer = (*myWriter)(nil)
  • I'll be a little confused at this time
    • I don't know what the author wants to do
  • In fact, this is the answer to this question
  • The compiler checks whether the * myWriter type implements io Writer interface

Let's take an example:

package main

import "io"

type myWriter struct {

}

/*func (w myWriter) Write(p []byte) (n int, err error) {
    return
}*/

func main() {
    // Check whether the * myWriter type implements io Writer interface
    var _ io.Writer = (*myWriter)(nil)

    // Check whether the myWriter type implements io Writer interface
    var _ io.Writer = myWriter{}
}

// After commenting out the Write function defined for myWriter, run the program:
src/main.go:14:6: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment:
    *myWriter does not implement io.Writer (missing Write method)
src/main.go:15:6: cannot use myWriter literal (type myWriter) as type io.Writer in assignment:
    myWriter does not implement io.Writer (missing Write method)
  • Error message: * myWriter/myWriter not implemented io The writer interface, that is, the Write method is not implemented.
    • After the comment is released, the program will run without error
  • In fact, the above assignment statement will have implicit type conversion
    • In the process of conversion
      • The compiler checks whether the type to the right of the equal sign implements the function specified by the interface to the left of the equal sign

To sum up, you can add the following code to the code to detect whether the type implements the interface:

var _ io.Writer = (*myWriter)(nil)
var _ io.Writer = myWriter{}

Keywords: Go Back-end

Added by and1c on Mon, 20 Dec 2021 15:19:30 +0200