Is Go an object oriented language? Can you program toward objects?

In the previous article, though, we explored some of the concepts related to object orientation in Go, and more specifically, the basic concepts of encapsulation and its implementation.

But that’s not enough. You can’t just settle for one path. You should take as many paths as possible so that you can build experience for future problems.

What is exploratory learning

The learning process of guessing hypotheses step by step through the guidance of existing knowledge and thought rules is exploratory learning, which is conducive to our thinking and deepen our understanding of new knowledge. Why not do it?

The process of learning Go is becoming more and more difficult, not because the grammar is difficult to understand but because the habits of mind behind the grammar habits are so different!

Go language is a new language compared with other mainstream programming languages, which is not only reflected in syntax but also in the difference of implementation ideas.

Particularly for the developers already have experience in other programming, this experience deeper, could have been here think well-deserved things Go language basically changed shape, largely in an approach to realize, this is actually a good thing, different ideas to accelerate the progress of thinking, and stand still, talk He Chuangxin development?

Thanks to the powerful IDE development tools, we would not have been able to detect errors in time, and it is this quick trial-and-error experience that gives us enough feedback to get closer and closer to the truth of Go programming.

The main line direction has been determined in the last article, and the concept and implementation of object-oriented encapsulation have been basically clarified. In order not to omit any potentially important knowledge points, this paper will continue to explore the open and strive to explain the knowledge points of encapsulation clearly.

If this learning process is likened to running a maze, the strategy of going all the way to black is the depth-first algorithm of algorithm theory. If you’re walking and looking, looking around at the landscape around you, that’s the breadth-first algorithm.

So, as you might have guessed, the depth-first algorithm is used and the breadth-first algorithm is used in this article to continue the exploration of encapsulated objects!

Defining structure

Is there only one way to define a structure, or is there no simplified form?

In my opinion, simplified forms do not exist. When a structure has multiple fields, the standard definition method is reasonable to use, but if there is only one field, still defining the structure in the standard form is a bit of a dead end.

type MyDynamicArray struct {
    ptr *[10]int
    len int
    cap int
}
Copy the code

A structure is simply a way to implement encapsulation. When the encapsulated object has only one field, the field name does not exist or the unique field name should be automatically defined by the compiler, so the field name can be omitted.

The field type is certainly essential, so that for objects that encapsulate only one field, only the type of that unique field needs to be considered.

Based on the above reasons, I think this conjecture is reasonable, but can it be realized according to the existing knowledge?

For the sake of simplicity, let’s take the struct declaration of a dynamic array from the previous article as a test case.

type MyDynamicArray struct {
    ptr *[10]int
    len int
    cap int
}
Copy the code

If you must choose a field from the three fields, it can only keep the internal array, exclude the rest of the fields, and the final result may not be able to achieve the function of dynamic array, there will be a lack of semantics, that is no matter the semantics, only talk about technology!

Since only the internal array is retained, the dynamic array looks like this. We’ve lost the semantics of dynamic arrays, and we’ve changed the name, let’s call it MyArray!

type MyArray struct {
    arr [10]int
}
Copy the code

Obviously, this is still the standard grammatical form for structs, so please think with me about how to simplify this form.

Because there is only one internal field in this simplified form, the field name must be omitted and the field type may be different, so only the declaration of the internal field type should be left in the simplified form.

type MyArray struct{[10]int
}
Copy the code

Since multiple fields need to be separated by line breaks, one field does not need line breaks, so curly braces are not necessary. This is also in line with the principle of semantic clarity under the condition of minimization in Go design.

Of course, if you ask me if I really have this principle, my answer is maybe yes and maybe no.

Because I do not know, just a feeling of learning Go language recently, everywhere reflects such a philosophical thought, do not take seriously, just my personal opinion.

type MyArray struct [10]int
Copy the code

Struct ([10]int) MyArray ([10]int); struct ([10]int) MyArray ([10]int);

Now let’s test it in the editor to see if the Go compilation gives an error, and can we verify our guess?

Unfortunately, the IDE editor tells us that [10]int is not legal and must be a type or a type pointer!

[10] The Go compiler does not support this simplified form.

