、数组
十载专业网站建设公司历程,坚持以创新为先导的网站服务,服务超过数千家企业及个人,涉及网站设计、app开发定制、微信开发、平面设计、互联网整合营销等多个领域。在不同行业和领域给人们的工作和生活带来美好变化。
与其他大多数语言类似,Go语言的数组也是一个元素类型相同的定长的序列。
(1)数组的创建。
数组有3种创建方式:[length]Type 、[N]Type{value1, value2, ... , valueN}、[...]Type{value1, value2, ... , valueN} 如下:
复制代码代码如下:
func test5() {
var iarray1 [5]int32
var iarray2 [5]int32 = [5]int32{1, 2, 3, 4, 5}
iarray3 := [5]int32{1, 2, 3, 4, 5}
iarray4 := [5]int32{6, 7, 8, 9, 10}
iarray5 := [...]int32{11, 12, 13, 14, 15}
iarray6 := [4][4]int32{{1}, {1, 2}, {1, 2, 3}}
fmt.Println(iarray1)
fmt.Println(iarray2)
fmt.Println(iarray3)
fmt.Println(iarray4)
fmt.Println(iarray5)
fmt.Println(iarray6)
}
结果:
[0 0 0 0 0]
[1 2 3 4 5]
[1 2 3 4 5]
[6 7 8 9 10]
[11 12 13 14 15]
[[1 0 0 0] [1 2 0 0] [1 2 3 0] [0 0 0 0]]
我们看数组 iarray1,只声明,并未赋值,Go语言帮我们自动赋值为0。再看 iarray2 和 iarray3 ,我们可以看到,Go语言的声明,可以表明类型,也可以不表明类型,var iarray3 = [5]int32{1, 2, 3, 4, 5} 也是完全没问题的。
(2)数组的容量和长度是一样的。cap() 函数和 len() 函数均输出数组的容量(即长度)。如:
复制代码代码如下:
func test6() {
iarray4 := [5]int32{6, 7, 8, 9, 10}
fmt.Println(len(iarray4))
fmt.Println(cap(iarray4))
}
输出都是5。
(3)使用:
复制代码代码如下:
func test7() {
iarray7 := [5]string{"aaa", `bb`, "可以啦", "叫我说什么好", "()"}
fmt.Println(iarray7)
for i := range iarray7 {
fmt.Println(iarray7[i])
}
}
二、切片
Go语言中,切片是长度可变、容量固定的相同的元素序列。Go语言的切片本质是一个数组。容量固定是因为数组的长度是固定的,切片的容量即隐藏数组的长度。长度可变指的是在数组长度的范围内可变。
(1)切片的创建。
切片的创建有4种方式:
1)make ( []Type ,length, capacity )
2) make ( []Type, length)
3) []Type{}
4) []Type{value1 , value2 , ... , valueN }
从3)、4)可见,创建切片跟创建数组唯一的区别在于 Type 前的“ [] ”中是否有数字,为空,则代表切片,否则则代表数组。因为切片是长度可变的。如下是创建切片的示例:
复制代码代码如下:
func test8() {
slice1 := make([]int32, 5, 8)
slice2 := make([]int32, 9)
slice3 := []int32{}
slice4 := []int32{1, 2, 3, 4, 5}
fmt.Println(slice1)
fmt.Println(slice2)
fmt.Println(slice3)
fmt.Println(slice4)
}
输出为:
[0 0 0 0 0]
[0 0 0 0 0 0 0 0 0]
[]
[1 2 3 4 5]
如上,创造了4个切片,3个空切片,一个有值的切片。
(2)切片与隐藏数组:
一个切片是一个隐藏数组的引用,并且对于该切片的切片也引用同一个数组。如下示例,创建了一个切片slice0,并根据这个切片创建了2个切片 slice1 和 slice2:
复制代码代码如下:
func test9() {
slice0 := []string{"a", "b", "c", "d", "e"}
slice1 := slice0[2 : len(slice0)-1]
slice2 := slice0[:3]
fmt.Println(slice0, slice1, slice2)
slice2[2] = "8"
fmt.Println(slice0, slice1, slice2)
}
输出为:
[a b c d e] [c d] [a b c]
[a b 8 d e] [8 d] [a b 8]
可见,切片slice0 、 slice1 和 slice2是同一个底层数组的引用,所以slice2改变了,其他两个都会变。
(3)遍历、修改切片:
复制代码代码如下:
func test10() {
slice0 := []string{"a", "b", "c", "d", "e"}
fmt.Println("\n~~~~~~元素遍历~~~~~~")
for _, ele := range slice0 {
fmt.Print(ele, " ")
ele = "7"
}
fmt.Println("\n~~~~~~索引遍历~~~~~~")
for index := range slice0 {
fmt.Print(slice0[index], " ")
}
fmt.Println("\n~~~~~~元素索引共同使用~~~~~~")
for index, ele := range slice0 {
fmt.Print(ele, slice0[index], " ")
}
fmt.Println("\n~~~~~~修改~~~~~~")
for index := range slice0 {
slice0[index] = "9"
}
fmt.Println(slice0)
}
如上,前三种循环使用了不同的for range循环,当for后面,range前面有2个元素时,第一个元素代表索引,第二个元素代表元素值,使用 “_” 则表示忽略,因为go语言中,未使用的值会导致编译错误。
只有一个元素时,该元素代表索引。
只有用索引才能修改元素。如在第一个遍历中,赋值ele为7,结果没有作用。因为在元素遍历中,ele是值传递,ele是该切片元素的副本,修改它不会影响原本值,而在第四个遍历——索引遍历中,修改的是该切片元素引用的值,所以可以修改。
结果为:
~~~~~~元素遍历~~~~~~
a b c d e
~~~~~~索引遍历~~~~~~
a b c d e
~~~~~~元素索引共同使用~~~~~~
aa bb cc dd ee
~~~~~~修改~~~~~~
[9 9 9 9 9]
(4)、追加、复制切片:
复制代码代码如下:
func test11() {
slice := []int32{}
fmt.Printf("slice的长度为:%d,slice为:%v\n", len(slice), slice)
slice = append(slice, 12, 11, 10, 9)
fmt.Printf("追加后,slice的长度为:%d,slice为:%v\n", len(slice), slice)
slicecp := make([]int32, (len(slice)))
fmt.Printf("slicecp的长度为:%d,slicecp为:%v\n", len(slicecp), slicecp)
copy(slicecp, slice)
fmt.Printf("复制赋值后,slicecp的长度为:%d,slicecp为:%v\n", len(slicecp), slicecp)
}
追加、复制切片,用的是内置函数append和copy,copy函数返回的是最后所复制的元素的数量。
(5)、内置函数append
内置函数append可以向一个切片后追加一个或多个同类型的其他值。如果追加的元素数量超过了原切片容量,那么最后返回的是一个全新数组中的全新切片。如果没有超过,那么最后返回的是原数组中的全新切片。无论如何,append对原切片无任何影响。如下示例:
复制代码代码如下:
func test12() {
slice := []int32{1, 2, 3, 4, 5, 6}
slice2 := slice[:2]
_ = append(slice2, 50, 60, 70, 80, 90)
fmt.Printf("slice为:%v\n", slice)
fmt.Printf("操作的切片:%v\n", slice2)
_ = append(slice2, 50, 60)
fmt.Printf("slice为:%v\n", slice)
fmt.Printf("操作的切片:%v\n", slice2)
}
如上,append方法用了2次,结果返回的结果完全不同,原因是第二次append方法追加的元素数量没有超过 slice 的容量。而无论怎样,原切片slice2都无影响。结果:
slice为:[1 2 3 4 5 6]
操作的切片:[1 2]
slice为:[1 2 50 60 5 6]
操作的切片:[1 2]
1、值接收者和指针接收者
所谓指针接收者和值接收者这两个概念,用GO写了一阵子代码的人都了解了,这里只做简要说明一下,也就是对于一个给定结构,咱们对结构进行方法包装的时候,固定必传的参数,用来指向这个对象结构自身的一个参数,在go中也就是形式如下:
我们对结构体testStruct进行了包装,提供了两个方法,sum和modify,其中sum的方法接收者为a testStruct,这个就是值接收者,而modify的接收者为a *testStruct就是指针接收者,也就是说固定对象指针,一个传递的是指针地址,而另外一个直接传递的是结构值拷贝了
对指针有一定了解的,都可以知道,指针传递过去的,可以直接修改结构内部内容,而值传递过去的,无论如何修改这个接收者的数据,不会对原对象结构产生影响。而对于咱们包装结构对象的时候,到底是使用指针还是使用值接收者,这个实际上没有太大的定论,就我个人的观点来说,如果结构体占有的内存空间不大(kb级别),而又不需要修改内部的,同时结构对象内部没有同步对象比如(sync包中的mutex,rwlock,waitgroup等之类的结构的话,可以直接值传递,实际上值copy也没有咱们想象的那么慢,很多时候,都用指针,最后的gc回收扫描可能都比咱们这个传递copy的消耗大) p="" /kb级别),而又不需要修改内部的,同时结构对象内部没有同步对象比如(sync包中的mutex,rwlock,waitgroup等之类的结构的话,可以直接值传递,实际上值copy也没有咱们想象的那么慢,很多时候,都用指针,最后的gc回收扫描可能都比咱们这个传递copy的消耗大)
2、实现接口的值接收者和指针接收者有啥区别
也就是比如定义如下
这里面的值接收者和指针接收者有什么区别,这里咱来写一个测试
通过这个测试用例可以发现,指针接收者实现的接口可以同时支持转移到值接收者接口和指针接收者接口,而用值接收者实现的接口,则无法转移到使用指针接收者实现的接口,为啥子呢?目前网上或者各类资料上都是给的一个很官方很官方,而且很书面话难以理解的说明,大致意思如下:
这是目前网络或者各种资料上都是差不多是这样说的,看似讲了,实际上就说了一个结果,根本就没说出来一个为什么。这样的总结出来,一个初学者的角度来看,是很不好理解的,初学者要么就是死记硬背,要么就是生搬硬套,甚至直到写了好多好多代码了,都还没有搞明白一个为啥子,只是会用了而已,从长远来说这是不利于自身提高的。
有这两个本质点,咱们自己来思考一下,如果你来实现这个编译器的时候,用指针接收的时候,指针接收者,默认就能直接获取支持,而值接收者实现接口的咱们可以直接来一个解指针就变成了值,就能匹配上值接收者实现的接口了,反过来说,如果值接收者,此时要匹配指针接收者,如何匹配呢,取一个地址就变成了指针了,此时数据类型确实是匹配了,但是,地址指向的数据区不对了,因为我们刚刚说了值接收者拷贝了一个新值之后是完全的一个新的对象,这个新对象和原始对象一点关系都没有,咱们取地址,取的也是这个新对象地址,对这个地址进行操作,也是这个新对象的内部数据,和原始数据内部没有任何关系,所以由此就能推断出,这个是为啥子值接收者不能匹配上指针接收者,而指针接收者却可以匹配上值接收者了。
1、在某个作用域内部,所有定义的字符串的数据区相同
这个很好验证,代码如下:
2、字符串相加会产生一个新串
这个也很好验证
3、字符串真的是不可变的吗
实际上从字符串的结构
从这个结构,就能大致的推断出来,字符串设计成这样就不具备直接扩容+来增加新数据,而如果咱们直接使用string[index] = 'a',用这种方式,就不能编译通过,官方也确定说字符串是不可变的。那么真的是不可变的吗?
通过上面的结构,在加上go的slice切片的数据结构
由此可见,咱们可以将字符串通过指针方式强转为一个byte数组指针,然后通过byte切片来修改,试试
编译通过,运行报错
unexpected fault address 0xae2e27
fatal error: fault
这个错误,基本上就是一个内存的保护错误,是写异常,所以说明了,这个肯定做了内存写保护,那么直接修改一下内存区的属性,去掉他的写保护,就能写了
以下代码都是在Win平台,Go1.18,Win上修改内存权限属性,使用VirtualProtect,代码如下
此时运行,就能发现tstr的内容被咱们变了,这种情况实际上在实际开发中不具有实际意义,因为本身在语言层面,已经做了层层限制,咱们这是属于非法强制的操作方式,是流氓行为,那么是否有比较温和一点的操作方式呢?答案是有的,且往下看。
通过上面,我们已经用到了字符串结构,切片结构,要想字符串内容可变,那么咱们自己构造字符串的数据内容区域,且让这个数据区木有内存写保护不就行了,内容区可变,GO原生态的byte数组不就行嘛,所以咱们自己构造一下
此时我们直接修改buffer的内容,就是直接修改了str的数据内容了。而又不会像前面的一样遇到内存写保护
4、字符串转换优化时可能碰到的坑
通过前面讨论的字符串的可变性的方法,咱们可以知道,很多时候,[]byte到字符串的转变,可以直接构造其结构,而共享数据,从而达到减少数据内存copy的方式来进行优化,再使用这些优化的时候,一定需要注意,字符串或者数组的生命周期,是否会存在被改写的情况,从而导致前后不一致的问题。
比如下面这段代码:
大家可以猜想一下,这个最后里面的数据mmp中,"test"的value是多少,"abcd"的value是多少,然后想想为什么,且等端午之后,再来分解
Go 中数组的长度是不可改变的,而 Slice 解决的就是对不定长数组的需求。他们的区别主要有两点。
数组:
切片:
注意 1
虽然数组在初始化时也可以不指定长度,但 Go 语言会根据数组中元素个数自动设置数组长度,并且不可改变。切片通过 append 方法增加元素:
如果将 append 用在数组上,你将会收到报错:first argument to append must be slice。
注意 2
切片不只有长度(len)的概念,同时还有容量(cap)的概念。因此切片其实还有一个指定长度和容量的初始化方式:
这就初始化了一个长度为3,容量为5的切片。
此外,切片还可以从一个数组中初始化(可应用于如何将数组转换成切片):
上述例子通过数组 a 初始化了一个切片 s。
当切片和数组作为参数在函数(func)中传递时,数组传递的是值,而切片传递的是指针。因此当传入的切片在函数中被改变时,函数外的切片也会同时改变。相同的情况,函数外的数组则不会发生任何变化。
衍生类型,interface{} , map, [] ,struct等
map类似于java的hashmap,python的dict,php的hash array。
常规的for循环,可以用for k,v :=range m {}. 但在下面清空有一个坑注意:
著名的map[string]*struct 副本问题
结果:
Go 中不存在引用传递,所有的参数传递都是值传递,而map是等同于指针类型的,所以在把map变量传递给函数时,函数对map的修改,也会实质改变map的值。
slice类似于其他语言的数组(list,array),slice初始化和map一样,这里不在重复
除了Pointer数组外,len表示使用长度,cap是总容量,make([]int, len, cap)可以预申请 比较大的容量,这样可以减少容量拓展的消耗,前提是要用到。
cap是计算切片容量,len是计算变量长度的,两者不一样。具体例子如下:
结果:
分析:cap是计算当前slice已分配的容量大小,采用的是预分配的伙伴算法(当容量满时,拓展分配一倍的容量)。
append是slice非常常用的函数,用于添加数据到slice中,但如果使用不好,会有下面的问题:
预期是[1 2 3 4 5 6 7 8 9 10], [1 2 3 4 5 6 7 8 9 10 11 12],但实际结果是:
注意slice是值传递,修改一下:
输出如下:
== 只能用于判断常规数据类型,无法使用用于slice和map判断,用于判断map和slice可以使用reflect.DeepEqual,这个函数用了递归来判断每层的k,v是否一致。
当然还有其他方式,比如转换成json,但小心有一些异常的bug,比如html编码,具体这个json问题,待后面在分析。
列表是一种有序的集合,有点类似c里面的数组。它的特点是,可以随时向里面添加或删除其中的元素,在python中经常用来存放数据。列表的特点是中括号,内部元素用逗号隔开。
在这个列表中,可以放进去任何元素,不论你的元素是字符串、整型、浮点型、还是布尔值、空值,包括列表什么的,都可以放进去。
元素与元素之间,用逗号隔开。
列表会为每个元素分配序号,这个序号代表它的位置,称为索引(index),第一个元素的位置是0,第二个元素是1,以此类推。
使用索引获取列表中的值时,需要使用中括号来访问,在中括号前面加上列表名,中括号内部是元素的索引。
0代表第一个元素的位置,1代表第二个,-1代表倒数第一个,-2代表倒数第二个
使用 len() 函数,可以查看列表里面有多少个元素
在python中,列表的操作是非常的灵活的,我们可以向其中添加或删除元素。
添加使用 list.append() 函数
list.append() 函数是将元素插入到列表的末尾,当我们想在特定位置插入元素时可以使用 list.insert() 函数
list.insert() 函数接受两个参数,第一个参数是插入位置,第二个参数是要插入的元素。
需要注意的是,在使用append和insert时,必须在前面注明要操作的列表。就像上面的例子,我们要操作classmates这个列表,所以必须写成 classmates.append() 或 classmates.insert() ,如果不这么写,计算机就不知道你要往哪个列表中加入元素。
没有特殊情况的话,推荐使用append()函数添加元素,因为使用append的时候,元素默认加在列表尾部,不会造成其他元素索引值的改变。如果使用insert的话,就像上面的insert(1,'Tom'),在位置1插入'Tom'后,Tom后面所有的元素,索引值都加了一个1,列表中元素越多,受影响的程度越大,因此使用append()函数要比insert()函数更快。
删除列表中元素的方法有三种
del后面需要用索引的方式表明要删除的元素,也就是上面的例子,names[1]代表names中的第二个元素,使用del即可删除
list.pop() 函数与del差不多,都是使用索引值进行删除,只不过写法不同。
我们可以发现,执行 names.pop(1) 后,python shell打印出了第二个元素的值,也就是我们要删除的那个值,这是因为 pop() 这个函数,是有返回值的,有时候我们需要使用这个值,这个时候就可以用变量存起来。
这样我们就可以通过调用a而使用刚才删掉的元素了。
list.remove() 函数的作用是删除第一个匹配的元素,上面的例子中,names这个列表里面,有两个'Bob',remove函数只删除了第一个'Bob'。这就是 list.remove() 函数的特点。
有时候我们想使用列表的前10个元素,或者前n个元素,这时候就应该使用列表的切片。
切片和索引类似,都是使用中括号,区别是,索引中的中括号里面只有一个数,而切片不同。切片是切割列表,形成切割下来的部分形成新的列表。
切片: list[start:end:[step=1]] ,这就是切片的表达式,要求start和end两者必须有一个,step不是可以不指定,不指定的时候默认为1。
切片该怎么理解呢,start就是开始的位置,end就是结束的位置。切片有个特点是“取前不取后”,看上面那个例子可以发现,1作为start,3作为end,1代表第二个元素,3代表第四个元素,列表切片的时候,是不取后面的那个数字对应的元素的,也就是不取第四个元素,所以names[1:3]只取了第二个元素和第三个元素,这就是所谓的取前不取后。
再看下一个例子。
当不指定start或者end的时候,start默认为0,end默认为最后一个元素的索引值+1,因为“取前不取后”,要想取到最后一个元素,必须加个1才行。
上例中,用 len(numbers) 表示了最后一个元素的索引值,因为索引值从0开始,最后一个元素的索引值一定是列表内元素个数-1,根据“取前不取后”,在end位置上的数字应该+1,所以最后就等于 len(numbers) 了。
当不设定start和end的时候,就默认取所有的元素了。
当加入step,这个选项后,step代表步长,默认为1,设定成2的时候,就是隔一个取一个,设定成3时就是隔两个取一个。
上例中,第一个切片,start和end均未设定,因此从第一个元素开始,隔一个取一个,得到了所有奇数位置的元素。
第二个切片,start设定为了1,因此取了所有偶数位置的元素。
3在列表中,0不在列表中,所以 3 in a 是True,而 0 in a 是False
更多关于列表的信息可以通过使用 help(list) 查看帮助文档。
队列的概念在 顺序队列 中,而使用循环队列的目的主要是规避假溢出造成的空间浪费,在使用循环队列处理假溢出时,主要有三种解决方案
本文提供后两种解决方案。
顺序队和循环队列是一种特殊的线性表,与顺序栈类似,都是使用一组地址连续的存储单元依次存放自队头到队尾的数据元素,同时附设队头(front)和队尾(rear)两个指针,但我们要明白一点,这个指针并不是指针变量,而是用来表示数组当中元素下标的位置。
本文使用切片来完成的循环队列,由于一开始使用三个参数的make关键字创建切片,在输出的结果中不包含nil值(看起来很舒服),而且在验证的过程中发现使用append()函数时切片内置的cap会发生变化,在消除了种种障碍后得到了一个四不像的循环队列,即设置的指针是顺序队列的指针,但实际上进行的操作是顺序队列的操作。最后是对make()函数和append()函数的一些使用体验和小结,队列的应用放在链队好了。
官方描述(片段)
即切片是一个抽象层,底层是对数组的引用。
当我们使用
构建出来的切片的每个位置的值都被赋为interface类型的初始值nil,但是nil值也是有大小的。
而使用
来进行初始化时,虽然生成的切片中不包含nil值,但是无法通过设置的指针变量来完成入队和出队的操作,只能使用append()函数来进行操作
在go语言中,切片是一片连续的内存空间加上长度与容量的标识,比数组更为常用。使用 append 关键字向切片中追加元素也是常见的切片操作
正是基于此,在使用go语言完成循环队列时,首先想到的就是使用make(type, len, cap)关键字方式完成切片初始化,然后使用append()函数来操作该切片,但这一方式出现了很多问题。在使用append()函数时,切片的cap可能会发生变化,用不好就会发生扩容或收缩。最终造成的结果是一个四不像的结果,入队和出队操作变得与指针变量无关,失去了作为循环队列的意义,用在顺序队列还算合适。
参考博客:
Go语言中的Nil
Golang之nil
Go 语言设计与实现
售后响应及时
7×24小时客服热线数据备份
更安全、更高效、更稳定价格公道精准
项目经理精准报价不弄虚作假合作无风险
重合同讲信誉,无效全额退款