GoLang学习笔记之基础语法(二):数组与切片

目录

一、数组

1.1 数组的声明和初始化

1.2 数组的类型

1.3 数组的遍历

二、切片

2.1切片的声明和初始化

2.2 切片的本质与理解

2.3 切片的长度及容量

2.4 两个有关切片的函数append()与copy()

2.4.1 append()函数

2.4.2 copy()函数

三、练习

3.1 实现一个在切片任意位置删除任意元素的函数

3.2 实现一个在切片任意位置插入任意元素的函数

总结


前言

数组和切片在go语言中的使用十分广泛,其中数组与其他编程语言比较相似,在本文中篇幅可能会较少介绍,而切片作为go语言独有的类型,本文重点讲解对切片的理解与使用


一、数组

1.1 数组的声明和初始化

数组的声明和初始化方式如下:

// 1.数组的声明和初始化
	a1 := [2]int{1,2}  // 短变量声明,适用于局部变量
	//var a1 = [2]int{1,2} // 通过关键字var声明并初始化
	a2 := new([2]int) // 注意,new函数返回的是指针
	a3 := [...]int{1,2,3} //使用[...]会自动计算数组的长度
	a4 := [5]int{1:1,4:4} // 还可以使用key:value的方式对特定位置进行初始化
	fmt.Println(a1,*a2,a3,a4) // 输出:[1 2] [1 2] [1 2 3] [0 1 0 0 4]

1.2 数组的类型

go语言中,数组的类型与其他语言不太相似,其类型不仅包括数组内部数据的类型,还包括数组的长度

// 数组的类型:数组的类型包括其容量和其内部元素的数据类型
	fmt.Printf("%T\n",a4) //输出:[5]int,即a4的类型是[5]int,而不是int

此外,数组还是一种值类型 ,即对其进行赋值时,内存会进行一份值拷贝。

// 数组是值类型,赋值的本质是值拷贝
	a5 := a4
	fmt.Println(a4) //输出:[0 1 0 0 4]
	a5[1] = 100
	fmt.Println(a5) //输出:[0 100 0 0 4]
	fmt.Println(a4) //输出:[0 1 0 0 4]

1.3 数组的遍历

for i:=0;i<len(a1);i++{
		fmt.Printf("%v ",a1[i]) // 输出:1 2
	}
	fmt.Println("")

	for _,v := range a2{
		fmt.Printf("%v ",v) // 输出:1 2
	}
	fmt.Println("")



二、切片



2.1切片的声明和初始化

// 切片的初始化方式
	s1 := []int{1,2}
	s2 := make([]int,0,4) //make(type,len,cap)可用于初始化一个长度为len,容量为cap的切片
	a := [2]int{1,2}
	s3 := a[:]  // [start:end]是一种对数组或切片进行切片的操作,区间是左闭右开的
	fmt.Println(s1,s2,s3) //输出:[1 2] [] [1 2]

需要注意的是,当make()函数只传len参数时,默认cap会等于len,我们常用的方法是

s := make(type,0,cap) 



2.2 切片的本质与理解

  • 切片的本质是对其底层数组的上层封装,我们可以把切片看成是一个框,它框着底层数组,但切片本身是不存放任何数据的,数据存储在其底层数组中,改变切片的值实际上修改的是其底层数组,切片本质如下图所示:

  • 切片是引用类型,本质是对其底层数组的引用
    // 理解切片的本质:对其底层数组的封装,切片是引用类型
    	s4 = []int{1,2,3}
    	fmt.Printf("slice s4 len:%v,cap:%v\n",len(s4),cap(s4)) //输出:slice s4 len:3,cap:3
    	// 本来s4的长度为3,容量为5,但是在赋值变化后,其容量变为3,是由于其底层的数组变了