Struct keyword does not support this simplified form, so remove this keyword.

I didn’t think I could!

At least for now, it appears that the Go compiler supports a simplified form, but it is not clear whether this form of support is consistent with the semantics we expect to implement, so keep experimenting!

By declaring variables and printing them directly, we initially prove that our simplified form can work, and the output is also the internal array we defined!

Now let’s see if we can manipulate this so-called internal array?

This simplified form has only one field, which only indicates the type of the field, but does not have the name of the field. Therefore, access to the field should be directly through the structure variable.

type MyArray [10]int

func TestMyArray(t *testing.T) {
    var myArr MyArray

    // [0 0 0 0 0 0 0 0 0
    t.Log(myArr)

    myArr[0] = 1
    myArr[9] = 9

    // [1 0 0 0 0 0 0 0 9]
    t.Log(myArr)
}
Copy the code

This time the conjecture is also verified, the Go compiler directly operates on the internal field through the structure variable, looks like we are closer to the truth!

Before you get too excited, change the unique field to a different type and test it a few times to see if it still works.

type MyBool bool

func TestMyBool(t *testing.T) {
    var myBool MyBool

    // false
    t.Log(myBool)

    myBool = true

    // true
    t.Log(myBool)
}
Copy the code

No errors were reported after some testing, so it is likely that this is a simplified form of structure supported by Go, as we expected.

For the time being, there is no other new Angle to explore the syntax rules of structure attributes.

In the process of exploration, try to put yourself in the position of thinking about how the Go language should be designed to be convenient for users. Try to imagine yourself as the designer of the Go language!

Methods may not be supported in the simplified form of the structure, and if so, that makes sense.

First, syntactically, why do single-field structures not support methods?

Remember when we tried to simplify a single-field structure?

type MyArray struct [10]int
Copy the code

If we place the single-field type directly after the struct keyword, the Go compiler will report an error, which will disappear when we omit the struct keyword.

From the Go compiler’s point of view,struct is a system keyword that tells the compiler to parse into struct syntax whenever it sees this keyword. Now, no srUCt keyword is encountered, which means it is not struct syntax.

Here, there is a one-to-one correspondence between the keyword and the structure, that is, sufficient and necessary conditions. The structure can be inferred from the keyword, and the keyword can also be inferred from the structure.

Now, how do we define our single-field structure?

type MyArray [10]int
Copy the code

Since there is no keyword struct, the compiler concludes that MyArray is not a structure, and since it is not a structure, it cannot define methods using the receiver function of the structure.

func (myBool *MyBool) IsTrue(a) bool{
    return myBool
}
Copy the code

So this method will report an error, so it makes sense that the Go language does not support single-field struct methods.

And then we’ll explain semantically why methods are not supported.

Back to the original intent of the exploration, when you are defining a structure that has multiple fields, you should specify the field name and type for each field in the standard writing.

If there is only one of these fields, the standard way of writing the definition is fine, but a more simplified way should also be provided.

Structure with only one field, the field name is meaningless and should not appear, because it can be replaced by a structure variable, then the only value of the structure is the type of the field!

Field types include both built-in and user-defined struct types. Either way, the semantics of a simplified struct are completely determined by the field type of that struct. So does a simplified struct need methods?

Nature is not needed!

Field types can be defined by their own field types, which also ensures that responsibilities are clear and separate from each other!

To sum up, I think even if Go does not support the method of single-field structure, there are rules to follow and reasons to follow behind the design!

When we defined dynamic arrays above, we used static arrays internally. Now, to facilitate further exploration of methods, we should provide overloaded methods to support dynamic arrays.

func NewMyDynamicArray(a) *MyDynamicArray {
    var myDynamicArray MyDynamicArray

    myDynamicArray.len = 0
    myDynamicArray.cap = 10
    var arr [10]int
    myDynamicArray.ptr = &arr

    return &myDynamicArray
}
Copy the code

An internal array ARR is a static array and should provide an interface for external callers to initialize specified arrays, overloading methods according to known object-oriented definitions of methods.

The first time you try to overload a method, you get a problem with an error indicating that the method is declared, so saying Go might not support method overloading is a bit of a hassle.

