浅谈 Go中的 Array 和 Slice

  1.  Go中参数传递的方式只有传值一种方式,不像C++还有传引用的方式

 

Array

声明一个 array的方式为: 

var variable_name [SIZE] variable_type 例子: var arr[10] int
值得注意的是,这里的size 作为明确的参数,需要显示的传进来, arr[10] int 和 arr[12]int 是不能使用简单的方式互转的

Array,进行赋值或者传参都是拷贝值,不影响原始数组:
例子:

func enterAndLeaveCall(tag string) string {
	fmt.Printf("\nenter  %v<<<<<\n", tag)
	return fmt.Sprintf("\nleave  %v>>>>>>\n", tag)
}


func testArrayChange(bArr [6]int) {
	defer fmt.Printf(enterAndLeaveCall("testArrayChange"))

	fmt.Printf(" before change array: %v,address %p \n", bArr, &bArr)

	func(cArr [6]int) {
		cArr[0] = -100
		fmt.Printf(" change array: %v, address %p \n", cArr, &cArr)
	}(bArr)

	fmt.Printf(" after change array: %v,address %p \n", bArr, &bArr)
}

func main() {

	var tmpArr = [...]int{1, 2, 3, 4, 5, 6}
	var tmpSlice = tmpArr[:]

	var noLengthBArr=[]int{1,2,3}

	testArrayChange(tmpArr)

}

输出的结果是

 

enter  testArrayChange<<<<<
 before change array: [1 2 3 4 5 6],address 0xc0000ac090 
 change array: [-100 2 3 4 5 6], address 0xc0000ac0f0 
 after change array: [1 2 3 4 5 6],address 0xc0000ac090 

leave  testArrayChange>>>>>>

可以看到,Array的原始值没有改变,说明函数传参的array 跟原始Array不是同一个Array ,是深拷贝出来的另一个array

 

有时候我们会避免传参的时候进行,太多的拷贝,这时候,我们会想直接通过传指针的方式来进行传参,这样的话,就不需要进行深拷贝了。

我们看看演示代码

 

func testArrayWithPoint(tmpArray *[]int)  {
	defer fmt.Printf(enterAndLeaveCall("testArrayWithPoint"))
	fmt.Printf("%v , address %p \n ",*tmpArray,tmpArray)

	func (bArr *[]int){
		(*bArr)[1]=30
		fmt.Printf("%v , address %p \n ",*bArr,bArr)

	}(tmpArray)

	/*(*tmpArray)[10]=300; // out of bounds*/
	fmt.Printf("%v , address %p \n ",*tmpArray,tmpArray)
	fmt.Printf("use len: %v \n",len(*tmpArray))

}

func main() {

	var tmpArr = [...]int{1, 2, 3, 4, 5, 6}
	var tmpSlice = tmpArr[:]

	var noLengthBArr=[]int{1,2,3}

	// testArrayChange(tmpArr)

	testArrayWithPoint(&noLengthBArr)
}

输出的结果为:

 

enter  testArrayWithPoint<<<<<
[1 2 3] , address 0xc0000a6020 
 [1 30 3] , address 0xc0000a6020 
 [1 30 3] , address 0xc0000a6020 
 use len: 3 

leave  testArrayWithPoint>>>>>>

可以看到,通过指针传参的方式,函数里面修改了数组内容,也能反映到外部地址,因为传参中,两者指向的地址是一致的

那么通过这种方式,是不是就能解决传参拷贝的问题了,是,算是能解决深拷贝效率不高问题,但指针传值略显复杂,还不能限制index视图。

 

 

Slice 

Slice 也是类似于指针的方式传递数组的值,在值拷贝或者函数参数传递的时候不会引起数组内容的拷贝,slice 和原数组,底层使用的是同一个数组,所以slice的改动,会引起 原数组的改变

例子:

 

func testSliceChange(aSlice []int) {
	defer fmt.Printf(enterAndLeaveCall("testSliceChange"))

	fmt.Printf("slice value %v,address is %p\n", aSlice,aSlice)
	//fmt.Printf("unsafe Point %v \n", unsafe.Pointer(&aSlice))

	func(tSlice []int) {
		tSlice[0] = 99
		fmt.Printf("value is %v, slice address %p \n ",tSlice, tSlice)
	}(aSlice)

	fmt.Printf(" slice value %v,address is %p\n", aSlice, aSlice)
	//bSlice:=aSlice[1:2]
	//fmt.Printf("%v \n",bSlice[4])


}



func main() {

	var tmpArr = [...]int{1, 2, 3, 4, 5, 6}
	var tmpSlice = tmpArr[:]

	var noLengthBArr=[]int{1,2,3}

	//testArrayChange(tmpArr)

	//testArrayWithPoint(&noLengthBArr)

	//noLengthBArr=append(noLengthBArr,30,3)

	testSliceChange(tmpSlice)

}

 