2.3 切片的长度及容量

  • 切片的长度:切片的长度即为切片中所包含的元素的个数
  • 切片的容量:切片的容量即切片中第一个元素到其底层数组最后一个元素的距离
  • 切片的长度与容量关系如下图所示:

  • 代码示例如下:
    // 对于切片长度,容量的理解
    	s4 := make([]int,3,5) // 构造长度为1,容量为5的切片,如果参数只有一个,则len=cap
    	fmt.Printf("slice s4 len:%v,cap:%v\n",len(s4),cap(s4)) //输出:slice s4 len:3,cap:5
    	s5 := s4[2:] //注意:子切片的索引不能超过其父切片的索引范围,否则会引起报错,比如这里s4[3:]是不对的,因为s4的最大索引为2
    	fmt.Printf("slice s5 len:%v,cap:%v\n",len(s5),cap(s5)) //输出:slice s5 len:1,cap:3
    	// 可以看出:切片的长度,就是该切片中元素的个数,切片的容量,是从切片的第一个元素到其底层数组最后一个元素的距离
  • 切片的扩容策略:切片是可以进行动态扩容的,其动态扩容遵循一定规则,伪代码如下:
    // 扩容标准,假设切片旧容量为old_cap,想要扩容到new_cap,最终的实际容量为cap,则:
    	//1. if new_cap > 2 * old_cap,cap = new_cap
    	//2. else if new_cap < 1024,cap = 2 * old_cap
    	//3. else ,先让cap = old_cap,然后循环叠加:cap += cap/4,直到cap>=new_cap

2.4 两个有关切片的函数append()与copy()