Similar functionality can be achieved either by defining different method names, or by defining a very large function that takes the most arguments and performs the corresponding logical processing based on the caller’s arguments.

Used to method overloading, it was a bit frustrating to find that this feature was not available in Go, which is quite different from other mainstream object-oriented languages.

No support for constructors, no support for method overloading, and features once taken for granted are not taken for granted.

For a moment, why doesn’t Go support method overloading? Are they like constructors, or do they overuse simply disable logic?

Because I am not a designer, I cannot understand and do not want to guess the reason, but it is certain that Go language is a brand new language, has a unique design ideas, not the same as others!

End of joke time, since the thief will have to go one way to black, do not support method overload change the function name or by parameter name distinction.

Oh my god, I just got rid of the method overload problem and array initialization can’t be a variable it can only be a constant expression, okay?

It’s incredible!

Since the array initialization length is only a constant expression, it can not receive the external transmission of the capacity cap, no capacity can only receive the length len, and the initialization of the internal array length and no way to determine, both variables can not be exposed!

All goes back to the origin, want to realize the function of dynamic array can only rely on the specific method to dynamically expand and shrink, can not initialize the specified length.

In this case, the method is also a dead end, stop exploring.

Declarative structure

The definition of structure has been basically explored, except for a simplified form of single-field structure, there is no new discovery at present.

Coming back to the user’s point of view, is there another way to declare a structure?

var myDynamicArray MyDynamicArray
    
t.Log(myDynamicArray)
Copy the code

This is how variables are declared. In addition to this form, remember that when we learned about variables in Go, we introduced the way of declaring and initializing variables. Does this also apply to structure variables?

var myDynamicArray = MyDynamicArray{
        
}

t.Log(myDynamicArray)
Copy the code

The compiler does not fail to show that this literal form is also valid, but empty data structures do not make much sense. How can we initialize the corresponding structure?

The closest data structure to a multi-field structure is a map map!

Remember how map initializes literals!

var m = map[string]string{
    "id":   "1006"."name": "Snow Dream Technology Station",
}

t.Log(m)
Copy the code

Copy the structure and see if you can initialize the structure in the same way.

I haven’t defined it yet, and you can’t?

The IDE editor indicates that the name of the column is invalid.

“Len” is different from len, right?

So let’s get rid of the double quotes and just use the field name.

var myDynamicArray = MyDynamicArray{
    len: 10,
}

t.Log(myDynamicArray)
Copy the code

The error has disappeared and a new hidden ability has been unlocked.

var myDynamicArray = MyDynamicArray{
    ptr: &[10]int{0.1.2.3.4.5.6.7.8.9},
    len: 10.cap: 10,
}

t.Log(myDynamicArray)
Copy the code

In addition to specifying the field name injection, can we initialize the fields in sequence without specifying the field name?

You can see in the editor that it’s actually injected sequentially, so it’s kind of interesting that when you instantiate a literal that doesn’t support a constructor, it looks like a constructor with no arguments, with arguments and full arguments, right?

As can be expected, this method of full parameter injection must be strictly in accordance with the definition of the order of matching, when the parameter is incomplete may be inserted by bit may not support, the truth is, try to know!

This form of incomplete arguments is not actually supported, so I think it’s semantically clear that there are either no arguments, full arguments, or specified initialization fields.

In addition to the literal approach, does Go support the make function used to create slices or maps?

It seems that the make function does not support the creation of constructs, and it is not clear why, and a source of personal confusion.

Since make can create built-in types such as slice and map, which are semantically the variables used to create the types, and constructs are also types, the only difference may be that constructs are mostly custom types rather than built-in types.

If I were to design it, it would probably rule the world, because semantically consistent functions only use the same keywords.

Going back to traditional object-oriented programming specifications, objects are instantiated using the keyword new, which is not the keyword in Go.

Functions in Go are first-class citizens, just as make is not a keyword, it is also a function.

Even for the same goal,Go has its own unique insights!

New does not appear as a keyword, but as a function. It can also instantiate objects.

Can’t a new function instantiate an object? Why is it wrong to say assignment error?

