1.数组案例
//统计输入20个字符中字母出现的个数案例
func checkNumFromInput(){
inputCharArr := [20]byte{},checkNum := [26]byte{}
//输入
for i:=0; i<20; i++{
fmt.Scanf("%c",&inputCharArr[i]);
}
//核验
for j:=0; j<20; j++{
checkNum[inputCharArr[j]-'a']++;
}
//输出
for k:=0; k
fmt.Printf("字母%c,出现的次数是%d次\n",'a'+k,checkNum[k]);
}
}
}
//模拟双色球摇号,6个不重复(1~33)红球,和一个蓝球
func getTicketArray () [7]int{
finalArr := [7]int{}
rand.Seed(time.Now().UnixNano())
//一轮一个随机红球
for i:=0; i<6; i++{
temp := rand.Intn(33)+1,flag := true
//判重
for j:=0; j if temp == finalArr[j]{
i--,flag = false
break
}
}
//判是否加
if(flag){
finalArr[i] = temp
}
}
//随机蓝球
finalArr[6] = rand.Intn(33)+1
return finalArr
}
2.二维数组
go语言中的二维数组和传统c语言中的二维数组大同小异,还是需要注意声明和赋值的语法就行
至于数组的特性,仍旧是第一维度表示行,第二维度表示列。数组访问也仍旧需要行列两个维度的参数。
eg:
//var arr [3][5]int = [3][5]int{{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
var arr [3][5]int = [3][5]int{};
for i:=0; i<3; i++{
for j:=0; j<5; j++{
arr[i][j] = rand.Intn(10);
}
}
fmt.Println(arr);//[[1 7 7 9 1] [8 5 0 6 0] [4 1 2 9 8]]
fmt.Println(arr[0][1]);//7
3.切片slice
与传统语言相比,go语言中的数组是一个长度固定的固有结构,因此对于数组的所有操作是不会影响到原数组的。
这样统一的规定虽然避免了很多情况下对于原数组的误操作,但是数组大多数情况下是有必要发生修改的,
因此go语言提出了切片(Slice)的概念,切片从某种意义上来说可以认为是数组的一种可修改的表现方式。
var 切片名 []数据类型 = []数据类型{...}
eg:
var slice1 []int = []int{1,2,3,4,5,...}
如果单独查看切片的语法可能会觉得有些奇怪,那么直接将切片的语法和数组的语法相对比结合就能看到些端倪
eg:
var slice1 []int = []int{1,2,3,4,5,...}
var array [5]int = [5]int{1,2,3,4,5}
非常明显看到,切片可以认为其实就是长度不固定的数组。其定义语法与数组定义方式几乎相同。
ps:
数组数据存栈区,切片数据存堆区
(1)切片的操作
1)make方法与切片的自动推导类型
如果每一次定义切片都采用标准声明方式,会显得代码十分冗余,因此提出了make方式
切片名 := make(切片类型,切片长度);
eg:
slice2 := make([]int, 5);
slice2[0] = 10;
slice2[1] = 11;
...
需要说明的是这里的长度并不是说切片就只能有5个长度,而是暂时只分配5个容量
之后会随着需求不断对切片进行容量扩充。
ps:切片长度,即切片中实际存储了内容的部分
ps:切片容量,是切片中用于系统分配存储空间的标尺
2)append方法与切片扩展
前面提到过切片是一种类似于可以被修改的数组,因此go语言提供了append方法对切片进行扩容
切片名 = append(切片名,扩展内容1,扩展内容2,...)
eg:
slice3 := []int{1,2,3,4,5};
fmt.Println(slice3);//[1,2,3,4,5]
slice3 = append(slice3,11,22,33,44,55);
fmt.Println(slice3);//[1,2,3,4,5,11,22,33,44,55]
ps:go语言中的append方法其意义为【扩容】,而不是修改,
因此append扩充的切片内容会在已知内容之后
eg:
slice4 := make([]int, 5);//[0,0,0,0,0]
slice4 = append(slice4,1,2,3);//[0,0,0,0,0,1,2,3]
3)cap方法-切片容量与切片长度
cap(切片名)方法能够返回切片容量
len(切片名)方法能够返回切片长度
其区别就是:
·容量必然大于等于长度,因为系统必须为内容分配出足够的存储空间。
·只要添加内容长度必然增加,而容量却不一定增加
·如果长度超过了容量,切片才会对容量进行扩展,而且每次扩展都是上次的倍数。
eg:
slice5 := []int{0,0,0,0,0}; //[0,0,0,0,0]
fmt.Printf("长度:%d",len(slice5)); //5
fmt.Printf("容量:%d",cap(slice5)); //5
slice5 = append(slice5, 1,2,3); //[0,0,0,0,0,1,2,3]
fmt.Printf("长度:%d",len(slice5)); //8
fmt.Printf("容量:%d",cap(slice5)); //10
slice5 = append(slice5, 4); //[0,0,0,0,0,1,2,3,4]
fmt.Printf("长度:%d",len(slice5)); //9
fmt.Printf("容量:%d",cap(slice5)); //10
ps:
如果切片的长度扩充超过了容量扩充的2倍,那么本次容量扩充就会以长度扩充为标准
eg:
slice6 := []int{0,0,0,0,0} //[0,0,0,0,0]
fmt.Printf("长度:%d",len(slice6)); //5
fmt.Printf("容量:%d",cap(slice6)); //5
slice6 = append(slice6, 1,2,3,4,5,6,7,8); //[0,0,0,0,0,1,2,3,4,5,6,7,8]
fmt.Printf("长度:%d",len(slice6)); //13
fmt.Printf("容量:%d",cap(slice6)); //13
上面这个例子就能够明显看出,本来切片slice6的一次扩容应当是扩展2倍,即变成10
但是由于本次内容长度的扩展已经超过了10,所以容量的扩充就会以长度为最低标准,也是13
并且下次在扩展的时候,倍数基准就会以13这个数为基准来扩容了
slice6 = append(slice6, 10,11); //[0,0,0,0,0,1,2,3,4,5,6,7,8,10,11]
fmt.Printf("长度:%d",len(slice6)); //15
fmt.Printf("容量:%d",cap(slice6)); //26
ps:
如果整体数据没有超过1024byte,每次扩展为上次的倍数
如果整体数据超过了1024byte,每次扩展为上次的1/4
ps:
有个疑问就是当切片没能被初始化,而是通过make方法只创建出结构时
超过2倍的单次扩容总是会让容量趋向于大于长度的一个偶数值,而不是倍数增长
目前这个问题的具体原因还没能被我理解
期望随着学习的深入,能够搞懂切片的创建方式对容量有什么不同的影响。
sliceTemp := make([]int, 5);
sliceTemp = append(sliceTemp, 1,2,3,4,5,6,7,8);
fmt.Printf("长度:%d",len(sliceTemp)); //13
fmt.Printf("容量:%d",cap(sliceTemp)); //14
4)切片快速遍历
go语言中的切片和数组除了不能修改之外,可以认为切片的标准访问方法操作与数组相同
eg:
slice7 := []int{1,2,3,4,5};
for i,v := range slice7{
fmt.Printf("序号:%d,值:%d\n",i,v);
}
ps:但是一定一定注意,切片在打印的时候只能以长度为标准,而不能以容量作为输出标准。
因为go语言不会对切片容量中的数据初始化,而是只初始化长度内的数据
eg:
slice8 := []int{0,0,0,0,0}; //[0,0,0,0,0]
slice8 = append(slice8,1,2);//[0,0,0,0,0,1,2] 此时长度为7,容量为10
//合法操作
for i:=0; i
}
//违法操作
for i:=0; i
}
(2)切片截取
事实上在很多情况下,go语言的开发过程中大都使用切片来代替数组使用。
而切片的截取从某种意义上来说,go语言的切片截取与其他所有传统语言的操作都不太相同。
eg:
baseSlice := []int{1,2,3,4,5,6};
subSlice := baseSlice[1:4];
1)切片截取
go语言的切片截取不会对原切片造成影响
eg:
fmt.Println(subSlice);//[2,3,4]
fmt.Println(baseSlice);//[1,2,3,4,5,6]
因为go语言切片截取是从原有的切片中“放大”一部分,并不会真的从原切片中将数据剪切出来
2)切片截取的操作
但是对截取切片的操作会对原切片造成影响,因为子切片和切片本身来讲都是同一个东西(内存地址)
eg:
fmt.Printf("%p",baseSlice);//0xc00008a000
fmt.Printf("%p",subSlice);//0xc00008a008
subSlice[1] = 100;
fmt.Println(subSlice);//[2,100,4]
fmt.Println(baseSlice);//[1,2,100,4,5,6]
3)切片截取的“阈值”与扩展
在对切片进行截取的时候,实际上还存在第三个参数:容量。
切片[其实下标:结束下标:子切片容量]
其表示的含义是对切片截取后,生成的子切片最大容量是多少。
由于子切片是原切片的“放大”操作,实际上是二位一体的东西,
子切片的容量必须小于等于原切片的容量!
eg:
baseSlice := []int{1,2,3,4,5,6};
baseSlice = append(baseSlice, 1000);
fmt.Printf("%d",len(baseSlice));//原长度:7
fmt.Printf("%d",cap(baseSlice));//原容量:12
-------------------------------------------
-------------------------------------------
subSlice := baseSlice[1:4:3];
fmt.Println(subSlice);
fmt.Printf("%d",len(subSlice));
fmt.Printf("%d",cap(subSlice));
错误,【容量】必须大于等于【结束下标】
-------------------------------------------
subSlice := baseSlice[1:4:5];
fmt.Println(subSlice);//[2,3,4];
fmt.Printf("%d",len(subSlice));//切片长度:3
fmt.Printf("%d",cap(subSlice));//切片容量:4
-------------------------------------------
subSlice := baseSlice[1:4:10];
fmt.Println(subSlice);//[2,3,4];
fmt.Printf("%d",len(subSlice));//切片长度:3
fmt.Printf("%d",cap(subSlice));//切片容量:9
-------------------------------------------
subSlice := baseSlice[1:4:13];
fmt.Println(subSlice);
fmt.Printf("%d",len(subSlice));
fmt.Printf("%d",cap(subSlice));
错误,【容量】不得超过【原切片的容量】
(3)切片追加与拷贝
1)append方法
append追加后内存地址可能发生变化,因为旧的容量不够用。
这可能对于指针传递时产生一些“旧变,新不变”的问题。
eg:
slice := []int{};
/*
这意味着这两个变量指向同一块内存地址,换句话说
这两个变量都指向了这个空切片
*/
temp := slice;
fmt.Printf("%p\n",slice); //0x1181f88
fmt.Printf("%p\n",temp); //0x1181f88
fmt.Println(temp); //[]
/*
此时切片的容量增加,因为原有的内存地址处的切片已经放不下追加的内容
因此go语言会自动寻找能够放下追加内容的切片的合适位置
因此在slice追加内容后,slice指向了一个新的内存地址
而temp还仍然指向着之前的内存地址 (即值传递,传递的内容是一个地址而已)
所以,追加append操作过程中如果出现了赋值传递
那么小心传递的内容不会跟随传递后的内容变化而一同变化,那只是一张快照而已。
*/
slice = append(slice, 1,2,3);
fmt.Printf("%p\n",slice); //0xc00008e000
fmt.Printf("%p\n",temp); //0x1181f88
fmt.Println(temp); //[]
2)copy方法
go语言中数组切片的copy方法,非常类似于JS中的数组截取方法subString
copy(desSlice目标切片[起始下标:结束下标], resSlice原切片[起始下标:结束下标]);
对于拷贝范围是可选部分,允许不写,而如果不写默认从起始位置开始拷贝,内容是全部原切片
eg:
slice := []int{1,2,3,4,5};
slice2 := make([]int, 5);
//默认拷贝全部
copy(slice2, slice);
fmt.Println(slice2);//[1,2,3,4,5]
//指定拷贝范围,但起始结束都不写,也是拷贝全部
copy(slice2, slice[:]);
fmt.Println(slice2);//[1,2,3,4,5]
//指定拷贝范围,起始不写,默认从起始拷贝
copy(slice2, slice[:3]);
fmt.Println(slice2);//[1,2,3,0,0]
//指定拷贝范围,结束不写,默认拷贝到结束
copy(slice2, slice[2:]);
fmt.Println(slice2);//[3,4,5,0,0]
//指定拷贝范围,指定拷贝到范围
copy(slice2[1:], slice[2,4]);
fmt.Println(slice2);//[0,3,4,0,0]
ps:对于拷贝切片操作来说,目标切片的长度是必须能够存放下要拷贝的内容的
只要长度超过要拷贝的内容,那么多长都无所谓,没有限制说必须小于原切片。
但是若目标切片的长度小于要拷贝的内容,则会造成拷贝内容的丢失
eg:
slice := []int{1,2,3,4,5};
slice2 := make([]int, 2);
copy(slice2, slice);
fmt.Println(slice2);//[1,2]
ps:目标拷贝切片和原切片是两个完全没有任何关联的内容,一处修改另一处不会跟随变化
这与切片拷贝完全不是一回事。
eg:
slice := []int{1,2,3,4,5};
slice2 := make([]int, 5);
copy(slice2, slice);
slice2[2] = 100;
fmt.Println(slice1);//[1,2,3,4,5]
fmt.Println(slice2);//[1,2,100,4,5]
(4)切片传参
切片传参是地址传递,换句话说就是内部修改外部跟随变化,与go语言中的数组是一个明显的不同。
eg:
func test(slice []int){
slice[2] = 100;
}
func main() {
slice := []int{1,2,3,4};
test(slice);
fmt.Println(slice);//[1,2,100,4]
}
但切片传参后若内容追加append,则外部不跟随变化(内容追加后内存发生变更)
eg:
func test(slice []int){
slice = append(slice, 100);
}
func main() {
slice := []int{1,2,3};
test(slice);
fmt.Println(slice);//[1,2,3]
}
总之,修改没问题,但追加就会出问题。当然将追加后的内容作为返回值再返回出来则一定不会出问题。
(5)切片案例:猜数字
func splitNumberToSlice(num int) []int{
numGe := num%10;
numShi := num%100/10;
numBai := num/100;
return []int{numBai,numShi,numGe};
}
func main() {
rand.Seed(time.Now().UnixNano());
pivotSlice := splitNumberToSlice(rand.Intn(899)+100);
var userNum int;
for{
fmt.Println("请输入一个三位数:");
fmt.Scan(&userNum);
if userNum>=100&&userNum<=999{
userSlice := splitNumberToSlice(userNum);
flag := 0;
fmt.Println("从左到右:");
for i:=0; i<3; i++{
if userSlice[i]
}else if userSlice[i]>pivotSlice[i]{
fmt.Printf("第%d位大了\n",i+1);
}else{
fmt.Printf("第%d位正确\n",i+1);
flag++;
}
}
if flag==3{
fmt.Printf("您猜对了,数字就是:%d\n", userNum);
break;
}
}else{
fmt.Println("您输入的数字范围不合法,请输入100~999之间的三位数\n");
}
}
}
4.字典map
(1)基本信息
在go语言中map数据类型表示字典结构,类似于传统c/c++/php中的字典、py中的列表、javascript的对象。
它是由键值对构成,键与值之间用冒号分隔,键值对之间用逗号分隔的无序存储方式。
(key可以是任何非复杂数据类型,value可以是任意数据类型。)
eg:
var map名称 map[keyType]valueType = map[keyType]valueType{};
eg:
var userDic map[string]int = map[string]int{"jack":100};
1)map的初始化
eg:
userDic := map[string]int{"lilei":10};
userDic := make(map[string]int, 10);
ps:
在go语言中map数据类型的零值是nil,因此map创建后必须初始化一下,否则无法正常使用。
eg:
var userDic map[string]int;
userDic["jack"] = 100;//违法操作
2)map自动扩容
在go语言中map和数组切片并不相同,map是自动扩容的,因此使用make初始化时给多少长度无所谓。
eg:
userDic := make(map[string]int);//长度压根就可以不写
userDic["jack"] = 100;
userDic["andy"] = 50;//正确
3)map的长度与容量
在go语言中map的长度是不能够用len来计算的,因为map内部对于数据的存储是无序的链式存储
(链式存储,即每个键值对在存储内容之外,还会存储下一个键值对所在的内存首地址)
因此对于map的长度只能通过range遍历计算。正因如此,对于map而言容量和长度的概念变得毫无意义。
但是某些情况下我们却仍然需要获知map中的键值对的个数
所以最终,go语言规定len方法作用于map的时候,返回的结果是map键值对的个数
而且对于map也不在考虑容量的问题,因为容量恒等于长度,即map中键值对的个数。
eg:
fmt.Println(len(userDic));//1
for k,v := range userDic{
fmt.Printf("%s--%d\n",k,v);
}
4)map的键名唯一
在go语言中map字典在【定义】时,key是唯一的,重复定义key会导致抛出异常。
注意仅仅是定义,使用的时候是无所谓的。因为使用重复key值表示对key所对应的值的修改。
eg:
userDic := map[string]int{"jack":100,"jack":20};//绝对违法!
//
userDic := map[string]int{"jack":100};
userDic["jack"] = 20;//没毛病
(2)猜数字案例-改写:
func splitNumberToMap(num int) map[string]int{
numGe := num%10;
numShi := num%100/10;
numBai := num/100;
return map[string]int{"百位":numBai,"十位":numShi,"个位":numGe};
}
func main() {
rand.Seed(time.Now().UnixNano());
pivotMap := splitNumberToMap(rand.Intn(899)+100);
var userNum int;
for{
fmt.Println("请输入一个三位数:");
fmt.Scan(&userNum);
if userNum>=100&&userNum<=999{
userMap := splitNumberToMap(userNum);
flag := 0;
for k,v := range userMap{
if v
}else if v>pivotMap[k]{
fmt.Printf("%s大了\n",k);
}else{
fmt.Printf("%s正确\n",k);
flag++;
}
}
if flag==3{
fmt.Printf("您猜对了,数字就是:%d\n", userNum);
break;
}
}else{
fmt.Println("您输入的数字范围不合法,请输入100~999之间的三位数\n");
}
}
}
(3)map的值
因为在go语言中,对map的访问总是能够得到一个确定的值,哪怕这个key并不存在于map中也是如此。
因此map提出了一种判别key值是否存在的机制。
eg:
userDic := map[string]int{"jack":100,"frank":300};
//真值
val,flag := userDic["jack"];
fmt.Printf("%d,%t");//100,true
//假值
val,flag := userDic["Alice"];
fmt.Printf("%d,%t");//0,false
(4)delete-map的删除
在go语言中map的内容的删除,实际上就是键值对的删除。
eg:
delete(要执行删除操作的map, 要删除键值对的key)
eg:
delete(userDic, "jack");
ps:
需要注意的是,delete秉承了go语言的一贯简洁的作风,
不但不存在返回值,而且不论key值是否存在都会正常向下执行。(简洁的有点大劲了感觉)
因此一般不确定key值是否存在时,用val,flag判别一下在删除是一个不错的选择
eg:
userDic := map[string]int{"jack":100};
val,flag = userDic["frank"];
if flag{
delete(userDic, "frank");
}else{
fmt.Println("map中不存在frank这样的key");
}
(5)map传参与返回值
在go语言中的map由于是自动扩容的,所以内存地址在变量完成内存分配后是不会发生变动的
所以map的传值采用的是真正意义上的地址传递。内部操作,外部变化
eg:
//合并map中的数组切片
func joinSliceFromMap(tempMap map[string][]int)[]int{
//粗糙算法,先算容量,在做合并
//代码冗余到刺眼
//finalArrLength := 0;
//for _,v := range tempMap{
// finalArrLength += len(v);
//}
//finalArr := make([]int, finalArrLength);
//testIndex := 0;
//for _,v := range tempMap{
// copy(finalArr[testIndex:],v);
// testIndex += len(v);
//}
//扩容算法,长度无所谓,有内容就填充
finalArr := make([]int,0);
for _,v := range tempMap{
for i:=0; i
}
}
return finalArr;
}
func main() {
myMap := map[string][]int{
"jack":[]int{1,2,3,4,5},
"frank":[]int{5,4,3,2,1},
}
resultArr := joinSliceFromMap(myMap);
fmt.Println(resultArr);//[1,2,3,4,5,5,4,3,2,1]
}