2.4.1 append()函数

  • append()函数用于给一个切片末尾追加一个或多个同类型的元素,其定义如下:
    func append(slice []Type,elem... Type)[]Type
  • append()函数的追加规则:若切片的容量足够追加元素,则在切片末尾进行追加,并返回原来的切片(即底层数组没有改变),如果输入的切片容量不足以追加元素,则会在内存中重新开辟一个足够容量的底层数组,并封装成切片后追加元素并返回(此时返回的切片已经不是原来的切片,其底层数组已经改变)
  • append()函数代码示例如下:
    // append()函数
    	s6 := make([]int,1)
    	s7 := []int{1,2,3}
    	fmt.Println(s6,s7) //输出:[0] [1 2 3]
    	fmt.Printf("s6 addr:%p\n",s6 //输出:s6 addr:0xc000016158
    	s8 := append(s6,s7...)
    	fmt.Println(s8)//输出:[0 1 2 3]
    	fmt.Printf("s8 addr:%p\n",s8) //输出:s8 addr:0xc000020220,与s6已经不同
    	// 使用append函数追加元素时候,如果切片的底层数组容量不够,则返回的一个新的切片,即其底层数组是在内存中另外开辟的一块容量足够的数组

2.4.2 copy()函数

  • 由于切片是引用类型,一旦修改切片的值,其底层数组就会改变,导致所有与该数组相关的切片的值都会改变,牵一发而动全身
  • copy()函数会在内存中开辟一块新的数组,并把原切片内容复制给新的切片,其函数定义如下
    func copy(dst []Type,src []Type)int
  • copy()函数使用示例如下
    // copy()函数
    	// 由于切片是引用类型,一旦修改切片的值,其底层数组就会被改变,导致所有与该数组相关的子切片都会被改变,牵一发而动全身
    	// 使用copy()函数,可以再内存中复制一块原切片的底层数组,然后返回一个新的切片,这样修改新切片就不会影响原切片及其底层数组
    	s9 := make([]int,len(s8))
    	copy(s9,s8) //copy函数不会扩容,dst切片的长度必须大于等于src的长度
    	fmt.Println(s8,s9) //输出:[0 1 2 3] [0 1 2 3]
    	s9[0] = 100
    	fmt.Println(s8,s9)  //输出:[0 1 2 3] [100 1 2 3]
  • 注意:copy函数不会为目的切片自动扩容,它只会根据目的切片的长度,去将原切片中对应长度的内容复制到目的切片。

三、练习

3.1 实现一个在切片任意位置删除任意元素的函数

  • 我们首先来看下面这段代码
func delete(s []int,start,end int)([]int,error){
	if start < 0 || end > len(s)-1{
		return s,errors.New("please input the right index\n")
	}
	s = append(s[0:start],s[end+1:]...)
	return s,nil
}

func main(){
// 下面展示了使用append函数删除切片元素对底层数组的影响
	// 可以看到,使用delete()函数删除切片元素后,其底层数组也会被改变
	s10 := make([]int,5,10)
	for i:=0;i<len(s10);i++{
		s10[i] = i
	}
	fmt.Println(s10) // 输出[0 1 2 3 4]
	fmt.Printf("slice s10 len:%v,cap:%v\n",len(s10),cap(s10)) // 输出slice s10 len:5,cap:10
	fmt.Printf("s10 addr:%p\n",s10) // 输出:s10 addr:0xc0000180f0
	s11,_ :=delete(s10,1,3)
	fmt.Println(s11) // 输出[0 4],删除元素成功
	fmt.Printf("slice s11 len:%v,cap:%v\n",len(s11),cap(s11)) //输出:slice s11 len:2,cap:10
	fmt.Printf("s11 addr:%p\n",s11)// 输出:s11 addr:0xc0000180f0,与s10一样,说明他们的底层数组是一样的
	fmt.Println(s10) //输出:[0 4 2 3 4]
}
  • 我们通过运行上述代码,发现切片对应位置的元素确实被删除了,但是也引发了一个问题,就是切片所封装的底层数组也被改变,为什么最后输出是[0 4 2 3 4]呢,我们通过下面图示来理解一下上述代码的全过程。 
  • 首先我们必须知道的一点是,go语言中的函数传参永远都是值拷贝方式传参,也就是说形参是实参的值拷贝,因此,我们可以通过下图解释切片作为参数传入函数时形参与实参的关系

  • 随后我们对形参开始使用append()函数进行切片重塑

 

  • 最后,我们把重塑后的切片赋值给形参s,并传出函数。底层数组实际上被改变成了[0 4 2 3 4]

  • 那么,我们有没有办法让切片删除指定元素后,不改变原数组的值呢?想要做到这个,就只能另外在数组中开辟一块空间,这样才不会修改到原数组的值。下面给出代码实现
    func delete2(s []int,start,end int)([]int,error){
    	result := make([]int,0,cap(s))
    	if start < 0 || end > len(s)-1{
    		return s,errors.New("please input the right index\n")
    	}
    	result = append(result,s[0:start]...)
    	result = append(result,s[end+1:]...)
    	return result,nil
    }

3.2 实现一个在切片任意位置插入任意元素的函数

  • 有了前面删除操作的解释,插入操作的原理也是相似的,这里直接给出代码实现
    func insert(s []int,index int,data... int)([]int,error){
    	result := make([]int,0,cap(s))
    	if index < 0 || index > len(s) - 1{
    		return s,errors.New("please input the right index\n")
    	}
    
    	result = append(result,s[:index]...)
    	result= append(result,data...)
    	s = append(result,s[index:]...)
    	return s,nil
    
    }



总结

数组的类型不仅包括其内部的数据类型,还包括其长度,数组是一种值类型,切片实质上是对底层数组的上层封装,其内部不存储数据,数据真正存储在其底层数组,可以将切片理解成一个框,框住底层数组。append()函数在追加元素时,若原切片容量足够,则在其后追加,若容量不够,则在内存中开辟一块新的空间。copy()函数会在内存中开辟一块新的空间,但其不会为目标切片自动扩容,因此使用copy()前必须保证目标切片容量足够。

以上就是本人对go语言中数组和切片的一些浅薄见解,如有错误还望指正。

天道酬勤,让我们一起努力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值