5

Preface

As a newcomer to the Go language, I am curious when I see all the "weird" code; for example, the few methods I have seen recently; the pseudo code is as follows:

func FindA() ([]*T,error) {
}

func FindB() ([]T,error) {
}

func SaveA(data *[]T) error {
}

func SaveB(data *[]*T) error {
}

I believe that most of the novices who are just getting started with Go are also confused when they see this code. The most confusing thing is:

[]*T
*[]T
*[]*T

In this way, the statement of the slice, do not look at the latter two ways of writing; it is easy to understand []*T
[]*T more space than storing T itself. At the same time, 060ff5a9fb99d9 can modify the value of T within the method, while []T cannot.

func TestSaveSlice(t *testing.T) {
    a := []T{{Name: "1"}, {Name: "2"}}
    for _, t2 := range a {
        fmt.Println(t2)
    }
    _ = SaveB(a)
    for _, t2 := range a {
        fmt.Println(t2)
    }

}
func SaveB(data []T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

type T struct {
    Name string
}

For example, the above example prints

{1}
{2}
{1}
{2}

Only modify the method to

func SaveB(data []*T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

To modify the value of T:

&{1}
&{2}
&{1233}
&{2}

Example

[]*T *[]T 's focus on the difference between 060ff5a9fb9ab0 and 060ff5a9fb9ab2. Here are two append functions:

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}

Looking at the first one, the output is the result:

appendA [1000 2 3]
main [1000 2 3]

Explain that in the process of function transfer, the modification inside the function can affect the outside.


Let's look at another example below:

func appendB(x []int) {
    x = append(x, 4)
    fmt.Printf("appendA %v\n", x)
}

The end result is:

appendA [1 2 3 4]
main [1 2 3]

There is no external impact.

And when we adjust it again, we will find that it is different again:

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v\n", x)
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendA %v\n", x)
}

final result:

appendA &[1 2 3 4]
main [1 2 3 4]

It can be found that if the pointer of the slice is passed, the external data will be affected when the function append

slice principle

Before analyzing the above three situations, let's first understand the data structure slice

Looking directly at the source code, you will find that slice is actually a structure, but it cannot be accessed directly.

Source address runtime/slice.go

There are three important attributes:

Attributesmeaning
arrayThe array that stores data at the bottom layer is a pointer.
lenSlice length
capSlice capacity cap>=len

When it comes to slices, you have to think of arrays, which can be understood as follows:

Slicing is an abstraction of arrays, and arrays are the underlying implementation of slicing.

In fact, it is not difficult to see through the name of slice, it just cuts a part from the array; relative to the fixed size of the array, the slice can be expanded according to the actual usage.

So slices can also be obtained by "slicing" the array:

x1:=[6]int{0,1,2,3,4,5}
x2 := x[1:4]
fmt.Println(len(x2), cap(x2))

The length and capacity of x1

The length and capacity of x2

  • The length of x2 is easy to understand.
  • The capacity equal to 5 can be understood as the maximum length that can be used for the current slice.

Because the slice x2 is a reference to the array x1, the maximum capacity of the slice, which is 5, is excluded from the unreferenced position on the left of the underlying array.

The same underlying array

Take the code just now as an example:

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}

In the process of transfer function, main in x and appendA x in the function sections refer to the same array.

So in the function, x[0]=100 , main can also be obtained.

Essentially what is modified is the same piece of memory data.

Misunderstandings caused by value passing

In the above example, after calling the append appendB , you will find that the main function is not affected. Here I have slightly adjusted the sample code:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}
The main reason is to modify the slice initialization method so that the capacity is greater than the length. The specific reasons will be explained later.

The output is as follows:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5

main data seems to function really not affected; but attentive friend should note appendB function of x in append() after length + 1 becomes 4.

In the main function, the length has changed back to 3.

This detailed difference is why append() "looks like" does not take effect; as for why you want to say "seems", the code is adjusted again:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}

On the basis of just now, append ; the range of the slice is all the data of the array referenced by x.

Let's take a look at the execution result:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5
y [1 2 3 444 0] len=5,cap=5

It will magically find that y will print all the data appendB function has actually been written into the array, but why is x not obtained by itself?

It is easy to understand by looking at the picture:

  • In appendB , data is indeed added to the original array, and the length is also increased.
  • But because it is value transfer, slice structure is modified to 4, it only modifies the length of the copied object, and the length in main is still 3.
  • Since the bottom array is the same, a full-length slice is regenerated based on this bottom array to see the additional data.

So the essential reason here is because slice is a structure, and the value is passed. No matter how the length is modified in the method, it will not affect the original data (here refers to the two attributes of length and capacity).

Slicing expansion

One more thing to note:

I specifically mentioned that the example here has been slightly changed, mainly because the capacity of the slice is set to exceed the length of the array;

What happens if you don't do this special setting?

func TestAppendB(t *testing.T) {
    x:=[]int{1,2,3}
    //x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}

Output result:

appendB [1 2 3 444] len=4,cap=6
main [1 2 3] len=3,cap=3
y [1 2 3] len=3,cap=3

At this time, you will find that the y slice data in the main function has not changed. Why?

This is because the length and capacity are both 3 when initializing the x slice. When appendB function, you will find that there is no position.

At this time, the expansion will be carried out:

  • Copy the old data to the new array.
  • Append data.
  • Return the new data memory address to x in appendB

Since the same value is transmitted, so appendB of the underlying array of transducer sections main no effect on the function of the slice, it results in a final main data without any change in the function.

Pass slice pointer

Is there any way to have an impact on the outside even during expansion?

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendC %v\n", x)
}

The output is:

appendC &[1 2 3 4]
main [1 2 3 4] len=4,cap=6

At this time, the external slice can be affected, in fact, the reason is very simple;

As mentioned earlier, because slice itself is a structure, when we pass the pointer, the usual custom struct uses the pointer to modify the data in the function. The principle is the same.

Finally appendC points to the expanded structure. Because the pointer of x in the main function is passed, the x in the same main function also points to the structure.

Summarize

So to summarize:

  • A slice is an abstraction of an array, and the slice itself is also a structure.
  • When parameters are passed, the internal and external references of the function are the same array, so the modification of the slice will affect the outside of the function.
  • If expansion occurs, the situation will change, and the expansion will result in data copy; therefore, try to estimate the slice size to avoid data copy.
  • When regenerating slices or arrays, since the same underlying array is shared, the data will affect each other, which requires attention.
  • Slices can also pass pointers, but there are few scenes and unnecessary misunderstandings; it is recommended to pass by value, because the length and capacity can't take up much memory.

I believe that after using slices, you will find that it is very similar to Java in ArrayList . It is also based on an array implementation, and data copying will also occur. It seems that the language is only the choice used by the upper layer, and some general low-level implementations are similar to everyone.

At this time, when we look at the []*T *[]T *[]*T in the title, we will find that these have no connection, but they seem to be easy to bluff.


crossoverJie
5.4k 声望4k 粉丝