Scared me to look at the documentation for new.


// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
Copy the code

Go does not support this form of assignment, so let’s initialize the declaration.

var myDynamicArray2 = new(MyDynamicArray)
    
t.Log(myDynamicArray2)  
Copy the code

Since there are two ways to instantiate objects, it’s important to look at the differences.

func TestNewMyDynamicArray(t *testing.T) {
    var myDynamicArray = MyDynamicArray{
        ptr: &[10]int{0.1.2.3.4.5.6.7.8.9},
        len: 10.cap: 10,
    }
    myDynamicArray = MyDynamicArray{
        &[10]int{0.1.2.3.4.5.6.7.8.9},
        10.10,
    }
    t.Log(myDynamicArray)
    t.Logf("%[1]T %[1]v", myDynamicArray)

    var myDynamicArray2 = new(MyDynamicArray)
    myDynamicArray2.ptr = &[10]int{0.1.2.3.4.5.6.7.8.9}
    myDynamicArray2.len = 10
    myDynamicArray2.cap = 10

    t.Log(myDynamicArray2)

    t.Logf("%[1]T %[1]v", myDynamicArray2)
}
Copy the code

T. logf (“%[1]T %[1]v”, myDynamicArray)

%[1]T is actually a variant of %T, and %[1]v is also a variant of %v. If you look carefully, you will find that the placeholders are exactly the same variable. Here is also the first parameter, so it is replaced with [1], which again reflects the simplicity of Go language design.

Here is another simple example to deepen the impression, look carefully oh!

test := "snowdreams1006"

// string snowdreams1006
t.Logf("%T %v", test, test)
t.Logf("%[1]T %[1]v", test)
Copy the code

%T is the type of the printed variable and should be short for type type and v should be short for value.

After explaining the meaning of the test code, I looked back at the test results and found that the variable types obtained by using the literal method and the variable types obtained by the new function were significantly different!

_struct.MyDynamicArray {0xC0000560f0 10 10} is the struct type and *_struct.MyDynamicArray &{0xC000056190 10 10} is the pointer type of the struct type.

This difference is also expected and semantic.

Literals instantiate objects that are value objects, while new instantiates objects that open up memory and return instance objects to references, just like the new keyword in other programming languages, right?

Now that we’re talking about value objects and reference objects, the age-old question is, which type should be passed when a function or method is passed?

Value pass or reference pass

The following example has nothing to do with dynamic arrays. For simplicity, I’ll create a new structure called Employee to review what I’ve learned so far about encapsulation.

type Employee struct {
    Id   string
    Name string
    Age  int
}

func TestCreateEmployee(t *testing.T) {
    e := Employee{
        "0"."Bob",
        20,
    }
    t.Logf("%[1]T %[1]v", e)

    e1 := Employee{
        Name: "Mike",
        Age:  30,
    }
    t.Logf("%[1]T %[1]v", e1)

    e2 := new(Employee)
    e2.Id = "2"
    e2.Name = "Rose"
    e2.Age = 18
    t.Logf("%[1]T %[1]v", e2)
}
Copy the code

The first test is reference-passing, which is also the way structures are commonly passed, and behaves in the same way as in other mainstream programming languages, where changes within a method affect the caller’s parameters.

func (e *Employee) toStringPointer(a) string {
    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}

func TestToStringPointer(t *testing.T) {
    e := &Employee{"0"."Bob".20}

    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    t.Log(e.toStringPointer())
}
Copy the code

Unsafe.Pointer(&e.name) looks at the memory address of the variable. The address is the same before and after the call.

func (e Employee) toStringValue(a) string {
    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}

func TestToStringValue(t *testing.T) {
    e := Employee{"0"."Bob".20}

    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    t.Log(e.toStringValue())
}
Copy the code

The memory address sent by the caller is not the same as the memory address received by the receiver.

The distinction between a value type and a reference type needs no further explanation. The magic thing is that the receiver of a method is a value type. Does the caller of a method have to pass a value type?

func (e Employee) toString(a) string {
    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}
Copy the code

Method callers pass a value type and a reference type respectively, and both work just fine, as if the definition of the method had nothing to do with it!

