[]*T *[]T *[]*T 傻傻分不清楚

[]*T *[]T *[]*T 傻傻分不清楚

前言

作为一个 Go 语言新手,看到一切”诡异“的代码都会感到好奇;比如我最近看到的几个方法;伪代码如下:

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

相信大部分刚入门 Go 的新手看到这样的代码也是一脸懵逼,其中最让人疑惑的就是:

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

这样对切片的声明,先不看后面两种写法;单独看 []*T 还是很好理解的:
切片中存放的是所有 T 的内存地址,会比存放 T 本身来说要更省空间,同时 []*T 在方法内部是可以修改 T 的值,而[]T 是修改不了。

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 }

比如以上例子打印的是

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

只有将方法修改为

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

才能修改 T 的值:

&{1} &{2} &{1233} &{2} 示例

下面重点来看看 []*T 与 *[]T 的区别,这里写了两个 append 函数:

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) }

先看第一种,输出是结果是:

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

说明在函数传递过程中,函数内部的修改能够影响到外部。

下面我们再看一个例子:

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

最终结果却是:

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

没有影响到外部。

而当我们再调整一下会发现又有所不同:

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) }

最终的结果:

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

可以发现如果传递切片的指针时,使用 append 函数追加数据时会影响到外部。

slice 原理

在分析上面三种情况之前,我们先来了解下 slice 的数据结构。

直接查看源码会发现 slice 其实就是一个结构体,只是不能直接对外访问。

[]*T *[]T *[]*T 傻傻分不清楚

源码地址 runtime/slice.go

其中有三个重要的属性:

属性 含义
array   底层存放数据的数组,是一个指针。  
len   切片长度  
cap   切片容量 cap>=len  

提到切片就不得不想到数组,可以这么理解:

切片是对数组的抽象,而数组则是切片的底层实现。

其实通过切片这个名字也不难看出,它就是从数组中切了一部分;相对于数组的固定大小,切片可以根据实际使用情况进行扩容。

所以切片也可以通过对数组"切一刀"获得:

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

[]*T *[]T *[]*T 傻傻分不清楚

其中 x1 的长度与容量都是6。

x2 的长度与容量则为3和5。

x2 的长度很容易理解。

容量等于5可以理解为,当前这个切片最多可以使用的长度。

因为切片 x2 是对数组 x1 的引用,所以底层数组排除掉左边一个没有被引用的位置则是该切片最大的容量,也就是5。

同一个底层数组

以刚才的代码为例:

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) }

[]*T *[]T *[]*T 傻傻分不清楚

在函数传递过程中,main 中的 x 与 appendA 函数中的 x 切片所引用的是同个数组。

所以在函数中对 x[0]=100,main函数中也能获取到。

[]*T *[]T *[]*T 傻傻分不清楚

本质上修改的就是同一块内存数据。

值传递带来的误会

在上述例子中,在 appendB 中调用 append 函数追加数据后会发现 main 函数中并没有受到影响,这里我稍微调整了一下示例代码:

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)) }

主要是修改了切片初始化方式,使得容量大于了长度,具体原因后续会说明。

输出结果如下:

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

main 函数中的数据看样子确实没有受到影响;但细心的朋友应该会注意到 appendB 函数中的 x 在 append() 之后长度 +1 变为了4。

而在 main 函数中长度又变回了3.

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyxffz.html