输出:

 

enter  testSliceChange<<<<<
slice value [1 2 3 4 5 6],address is 0xc0000ac060
value is [99 2 3 4 5 6], slice address 0xc0000ac060 
  slice value [99 2 3 4 5 6],address is 0xc0000ac060

leave  testSliceChange>>>>>>

 

可见 slice能改变原数组的值

 

Slice 的创建方式

序号方式代码示例
1直接声明var slice []int
2newslice := *new([]int)
3字面量slice := []int{1,2,3,4,5}
4makeslice := make([]int, 5, 10)
5从切片或数组“截取”slice := array[1:5] 或 slice := sourceSlice[1:5]

 

Slice 的常见操作:

1)拼接两个切片

a = append(a, b...)

(2)复制一个切片

b = append([]T(nil), a...)
b = append(a[:0:0], a...)

(3)删除切片的第i~第j-1个元素([i,j)),如果切片的元素是指针或者具有指针成员的结构体,需要避免内存泄露问题,此时需要修改删除切片元素的代码如下:

for k, n := len(a)-j+i, len(a); k < n; k++ {
    a[k] = nil // 或该类型的零值}
a = a[:len(a)-j+i]

(4)删除第i个元素a = append(a[:i], a[i+1:]...)

同样的,为了避免内存泄露

copy(a[i:], a[i+1:])
a[len(a)-1] = nil // or the zero value of Ta = a[:len(a)-1]

(5)弹出切片最后一个元素,即出队列尾(pop back)

x, a = a[len(a) - 1], a[:len(a)-1]

(6)弹出切片第一个元素,即出队列头(pop)

x, a = a[0], a[1:]

(7)在第i个元素前插入一个切片

ba = append(a[:i], append(b, a[i:]...)...)

(8)切片乱序(Go 1.10以上)

for i := len(a) - 1; i > 0; i-- { j := rand.Intn(i + 1) // 生成一个[0,i+1)区间内的随机数 a[i], a[j] = a[j], a[i] }


浅谈 Slice 的内存结构


s 为 一个slice

根据上图,我们可以通过代码来验证 slice的内存结构

一个常见的思路是,怎么从slice获取到 指针,通过指针来访问length, capacity, 和 底层的[]int ,而不是简单通过len(),cap()和索引方式取得slice的值

 

先上整体代码

 

 

func pointerCaclu(tmpSlice []int) {
	defer fmt.Printf(enterAndLeaveCall("pointerCaclu"))
	p1 := unsafe.Pointer(&tmpSlice)

	fmt.Printf("change to Slice Header %v \n",*(*reflect.SliceHeader)(p1))

	fmt.Printf("slice addr is %p,slice[0] addr is %p ,and point is %v \n",&tmpSlice,&(tmpSlice[0]),p1)

	// **[]int ,指的是 *(*[]int) ,  **(**[]int) 就是 []int了
	// unsafe Point 转成啥样的指针都行,但是看意义
	var p2 = unsafe.Pointer(uintptr(p1) + uintptr(8))
	var p3 = unsafe.Pointer(uintptr(p1) + uintptr(16))
	fmt.Printf("len is %v,cap is %v \n", *((*int)(p2)), *((*int)(p3)))

	fmt.Printf("%v,  %v,value is  %v\n ", p1, uintptr(p1), **((**int)(p1)))



	//var forceGetArrayP = **(**[20]int)(p1)
	//for i := 0; i < 10; i++ {
	//	fmt.Printf("%v  ", forceGetArrayP[i])
	//}
}

 

输出内容为:

 

 

enter  pointerCaclu<<<<<
change to Slice Header {824634466400 6 6} 
slice addr is 0xc0000b0160,slice[0] addr is 0xc0000b6060 ,and point is 0xc0000b0160 
len is 6,cap is 6 
0xc0000b0160,  824634442080,value is  99
 
leave  pointerCaclu>>>>>>

fmt.Printf("%p ",&v) 输出的是v 指针的表示的值 

 怎么获取, 获取一个value的指针值,并为了能对其进行加减操作,把其转为uintptr.

把 slice 的值 ,转为内部实现的SliceHeader 的指针值,可以看到,

slice 里面指针的值,和len,cap等信息。

 

 

 

 

其中struct 的地址和struct里面的第一个变量的地址相同

test:

 

func printStructPoint()  {
	/*struct 的地址,和其第一个变量一致,跟数组一样的逻辑*/
	defer fmt.Printf(enterAndLeaveCall("printStructPoint"))
	tP:=TestStrP{num: 10,name: "apple"}
	fmt.Printf("tp point value %p, tp inner element num address %v, another name address %v \n",&tP,&(tP.num),&(tP.name))
}

 

输出

 

enter printStructPoint<<<<<
tp point value 0xc000094000, tp inner element num address 0xc000094000, another name address 0xc000094008

leave printStructPoint>>>>>>

 

 

 

slice 内部运行时候实现是以SliceHeader实现的

 

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

 

p1 := unsafe.Pointer(&tmpSlice) 获取到slice 的指针值, 而从上图可以知道,这个地址获取到slice整体指针,这个地址和 Data的地址(就是底层数组指针的值是一致的),和 slice 里,而这个值 &(tmpSlice[0]) 是不一样的

 

var p2 = unsafe.Pointer(uintptr(p1) + uintptr(8))
var p3 = unsafe.Pointer(uintptr(p1) + uintptr(16))
fmt.Printf("len is %v,cap is %v \n", *((*int)(p2)), *((*int)(p3)))

其中 64位机器,地址总线也是64位的,也就是每个指针地址相隔8个字节,(把 指向底层数组的指针值叫 P,  由于 &slice 的值是  &P, 所以 uintptr(unsafe.Pointer(&slice)+8 为 &len,  uintptr(unsafe.Pointer(&slice)+16 为 &cap

 

 

所以通过 unsafe.Point(&slice)指针,直接构造出slice 也是可以的

 

unc constructWithSliceHeader(tmpSlice []int) {
	defer fmt.Printf(enterAndLeaveCall("constructWithSliceHeader"))
	var bWithSliceHead = reflect.SliceHeader{Len: len(tmpSlice) + 10, Cap: len(tmpSlice) + 10,
		Data: uintptr(unsafe.Pointer(&tmpSlice))}

	var cSlice = **(**[]int)(unsafe.Pointer(&bWithSliceHead))

	for i := 0; i < len(cSlice) /*what if put 11 */ ; i++ {
		fmt.Printf("%v ", cSlice[i])
	}
	fmt.Printf("\n");

	var sliHeadP=*(*reflect.SliceHeader)(unsafe.Pointer(&tmpSlice)) // 地址,长度,容量

	fmt.Printf("%v \n",sliHeadP)
	fmt.Printf("%v \n",*((*[6]int)(unsafe.Pointer(sliHeadP.Data))))

}

输出

 

enter constructWithSliceHeader<<<<<
99 2 3 4 5 6
{824633844000 6 6}
[99 2 3 4 5 6]

leave constructWithSliceHeader>>>>>>

 

 

关于slice ,里面的常规方法, copy ,append,make 等方法,代码研读

直接看代码:  go/src/runtime/slice.go

 

 

切片中有一个需要注意的问题。
就是循环,值拷贝问题。

 

func main() {
	slice := []int{10, 20, 30, 40}
	for index, value := range slice {
		fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])

 

输出:

value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320

value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328

value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330

value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338

 

从上面结果我们可以看到,如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变。

 

 

所以在for 循环里面做闭包,参数传递是for 循环参数可能会有问题,需采用拷贝变量方式,不适合直接引用。

 

 

源码:

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func enterAndLeaveCall(tag string) string {
	fmt.Printf("\nenter  %v<<<<<\n", tag)
	return fmt.Sprintf("\nleave  %v>>>>>>\n", tag)
}

func testSliceChange(aSlice []int) {
	defer fmt.Printf(enterAndLeaveCall("testSliceChange"))

	fmt.Printf("slice value %v,address is %p\n", aSlice,aSlice)
	//fmt.Printf("unsafe Point %v \n", unsafe.Pointer(&aSlice))

	func(tSlice []int) {
		tSlice[0] = 99
		fmt.Printf("value is %v, slice address %p \n ",tSlice, tSlice)
	}(aSlice)

	fmt.Printf(" slice value %v,address is %p\n", aSlice, aSlice)
	//bSlice:=aSlice[1:2]
	//fmt.Printf("%v \n",bSlice[4])


}

func testArrayWithPoint(tmpArray *[]int)  {
	defer fmt.Printf(enterAndLeaveCall("testArrayWithPoint"))
	fmt.Printf("%v , address %p \n ",*tmpArray,tmpArray)

	func (bArr *[]int){
		(*bArr)[1]=30
		fmt.Printf("%v , address %p \n ",*bArr,bArr)
		//var c2=bArr
		//(*c2)[2]=39

	}(tmpArray)

	/*(*tmpArray)[10]=300; // out of bounds*/
	fmt.Printf("%v , address %p \n ",*tmpArray,tmpArray)
	fmt.Printf("use len: %v \n",len(*tmpArray))

}

func pointerCaclu(tmpSlice []int) {
	defer fmt.Printf(enterAndLeaveCall("pointerCaclu"))
	p1 := unsafe.Pointer(&tmpSlice)


	fmt.Printf("change to Slice Header %v \n",*(*reflect.SliceHeader)(p1))

	fmt.Printf("slice addr is %p,slice[0] addr is %p ,and point is %v \n",&tmpSlice,&(tmpSlice[0]),p1)


	// **[]int ,指的是 *(*[]int) ,  **(**[]int) 就是 []int了
	// unsafe Point 转成啥样的指针都行,但是看意义
	var p2 = unsafe.Pointer(uintptr(p1) + uintptr(8))
	var p3 = unsafe.Pointer(uintptr(p1) + uintptr(16))
	fmt.Printf("len is %v,cap is %v \n", *((*int)(p2)), *((*int)(p3)))


	var oneApple=*((**int)(p1))
	var d1= unsafe.Pointer(oneApple)
	//d2:= unsafe.Pointer(uintptr(d1)+8);
	//d2:=(*[12]int)d1
	var oneMe=(*[336]int)(d1)

	fmt.Printf("%v,  %v,value is  %v\n ", p1, uintptr(p1), oneMe[233])


	//var forceGetArrayP = **(**[20]int)(p1)
	//for i := 0; i < 10; i++ {
	//	fmt.Printf("%v  ", forceGetArrayP[i])
	//}
}

func selfConstructSlice(tmpSlice []int) {
	defer fmt.Printf(enterAndLeaveCall("selfConstructSlice"))

	var s1 = struct {
		addr uintptr
		len  int
		cap  int
		//tmp2 int
	}{addr: uintptr(unsafe.Pointer(&tmpSlice)) , len: 136, cap:336}
	var cSlice = **(**[]int)( unsafe.Pointer(&s1)) // unsafe Point 转成啥样的指针都行,但是看意义
	// **[]int ,指的是 *(*[]int) ,  **(**[]int) 就是 []int了

	fmt.Printf("len of cSlice %v\n", len(cSlice))

	for i := 0; i <  len(tmpSlice)  ; i++ {
		fmt.Printf("%v ", cSlice[i])
	}
}

func constructWithSliceHeader(tmpSlice []int) {
	defer fmt.Printf(enterAndLeaveCall("constructWithSliceHeader"))
	var bWithSliceHead = reflect.SliceHeader{Len: len(tmpSlice) + 10, Cap: len(tmpSlice) + 10,
		Data: uintptr(unsafe.Pointer(&tmpSlice))}

	var cSlice = **(**[]int)(unsafe.Pointer(&bWithSliceHead))

	for i := 0; i < len(cSlice) /*what if put 11 */ ; i++ {
		fmt.Printf("%v ", cSlice[i])
	}
	fmt.Printf("\n");

	var sliHeadP=*(*reflect.SliceHeader)(unsafe.Pointer(&tmpSlice)) // 地址,长度,容量

	var oneMe=(*[6]int)(unsafe.Pointer(sliHeadP.Data))
	fmt.Printf("%v \n",sliHeadP)
	fmt.Printf("%v \n",(*(oneMe))[2])

}

func testArrayChange(bArr [6]int) {
	defer fmt.Printf(enterAndLeaveCall("testArrayChange"))

	fmt.Printf(" before change array: %v,address %p \n", bArr, &bArr)

	func(cArr [6]int) {
		cArr[0] = -100
		fmt.Printf(" change array: %v, address %p \n", cArr, &cArr)
	}(bArr)

	fmt.Printf(" after change array: %v,address %p \n", bArr, &bArr)

	fmt.Printf("array addr is %p, and array 0 ele addr is %p \n",&bArr,&(bArr[0])) //the same
}

func testPrintAddressAndPoint()  {
	defer fmt.Printf(enterAndLeaveCall("testPrintAddressAndPoint"))

	var num=9
	var tmpP=&num;
	fmt.Printf("num %s %p, and point %v, and tmpP %p", &num,&num,tmpP,tmpP)
}

type TestStrP struct {
	num int
	name string
}

func printStructPoint()  {
	/*struct 的地址,和其第一个变量一致,跟数组一样的逻辑*/
	defer fmt.Printf(enterAndLeaveCall("printStructPoint"))
	tP:=TestStrP{num: 10,name: "apple"}
	fmt.Printf("tp point value %p, tp inner element num address %v, another name address %v \n",&tP,&(tP.num),&(tP.name))
}

func main() {

	var tmpArr = [...]int{1, 2, 3, 4, 5, 6}
	var tmpSlice = tmpArr[:]

	var noLengthBArr=[]int{1,2,3}

	testArrayChange(tmpArr)

	testArrayWithPoint(&noLengthBArr)

	//noLengthBArr=append(noLengthBArr,30,3)

	testSliceChange(tmpSlice)

	pointerCaclu(tmpSlice)

	selfConstructSlice(tmpSlice)

	constructWithSliceHeader(tmpSlice)

	//testPrintAddressAndPoint()
	//printStructPoint()

	var ber=[3]int{1,23,3}
	var cer=&ber
	fmt.Print(cer[2])


}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值