func TestToString(t *testing.T) {
    e := Employee{"0"."Bob".20}

    fmt.Printf("Name address is %x\n", unsafe.Pointer(&e.Name))

    t.Log(e.toString())
    t.Log((&e).toString())
}
Copy the code

Although the recipient of a method requires a value type, the caller passes a value type or a reference type!

Just by changing the type of the method receiver, the caller does not have to make any changes and still works fine!

So that’s kind of amazing. The recipient of a method can be either a value type or a pointer type, and the caller can be either a value type or a pointer type. Why?

Also, based on semantic analysis, the method of the designer and the caller is loose coupling, between the designer’s change for the caller didn’t much impact, and that means designers feel after receive value type with parameter is not good, can directly change the adjust the logic for pointer type without having to notify the caller!

This is due to the way the Go language’s designers have handled the invocation of value types and pointer types. Both value types and reference types use the dot operator. Call methods, unlike some languages where pointer types are prefixed with -> or * to call methods of pointer type.

There is something to be done and there is nothing to be done. It is possible to see the difference brought by these two calls. Go is unified into the point operator!

Although both calls are formally the same, should a method or function be designed as a value type or a pointer type?

Here are three suggestions:

  • If the receiver needs to change the caller’s value, only the pointer type can be used
  • If the parameter itself is very large, copying the parameter consumes memory, so only the pointer type can be used
  • If the argument itself has state, copying the argument may affect the state of the object. Only pointer types can be used
  • If it is a built-in type or a relatively small structure, you can completely ignore the copying problem and recommend value types.

Of course, the actual situation may be business dependent, the specific type of the use of your own judgment, do not worry about the wrong choice, just change the parameter type will not affect the code logic of the caller.

How to access it after encapsulation

The encapsulation problem is basically explained clearly. Generally speaking, the structure after encapsulation is not only for our own use but also may be provided to the outside world. At the same time, to ensure that the outside world can not modify our packaging logic at will, this part involves access control rights.

Go language access level has two kinds, one is public and the other is private, because there is no inheritance feature, also does not involve the inheritance of access permissions between subclasses and parent classes, suddenly feel that there is no inheritance is not a bad thing, less a lot of error-prone concept!

Although it is easy to understand now, it is difficult to judge whether it is convenient to use.

The naming conventions for visibility are as follows:

  • The name is generally used in the large hump nomenclature i.eCamelCase
  • Capitalized for publicpublic, lowercase means privateprivate .
  • The above rules apply not only to methods, but also to structures, variables and constantsGoThe totality of language.

So the question is, who are public and private here for?

The basic structure of Go language is package. There is a difference between package and directory. Unlike Java language, package and directory are strictly related, which needs special attention for Java friends.

A package is a collection of related code, possibly in different directory files, that tells the Go compiler that we are a family by declaring the package package.

If different file directories can be declared in the same package, this is equivalent to allowing family migration and keeping only the last name.

Let’s talk in code, let’s see if scattered friends can have a common last name!

package main

import (
    "fmt"
    "github.com/snowdreams1006/learn-go/oop/pack"
)

