1, Go programming mode: slicing, interface, time and performance

Slice

First of all, let's discuss Slice, which is called "Slice" in Chinese translation. This thing is not an array in Go language, but a structure. Its definition is as follows:

type slice struct {
    array unsafe.Pointer //Pointer to the array where the data is stored
    len   int            //How long is it
    cap   int            //How big is the capacity
}

From the diagram, the performance of an empty slice is as follows:


Students who are familiar with C/C + + will know that the problem of using array pointers in structures - data sharing will occur! Let's take a look at some operations of slice

foo = make([]int, 5)
foo[3] = 42
foo[4] = 100

bar  := foo[1:4]
bar[1] = 99

For the above code.

  • First, create a slice of foo, in which the length and capacity are 5
  • Then start assigning values to the elements with indexes 3 and 4 in the array pointed to by foo
  • Then, after slicing foo, assign it to bar, and then modify bar[1]

From the above figure, we can see that because the memory of foo and bar is shared, the modification of array contents by foo and bar will affect each other.

Next, let's take a look at an example of the data operation append()

a := make([]int, 32)
b := a[1:16]
a = append(a, 1)
a[2] = 42

In the above code, the slice of a[1:16] is assigned to b. at this time, the memory space of a and b is shared. Then, an append() operation is performed on a, which will make a re share memory, resulting in a and b no longer sharing, as shown in the following figure:

From the above figure, we can see that the append() operation makes the capacity of a 64 and the length 33. Here, we need to pay special attention to the function append(). When the cap is not enough, it will reallocate the memory to expand the capacity. If it is enough, it will not re share the memory!

Let's take another example:

func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')

    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB
}

In the above example, dir1 and dir2 share memory. Although dir1 has an append() operation, because the cap is enough, the data is expanded to the space of dir2. The following is the relevant diagram (note the changes of cap and len in dir1 and dir2 structures in the above figure)

To solve this problem, we only need to modify one line of code.

dir1 := path[:sepIndex]

Change to

dir1 := path[:sepIndex:sepIndex]

The new code uses Full Slice Expression, and its last parameter is "Limited Capacity". Therefore, the subsequent append() operation will result in reallocation of memory.

Depth comparison

When we complex an object, the object can be built-in data type, array, structure, map... When we copy the structure, when we need to compare whether the data in the two structures are the same, we need to use depth comparison instead of simply making shallow comparison. You need to use reflection Deepequal(), here are some examples

import (
    "fmt"
    "reflect"
)

func main() {

    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))
    //prints: v1 == v2: true

    m1 := map[string]string{"one": "a","two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2))
    //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))
    //prints: s1 == s2: true
}

Interface programming

Next, let's take a look at the code. There are two methods. They both output a structure. One uses a function and the other uses a "member function".

func PrintPerson(p *Person) {
    fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
  p.Name, p.Sexual, p.Age)
}

func (p *Person) Print() {
    fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",
  p.Name, p.Sexual, p.Age)
}

func main() {
    var p = Person{
        Name: "Hao Chen",
        Sexual: "Male",
        Age: 44,
    }

    PrintPerson(&p)
    p.Print()
}

Which way do you prefer? In Go language, the method of using "member function" is called "Receiver". This method is a kind of encapsulation, because PrintPerson() is strongly coupled with Person, so it should be put together. More importantly, interface programming can be carried out in this way. For interface programming, it is an abstraction, which is mainly used in "polymorphism". This technology is used in< Introduction to Go language (Part I): interface and polymorphism >It has already been mentioned in. Here, I want to talk about another programming mode of Go language interface.

First, let's take a look at the following code:

type Country struct {
    Name string
}

type City struct {
    Name string
}

type Printable interface {
    PrintStr()
}
func (c Country) PrintStr() {
    fmt.Println(c.Name)
}
func (c City) PrintStr() {
    fmt.Println(c.Name)
}

c1 := Country {"China"}
c2 := City {"Beijing"}
c1.PrintStr()
c2.PrintStr()

Among them, we can see that it uses a Printable interface, and both Country and City implement the interface method PrintStr() to output themselves. However, these codes are the same. Can you spare it?

We can use "structure embedding" to do this, as shown in the following code,

type WithName struct {
    Name string
}

type Country struct {
    WithName
}

type City struct {
    WithName
}

type Printable interface {
    PrintStr()
}

func (w WithName) PrintStr() {
    fmt.Println(w.Name)
}

c1 := Country {WithName{ "China"}}
c2 := City { WithName{"Beijing"}}
c1.PrintStr()
c2.PrintStr()

