Golang学习笔记一Slice篇

1. 从数组说起

  数组是具有固定长度具有零个或者多个相同数据类型元素的序列。

  由于数组长度固定,在Go里很少直接使用。

1.1 定义数组

// 3种方式,声明,初始,省略号

// 变量arr1类型为[5]int
var arr1 [5]int

// 变量arr2类型为[3]int,同时初始化赋值
var arr2 [5]int = [5]int{1,2,3}

// 让编译器自己数,结果为[3]int
arr3 := [...]int{1,2,3}

// 错误例子,因为[3]int和[4]int是两种类型
arr3 := [4]int{1,2,3,4}

  • 数组长度是数组类型的一部分[3]int[5]int就是两种类型。
  • 数组长度必须是常量表达式,即这个表达式的值在编译阶段就可以确定。

1.2 查看数组基本信息

  • Go内置函数len,可以返回数组中元素个数。
arr1 := [...]{1,2,3}
lenth := len(arr1)

1.3 数组遍历

  • 数组中的每个元素是通过index来访问的。
arr1 := [3]{1,2,3}
for i,val := range arr1 {
    
}

1.4 数组作为函数参数

  • 数组是值类型,值类型意味着拷贝。

  • 注意!!!在其他语言中,数组是隐式的使用引用传递;Golang传参时,传入的参数会创建一个副本,使用这种方式传递大的数组会变的很低效。

  • 可以显式的给函数传递一个数组的指针。

// 1. 遍历数组,将数组中元素清零
fun zero (ptr *[32]byte) {
    for i := range ptr {
        ptr[i] = 0
    }
}

// 2. 利用数组指针,将数组元素清零
func zero (ptr *[32]byte) {
    *ptr = [32]byte{}
}

1.5 数组比较

  • 如果一个数组中的元素是可比较的,那么数组也是可比较的
  • 可以直接使用==和!=比较两个数组
a := [2]int{1,2}	// [2]int类型
b := [...]int{1,2}	// [2]int类型
c := [2]int{1,3}	// [2]int类型
fmt.Println(a==b,a==c,b==c)	// true,false,false

d := [3]int{1,2}	// [3]int类型
fmt.Println(a == d)	// 编译错误:不能比较[2]int和[3]int

2. 切片

  切片用于表示一个拥有相同类型元素可变长的序列,看上去像是没有长度的数组类型。

2.1 定义Slice

  通过数组定义

// 定义一个数组
arr := [...]int{0,1,2,3,4,5,6,7}

// 根据数组,定义该数组的view,即slice
s := arr[2:6]	// s={2,3,4,5},左开右闭

// s就是一个切片,它是数组arr的一个视图
// 总结来说,一个数组取它的slice,使用slice操作符arr[:]即可,看的是底层数组的全部景象

  直接定义切片

var s1 []int

s2 := []int{0,1,2,3,4}

s3 := make([]int,6)			// 默认len == cap == 6

s4 := make([]int,10,32)		// len == 10 ,cap == 32

2.2 查看切片基本信息

  • Go内置函数len和cap,用来返回slice的长度和容量。
// 定义一个数组
arr := [...]int{0,1,2,3,4,5,6,7}

// 根据数组,定义该数组的view,即slice
s := arr[2:6]	// s={2,3,4,5},左开右闭

fmt.Println(len(s))	// len(s) == 4 (2-5)

fmt.Println(cap(s))	// cap(s) == 6 (2-7)
  • 判断slice是否为空。
// slice 不能使用 == 操作符来比较两个切片元素是否相等
// 唯一允许的比较操作是和nil作比较
// 但是,slice为nil的情况有很多种
var s []int			// len(s) == 0, s == nil
s = nil				// len(s) == 0, s == nil
s = []int{nil}		// len(s) == 0, s == nil
s = []int{}			// len(s) == 0, s!=nil

// 所以一般情况不用 s == nil来判断slice是否为空,而是使用len(s) == 0来判断

2.3 切片相关操作

  • 切片遍历在语法上和数组是相似的