func main(a) {
    var l = new(pack.Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    fmt.Println(l.ToString())
}

Copy the code

The pack.go source file and the pack_test test file are both in the same directory pack and the package declaration is also pack.

This situation is equivalent to a clan living together in a village and behaving in the same way as other languages.

Now let’s see if a part of this clan can move to another village.

Isn’t the cross-domain area a little too large to support defining methods? Try moving it closer to the Pack directory!

Still can’t, can’t create a subdirectory, so in the same directory as the original?

Only this can be identified as a bit structure method, if not a method, can be arbitrarily stored, this point is no longer demonstrated, small partners can test yourself yo!

package main

import (
    "fmt"
    "github.com/snowdreams1006/learn-go/oop/pack"
)

func main(a) {
    var l = new(pack.Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    fmt.Println(l.ToString())

    l.PrintLangName()
}
Copy the code

“Github.com/snowdreams1006/learn-go/oop/pack” is the import dependency in the current file path, so the caller can access to our normal encapsulation structure.

The property in the current structure is set to start with a lowercase letter, so it cannot be accessed outside the same package.

How does it extend after encapsulation

When designers encapsulate objects for use by others, there are inevitably omissions, and users need to extend existing structures.

If you are object-oriented, the simplest way to implement it is probably inheritance, rewriting the extension and so on, but Go does not believe that, does not support inheritance!

So the remaining method is composition, which is also learning object-oriented predecessors summed up a kind of experience: more composition and less inheritance!

Come to think of it, the Go language not only implements this idea, but also strictly, because Go directly eliminates inheritance.

type MyLang struct {
    l *Lang
}

func (ml *MyLang) Print(a) {
    if ml == nil || ml.l == nil {
        return
    }

    fmt.Println(ml.l.ToString())
}

func TestMyLangPrint(t *testing.T) {
    var l = new(Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    var ml = MyLang{l}

    ml.Print()
}
Copy the code

Extend or rewrite methods that Lang does not have by customizing the internal properties of the structure as Lang types.

If our custom structure happens to have only one of these attributes, we can use the simplified form, which is technically called an alias.

type Lan Lang

func (l *Lan) PrintWebsite(a){
    fmt.Println(l.website)
}

func TestLanPrintWebsite(t *testing.T) {
    var la = new(Lan)
    la.name = "GoLang"
    la.website = "https://golang.google.cn/"

    la.PrintWebsite()
}
Copy the code

As designers and users have figured out, the basics of encapsulation are coming to an end, and since Go does not support inheritance, there is no need to demonstrate the code, leaving only the interface.

Go doesn’t support polymorphism either, but the interface it provides is a bit different and has some flavor to it. The next section will explore the interface.

A review of packaging

  • Define structure fields
type Lang struct {
    name    string
    website string
}
Copy the code

When a structure has multiple fields, wrap each other without commas or semicolons.

  • Define struct methods
func (l *Lang) GetName(a) string {
    return l.name
}
Copy the code

An ordinary function whose name is preceded by a parameter pointing to the current structure is no longer a function but a method, and the current structure parameter is called a receiver, similar to the effect achieved by the this or self keyword in other object-oriented languages.

  • Literals declare structures
func TestInitLang(t *testing.T) {
    l := Lang{
        name:    "Go",
        website: "https://golang.google.cn/",
    }

    t.Log(l.ToString())
}
Copy the code

In addition to the argument constructors, there is also the no-argument and full-argument constructors, which just look like they are not really constructors.

  • newDeclarative structure
func TestPack(t *testing.T) {
    var l = new(Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    t.Log(l.ToString())
}
Copy the code

The new function is similar to the new keyword in other mainstream programming languages. It is used to declare structures. Unlike the literal declaration, the output object of the new function is a pointer type.

  • Uppercase controls access rights

For both variable and method names, the first letter of the name is uppercase for public and lowercase for private.

  • The basic organizational unit of code is the package

Access control permissions are also for code packages. There is only one code package in a directory, and the package name is not necessarily related to the directory name.

  • Compound extends existing types
type MyLang struct {
    l *Lang
}

func (ml *MyLang) Print(a) {
    if ml == nil || ml.l == nil {
        return
    }

    fmt.Println(ml.l.ToString())
}

func TestMyLangPrint(t *testing.T) {
    var l = new(Lang)
    l.SetName("Go")
    l.SetWebsite("https://golang.google.cn/")

    var ml = MyLang{l}

    ml.Print()
}
Copy the code

Custom constructs embedded with other constructs and enhanced control over existing types through composition rather than inheritance are also a recommended programming specification.

  • Aliases extend existing types
type Lan Lang

func (l *Lan) PrintWebsite(a) {
    fmt.Println(l.website)
}
Copy the code

An alias can be thought of as a simplified form of a single-field structure that can be used to extend existing structure types and support features such as methods.

Finally, thank you very much for your reading, I have shallow knowledge, if there is any improper description of the place, please also point out that every time you leave a message I will seriously reply, your forwarding is the biggest encouragement to me!

If you need to see the source code, you can go to github.com/snowdreams1… At the same time, it is also recommended to pay attention to the public number to communicate with me.