preface

As a Go novice, I was curious to see any “weird” code. For example, there are several methods THAT I’ve seen recently; The pseudocode is as follows:

func FindA(a) ([]*T,error){}func FindB(a) ([]T,error){}func SaveA(data *[]T) error{}func SaveB(data *[]*T) error{}Copy the code

I believe that most beginners in Go are confused when they see this kind of code. The most confusing part is:

[]*T
*[]T
*[]*T
Copy the code

This is a declaration of slicing, and I don’t want to write it either way; The []*T section contains all the memory addresses of T, which is more efficient than storing T itself. In addition, []*T can be modified inside the method, whereas []T cannot be modified.

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
}
Copy the code

For example, the above example printed is

{1}
{2}
{1}
{2}
Copy the code

Only change the method to

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

To change the value of T:

& {1} and {2} and {1233} and {2}Copy the code

The sample

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

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)
}
Copy the code

If we look at the first one, the output is:

appendA [1000 2 3]
main [1000 2 3]
Copy the code

During function transfer, changes inside the function can affect the outside.


Here’s another example:

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

The end result:

appendA [1 2 3 4]
main [1 2 3]
Copy the code

It doesn’t affect the outside.

And when we adjust it a little bit, it’s different:

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)
}
Copy the code

The final result:

appendA &[1 2 3 4]
main [1 2 3 4]
Copy the code

You can see that if you pass a pointer to a slice, appending the data using the append function will affect the outside.

Slice the principle

Before looking at these three scenarios, let’s look at slice’s data structure.

A direct look at the source code reveals that Slice is actually a structure, but not directly accessible.

Source address runtime/slice.go

There are three important attributes:

attribute meaning
array The underlying array of data, which is a pointer.
len Section length
cap Section capacitycap>=len

When you think about slices, you have to think about arrays.

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

It’s not hard to tell by the name slice, it’s just a slice from the array; As opposed to a fixed size array, slices can be expanded based on actual usage.

So slicing 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))
Copy the code

Where x1 has a length and a capacity of 6.

The length and capacity of x2 are 3 and 5.

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

Since slice X2 is a reference to array X1, the underlying array excluding the unreferenced position on the left is the maximum size of the slice, which is 5.

Same underlying array

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)
}
Copy the code

During function passing, the x in main and the X slice in appendA refer to the same array.

If x[0]=100, you can get it from main.

You’re essentially modifying the same piece of in-memory data.

Misunderstandings caused by value passing

In the example above, the append function that calls append in appendB does not affect the main function. Here I have adjusted the example code a little:

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))
}
Copy the code

The main reason is that the initialization mode of slices is modified, so that the capacity is larger 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
Copy the code

The data in main does seem to be unaffected; But attentive friends should notice that the x in the appendB function becomes 4 after append() with length +1.

And in main, the length goes back to 3.

This detail difference is why append() doesn’t seem to work; As for why “looks like”, I adjusted the code 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))
}
Copy the code

On top of that, I make another slice based on the x after append; The slice ranges from all the data in the array referenced by x.

Let’s take a look at the results:

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
Copy the code

It would be amazing to see that y prints out all the data. The data appended in appendB is actually written to the array, but why is x not fetching it?

It’s easy to understand when you look at the picture:

  • inappendBIt does append data to the original array and its length increases.
  • But since it’s value passing, sosliceEven if the length of this structure is changed to 4, it’s only changing the length of the object that was copied,mainThe length of PI is still 3.
  • Since the underlying array is the same, you can see the appended data by regenerating a full-length slice based on the underlying array.

So the fundamental reason for this is that slice is a structure that passes a value, and no matter how much length is changed in the method, it doesn’t affect the original data (in this case, the attributes of length and capacity).

Section capacity

One more note:

The example was changed a little bit. The size of the slice was set to exceed the size 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))
}
Copy the code

Output results:

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
Copy the code

The y slice in main has not changed. Why?

This is because the x slice was initialized with size and capacity of 3, and when data is appented in the appendB function, there is no place left.

This is where we expand:

  • Copy the old data into the new array.
  • Append data.
  • Returns the new data memory address toappendBThe x.

Similarly, since this is a value pass, a slice in appendB replacing the underlying array has no effect on the slice in main, leaving the data in main unchanged.

Transfer slice pointer

Is there any way to make an impact on the outside even as you expand?

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)
}
Copy the code

The output is:

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

And then the external slice can be affected, and the reason is very simple;

As I said before, since slice itself is a structure, when we pass a pointer, we use the same principle as when we pass a custom struct inside a function to modify data through a pointer.

Finally, the pointer to x in appendC points to the enlarged structure, and because we passed a pointer to x in main, the x in main also points to that structure.

conclusion

So to summarize:

  • A slice is an abstraction of an array, and the slice itself is a structure.
  • The argument is passed to the same array that is referenced inside and outside the function, so changes to the slice affect the outside of the function.
  • If capacity expansion occurs, the situation will change and data will be copied. So try to estimate the slice size and avoid copying data.
  • When reslicing slices or arrays, it is important to be aware that the data will affect each other because you are sharing the same underlying array.
  • Slices can also pass Pointers, but in fewer Settings and can be unnecessarily confusing; The recommended value is passed, the length and capacity do not take up much memory.

I believe that the use of slices will be found very similar to the ArrayList in Java, is also based on the array implementation, will also expand the occurrence of data copy; It seems that the language is just a choice for the upper level use, and some common low-level implementations are common to everyone.

Look at []*T *[]T *[]T *[]*T in the title.