for _,s := range s1 {
    
}
  • append()函数追加元素
// 使用append追加元素可以超过capacity,它会触发slice底层数组的扩容机制
// 扩容就是重新分配长度更大的底层数组,原来的数组就会进行垃圾回收

// 由于值传递的关系,必须要使用一个新的slice接收append的返回值
// append的时候,ptr,len,cap有可能都会发生变化,必须接收新的ptr,len和cap
s := []int{0,1,2,3,4,5}
s = append(s,val)

// 下面的一种操作,可以达到删除slice中元素的目的
// ...表示变长参数列表,追加s[4:]切片后所有元素
s = append(s[:3],s[4:]...)

// append是从切片的len,向后进行扩展的操作
var ints []int = make([]int,2,5)	// len=2,cap=5
									// 底层数组:0 0 0 0 0
ints = append(ints,1)				// len=3,cap=5 
									// 底层数组:0 0 1 0 0

2.4 切片作为函数参数

  • 所谓切片是引用类型,是指切片作为函数的参数,操作的是同一个底层数组。(由于引用类型数据存放在堆上,值类型数据存放在栈上,因此切片数据存放在堆上,栈上保存堆在内存中的地址)

切片拷贝:

  1. 对于切片直接使用=拷贝,实际上是浅拷贝,只拷贝了切片在堆上的内存地址;sliceA = sliceB
  2. 对于切片深拷贝的需求,可以使用copy内置函数完成;copy(sliceA,sliceB)
	func nonempty(strings []string) []string {
	    i := 0
	    for _, s := range strings {
	        if s != "" {
	            strings[i] = s
	            i++
	        }
	    }
	    return strings[:i]
	}
  • golang中函数传递都是值传递,map、channel、slice都是引用传递,会传递指针值。参数传递时,切片进行浅拷贝(指针、cap、len),在函数内部发生append扩容等操作时,会改变原切片的值。https://zhuanlan.zhihu.com/p/111796041
	//定义一个函数,给切片添加一个元素
	func addOne(s []int) {
	    s[0] = 4  // 可以改变原切片值
	    s = append(s, 1)  // 扩容后分配了新的地址,原切片将不再受影响
	    s[0] = 8 
	}
	var s1 = []int{2}   // 初始化一个切片
	addOne(s1)          // 调用函数添加一个切片
	fmt.Println(s1)     // 输出一个值 [4]

3. 面试

3.1. 数组和切片

3.1.1. 数组

  1. Go中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份
  2. 如果Go中的数组作为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针。

3.1.2. 切片

  1. 切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。
  2. 切片可能会在堆上分配内存,本身不是动态数组或者数组指针,内部是通过指针引用底层数组,切片本身是一个只读对象,本身没有数据,底层数组才有数据,类似于数组指针的一种封装,是引用类型
  3. 使用for range 遍历slice的时候,拿到的value其实是切片的值拷贝,每次打印出来的value地址不变
  4. slice扩容时,当cap小于1024的时候,每次扩容都会变成原来容量的2倍;当大于1024的时候,每次变为之前的1.25倍

3.1.3. 切片扩容

  1. 预估扩容后的newCap
// 预估规则:
if oldCap * 2  < newCap
	直接分配内存
else 
	if oldLen < 1024  newCap = oldCap * 2
	if oldLen > 1024 newCap = oldCap *1.25
  1. newCap个元素需要多大的内存?
// 预估到的newCap只是扩容后元素的个数,具体分配多大的内存呢?
// newCap * sizeof(T)吗?
// 事实上,许多编程语言中,申请分配内存,并不是直接和操作系统交涉,而是和语言自身实现的内存管理模块。内存管理模块会提前申请一批常用的内存,管理起来,需要申请 内存时内存管理模块会帮我们匹配到最接近的规格
  1. 匹配到合适的内存规格

3.2 切片为什么叫切片?与Java中动态数组有区别吗?

TODO: 以java 中StringBuffer和ArrayList为例作比较

你可能感兴趣的:(go,golang)