A structure called WithName is introduced. However, the problem is that it becomes a little messy during initialization. So, do we have a better way? Here is another solution.

type Country struct {
    Name string
}

type City struct {
    Name string
}

type Stringable interface {
    ToString() string
}
func (c Country) ToString() string {
    return "Country = " + c.Name
}
func (c City) ToString() string{
    return "City = " + c.Name
}

func PrintStr(p Stringable) {
    fmt.Println(p.ToString())
}

d1 := Country {"USA"}
d2 := City{"Los Angeles"}
PrintStr(d1)
PrintStr(d2)

From the above code, we can see that we use an interface called Stringable, which decouples the "business type" Country and City from the "control logic" Print(). Therefore, as long as the Stringable interface is implemented, it can be passed to PrintStr() for use.

There are many examples of this programming mode in Go's standard library, the most famous of which is Io Read and ioutil How to play readall, where io Read is an interface. You need to implement one of its read (P [] byte) (n int, err, error) interface methods. As long as it meets this scale, it can be used by ioutil Readall is used by this method. This is the golden rule of object-oriented programming method - "Program to an interface not an implementation"

Interface integrity check

In addition, we can see that the programmer of Go language does not strictly check whether an object implements all interface methods of an interface, as shown in the following example:

type Shape interface {
    Sides() int
    Area() int
}
type Square struct {
    len int
}
func (s* Square) Sides() int {
    return 4
}
func main() {
    s := Square{len: 5}
    fmt.Printf("%d\n",s.Sides())
}

We can see that Square does not implement all the methods of the Shape interface. Although the program can run through, the way of programming is not rigorous. If we need to enforce the implementation of all the methods of the interface, what should we do?

There is a standard practice in the Go language programming circle:

var _ Shape = (*Square)(nil)

Declare a_ Variable (no one uses it), which will convert a nil null pointer from Square to Shape. In this way, if the relevant interface methods are not implemented, the compiler will report an error:

cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)

In this way, a strong verification method is achieved.

time

For time, it should be a complicated problem in programming. Believe me, time is a very complicated thing (for example< Are you sure you know the time?>,<About leap seconds >Etc.). Moreover, the complexity of time zone, format, accuracy and so on can not be handled by ordinary people. Therefore, we must reuse the existing time to deal with it instead of doing it by ourselves.

In Go language, you must use time Time and time Duration has two types:

  • On the command line, via. flag Parseduration supports time Duration
  • In encoding/json in JSon, you can also put time Time code into RFC 3339 Format of
  • The database/sql used by the database also supports the conversion of DATATIME or TIMESTAMP type to time Time
  • YAML you can use gopkg in/YAML. V2 also supports time Time ,time.Duration and RFC 3339 format

If you have no way to interact with a third party, please also use RFC 3339 Format.

Finally, if you want to do global cross time zone applications, you must use UTC time for all servers and times.

Performance tips

Go language is a high-performance language, but it doesn't mean that we don't need to care about performance. We still need to care about it. Here is a performance related tip for programming.

  • If you need to convert a number to a string, use strconv Itoa () is better than FMT Sprintf () is about twice as fast
  • Try to avoid converting String to [] Byte. This conversion will lead to performance degradation.
  • If you use append() for a slice in the for loop, please expand the slice's capacity in place first, so as to avoid memory re sharing and the system automatically expands to the nth power of 2, but it is not used, thus wasting memory.
  • Using StringBuffer or StringBuild to splice strings is three to four orders of magnitude better than using + or + = to splice strings.
  • Use concurrent go routine as much as possible, and then use sync Waitgroup to synchronize sharding operations
  • Avoid allocating memory in hot code, which can cause the gc to be busy. Use sync as much as possible Pool to reuse objects.
  • Use lock free operation, avoid using mutex, and use sync/Atomic package as much as possible. (for topics related to lockless programming, see< Implementation of lockless queue >Or< Implementation of lockless Hashmap>)
  • Use I/O buffer. I/O is a very, very slow operation. Use bufio Newwrite() and bufio Newreader () can bring higher performance.
  • For fixed regular expressions in for loop, be sure to use regexp Compile() compiles regular expressions. Performance will increase by two orders of magnitude.
  • If you need a higher performance protocol, you should consider using it protobuf or msgp Instead of JSON, because reflection is used in JSON serialization and deserialization.
  • When you use map, the integer key will be faster than the string key, because the integer comparison is faster than the string comparison.
This article is not written by myself. It reprints the left ear mouse blog and its source Cool shell – CoolShell

Keywords: Go

Added by sillyman on Sun, 06 Feb 2022 21:41:59 +0200