• Go学习第七章——数组arr,切片slice和映射map


    1 数组

    数组可以存放多个同一类型数据。数组也是一种数据类型在Go中,数组是值类型。

    1.1 快速入门

    数组定义格式:
    var 数组名 [数组大小]数据类型

    步骤:

    1. 定义数组
    2. 将数据存入数组
    3. 根据所需获取数据进行计算

    案例讲解:

    func main() {
        //实现的功能:给出五个学生的成绩,求出成绩的总和,平均数:
        //给出五个学生的成绩:--->数组存储:
        //1.定义一个数组:
        var scores [5]int
        //20 将成绩存入数组:
        scores[0] = 95
        scores[1] = 91
        scores[2] = 39
        scores[3] = 60
        scores[4] = 21
        //3.求和:
        //定义一个变量专门接收成绩的和:
        sum := 0
        for i := 0; i < len(scores); i++ { //i: 0,1,2,3,4
           sum += scores[i]
        }
        //4.平均数:
        avg := sum / len(scores)
        //输出
        fmt.Printf("成绩的总和为:%v,成绩的平均数为:%v", sum, avg)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    输出结果:成绩的总和为:306,成绩的平均数为:61

    1.2 数组的内存布局

    首先我们来看一下数组的地址

    func main() {
        //声明数组:
        var arr [3]int
        //获取数组的长度:
        fmt.Println(len(arr))
        //打印数组:
        fmt.Println(arr) //[0 0 0]
        //证明arr中存储的是地址值:
        fmt.Printf("arr的地址为:%p\n", &arr)
        //第一个空间的地址:
        fmt.Printf("arr第一个空间的地址为:%p\n", &arr[0])
        //第二个空间的地址:
        fmt.Printf("arr第二个空间的地址为:%p\n", &arr[1])
        //第三个空间的地址:
        fmt.Printf("arr第三个空间的地址为:%p\n", &arr[2])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    输出结果:

    3
    [0 0 0]
    arr的地址为:0xa00a0d0
    arr第一个空间的地址为:0xa00a0d0
    arr第二个空间的地址为:0xa00a0d4
    arr第三个空间的地址为:0xa00a0d8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    从上面的结果可以看出以下几点:

    1. 如果是int类型数组,默认不赋值都为0,那么可以得出,不赋值的情况,默认值就是基本数据类型的默认值。
    2. 获取数组的地址值,跟获取基本数据类型一样:&arr即可
    3. arr第一个空间的地址值跟直接获取的地址值相同:&arr = &arr[0]
    4. 因为int类型根据电脑是int32,那么一个int数据类型占用4个字节,所以第二个数组的空间地址值与第一个空数组就相差4个数,并且发现这是16进制的数。

    在这里插入图片描述

    1.3 四种初始化数组的方式
    func main() {
        // 方式一
        var arr1 [3]int = [3]int{1, 2, 3}
        // 方式二
        var arr2 = [3]int{1, 2, 3}
        // 方式三
        var arr3 = [...]int{1, 2, 3}
        // 方式四
        var arr4 = [3]string{1: "Tom", 0: "Jack", 2: "marry"}
    
        fmt.Println(arr1)
        fmt.Println(arr2)
        fmt.Println(arr3)
        fmt.Println(arr4)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    输出结果:

    [1 2 3]
    [1 2 3]         
    [1 2 3]         
    [Jack Tom marry]
    
    • 1
    • 2
    • 3
    • 4

    一般情况,我们定义数组都是用这种方式:arr := [5]int{1,2,3}这样,后面两个会给默认值0

    1.4 数组的遍历

    方式一:常规的遍历

    前面的方式就是,这里不写了

    方式二:使用for-range结构遍历

    (键值循环) for range结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道,for range 语法上类似于其它语言中的 foreach 语句,一般形式为:

    for index, value := range arr {
        ...
    }
    
    • 1
    • 2
    • 3

    (1)arr就是你要遍历的数组名
    (2)每次遍历得到的索引用index接收,每次遍历得到的索引位置上的值用value
    (3)index、value的名字随便起名 i、v index、value
    (4)index、value属于在这个循环中的局部变量
    (5)你想忽略某个值:用_就可以了:

    for _, val := range scores {
        ...
    }
    
    • 1
    • 2
    • 3

    简单使用:

    func main() {
    	arr := [5]int{1, 2, 3}
    	
    	for k, v := range arr {
    		fmt.Printf("数组第%v个的值为:%v\n", k, v)
    	}
    
    	for _, v := range arr {
    		fmt.Printf("数组值为:%v\n", v)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    输出结果:

    数组第0个的值为:1
    数组第1个的值为:2
    数组第2个的值为:3
    数组第3个的值为:0
    数组第4个的值为:0
    数组值为:1
    数组值为:2
    数组值为:3
    数组值为:0
    数组值为:0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1.5 注意事项以及分析
    1. 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化

    2. var arr []int 这时arr就是一个slice切片,切片下一步讲,等等说。

    3. 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用

    4. 数组创建后,如果没有赋值,有默认值

      • 数值类型数组:默认: 0
      • 字符串数组:默认:“”
      • bool数组:默认值:false
    5. 使用数组的步骤 1. 声明数组并开辟空间 2.给数组各个元素赋值 3.使用数组

    6. 数组的下标是从0开始的

    7. 数组下标必须在指定范围内使用,否则报 panic;数组越界。

    8. Go的数组属于值类型,在默认情况下是值传递,因此会进行值拷贝。数组间不会互相影响。

    9. 如果想在其他函数中,去修改原来的数组,可以使用引用传递(指针方式)

      案例讲解:

      // 通过普通方式,修改数组的值
      func updateArr(num int) {
          num = 10
      }
      
      // 通过指针方式,修改数组的值
      func updateArrPointer(num *int) {
          *num = 10
      }
      
      func main() {
          arr := [5]int{1, 2, 3}
          fmt.Println("普通值传递方式")
          fmt.Println("修改前,数组的值:", arr[0])
          updateArr(arr[0])
          fmt.Println("修改后,数组的值:", arr[0])
      
          fmt.Println("通过指针方式")
          fmt.Println("修改前,数组的值:", arr[0])
          updateArrPointer(&arr[0])
          fmt.Println("修改后,数组的值:", arr[0])
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22

    输出结果:

    普通值传递方式
    修改前,数组的值: 1
    修改后,数组的值: 1
    通过指针方式
    修改前,数组的值: 1
    修改后,数组的值: 10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1.6 数组反转

    要求:随机生成五个数,并将其反转打印
    思路:

    1. 随机生成五个数 , rand.Intn() 函数
    2. 当我们得到随机数后,就放到一个数组 int数组
    3. 反转打印 , 交换的次数是 len / 2, 倒数第一个和第一个元素交换, 倒数第2个和第2个元素交换

    概念一:随机数生成

    在随机数生成中,种子(seed)是一个用于初始化随机数生成器的值。通过给定特定的种子,我们可以确保每次运行程序时生成的随机数序列是不同的。

    在 Go 语言中,种子通常以整数形式表示,并且可以通过调用 rand.Seed(seed int64) 方法来设置种子值。种子值可以是任何整数,但是如果种子值相同,那么生成的随机数序列也将相同。

    常用的设置种子的方法是使用当前时间作为种子。在这种情况下,我们可以使用 time.Now().UnixNano() 函数来获取当前时间的纳秒级别时间戳,并将其作为种子值传递给 rand.Seed(seed int64) 方法。

    例如,rand.Seed(time.Now().UnixNano()) 将使用当前时间的纳秒级别时间戳作为种子值,从而使得每次运行程序生成的随机数序列都是不同的。这样做的目的是为了增加随机性,使得生成的随机数更加随机和分散。

    需要注意的是,在实际应用中,如果我们需要可重复的随机数序列,可以使用相同的种子值来初始化随机数生成器。这样可以确保每次运行程序时,生成的随机数序列都是相同的。

    概念二:生成随机数新方法

    根据Go 1.17版本,新增了一个 math/rand.Source 接口和 math/rand.NewSource 函数,用于生成随机种子。在新版本中,随机数生成器的实现和种子值的管理分离开来,使得随机数生成器更加灵活而且易于维护。

    所以生成随机数的代码

    func main() {
    	intArr := [5]int{}
    	randGen := rand.New(rand.NewSource(time.Now().UnixNano()))
    
    	for i := 0; i < len(intArr); i++ {
    		intArr[i] = randGen.Intn(100)
    	}
    
    	fmt.Println("随机数:", intArr)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出结果:随机数: [66 6 59 99 64]

    最后的代码为:

    func main() {
    	//要求:随机生成五个数,并将其反转打印
    	//思路
    	//1. 随机生成五个数 , rand.Intn() 函数
    	//2. 当我们得到随机数后,就放到一个数组 int数组
    	//3. 反转打印 , 交换的次数是  len / 2, 倒数第一个和第一个元素交换, 倒数第2个和第2个元素交换
    
    	intArr := [5]int{}
    	len := len(intArr)
    
    	for i := 0; i < len; i++ {
    		intArr[i] = rand.Intn(100)
    	}
    
    	fmt.Println("交换前~=", intArr)
    	//反转打印 , 交换的次数是  len / 2,
    	//倒数第一个和第一个元素交换, 倒数第2个和第2个元素交换
    	temp := 0 //做一个临时变量
    	for i := 0; i < len/2; i++ {
    		temp = intArr[len-1-i]
    		intArr[len-1-i] = intArr[i]
    		intArr[i] = temp
    	}
    
    	fmt.Println("交换后~=", intArr)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    输出结果:

    交换前~= [7 65 83 17 12]
    交换后~= [12 17 83 65 7]
    
    • 1
    • 2
    1.7 二维数组

    二维数组是一种常见的数据结构,通常用于表示表格、矩阵等具有行列结构的数据。在 Go 语言中,二维数组的定义和初始化方式如下:

    // 定义一个 3 行 4 列的二维数组
    var arr2D [3][4]int
    
    // 初始化二维数组
    arr2D = [3][4]int{
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12},
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的代码中,arr2D 是一个 3 行 4 列的二维数组,每个元素的类型都是 int。我们可以使用数组字面量初始化二维数组,每个数组字面量表示一行数据。

    输出结果为:[[1 2 3 4] [5 6 7 8] [9 10 11 12]]

    二维数组的遍历:

    func main() {
        //定义二维数组:
        var arr [3][3]int = [3][3]int{{1, 4, 7}, {2, 5, 8}, {3, 6, 9}}
        fmt.Println(arr)
        fmt.Println("------------------------")
        //方式1:普通for循环:
        for i := 0; i < len(arr); i++ {
           for j := 0; j < len(arr[i]); j++ {
              fmt.Print(arr[i][j], "\t")
           }
           fmt.Println()
        }
        fmt.Println("------------------------")
        //方式2:for range循环:
        for key, value := range arr {
           for k, v := range value {
              fmt.Printf("arr[%v][%v]=%v\t", key, k, v)
           }
           fmt.Println()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出结果:

    [[1 4 7] [2 5 8] [3 6 9]]
    ------------------------
    1       4       7
    2       5       8
    3       6       9
    ------------------------
    arr[0][0]=1     arr[0][1]=4     arr[0][2]=7
    arr[1][0]=2     arr[1][1]=5     arr[1][2]=8
    arr[2][0]=3     arr[2][1]=6     arr[2][2]=9
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2 切片

    切片是一个拥有动态长度的可索引序列,它是对底层数组的引用(所以是一个引用类型)。切片提供了一种灵活、方便的方式来管理数组,并允许数组可以自动扩容。切片通常用于表示动态长度的集合,如列表、队列和栈等。

    2.1 快速入门
    1. 切片的英文slice

    2. 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。

    3. 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样。

    4. 切片的长度是可以变化的,因此切片是一个可以动态变化数组。

    5. 切片定义的基本语法:

      var 变量名 []类型,比如:var a []int,这里的[]不需要写长度,只要不写东西就是切片,写就是数组。

    案例讲解:

    func main() {
    	//定义二维数组:
    	var intArr [5]int = [...]int{1, 22, 33, 66, 99}
    	//声明/定义一个切片
    	//slice := intArr[1:3]
    	//1. slice 就是切片名
    	//2. intArr[1:3] 表示 slice 引用到intArr这个数组
    	//3. 引用intArr数组的起始下标为 1 , 最后的下标为3(但是不包含3)
    	slice := intArr[1:3]
    	fmt.Println("intArr=", intArr)
    	fmt.Println("slice 的元素是 =", slice)       //  22, 33
    	fmt.Println("slice 的元素个数 =", len(slice)) // 2
    	fmt.Println("slice 的容量 =", cap(slice))   // 切片的容量是可以动态变化
    
    	// 切片是引用类型,也就是,实际存储的是intArr地址值
    	fmt.Println()
    	fmt.Printf("intArr[1]的地址=%p\n", &intArr[1])
    	fmt.Printf("slice[0]的地址=%p slice[0]=%v\n", &slice[0], slice[0])
    
    	// 修改了切片的数值,同样会把intArr的值修改,也就是直接修改了地址对应的数据值
    	fmt.Println()
    	slice[1] = 34
    	fmt.Println("intArr=", intArr)
    	fmt.Println("slice 的元素是 =", slice) //  22, 33
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    输出结果:

    intArr= [1 22 33 66 99]             
    slice 的元素是 = [22 33]            
    slice 的元素个数 = 2                
    slice 的容量 = 4                    
                                        
    intArr[1]的地址=0x9d16004           
    slice[0]的地址=0x9d16004 slice[0]=22
                                        
    intArr= [1 22 34 66 99]             
    slice 的元素是 = [22 34]      
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    2.2 内存解析

    通过上面的代码,我们已经出门了解了slice,现在从底层内存了解一下

    在这里插入图片描述

    从这个图可以看出:

    1. slice在内存中的展示,的确是一个引用类型

    2. slice从底层来说,其实就是一个数据结构(struct结构体)

      type slice struct{
          ptr *[2] int // 存储被切的数组的地址值,例如:这里的22是被切的首部,那就存储它的地址值
      	len int      // 存储元素的数量
      	cap int		 // 存储切片的容量(底层数组的长度)
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
    2.3 切片的使用和遍历

    方式1:定义一个切片,然后让切片去引用一个已经创建好的数组,比如:前面的案例就是这样的。

    略。。。

    方式2:通过make来创建切片。

    基本语法:var 切片名 []type = make([], len, [cap])

    参数说明:

    type:就是数据类型 len:大小 cap(可选):指定切片的容量,如果你分配了cap,则要cap >=l en

    func main() {
    	// 基本语法
    	var sliceName []int = make([]int, 5)
    	fmt.Println(sliceName)
    
    	// 创建一个带有 5 个元素的切片
    	// 这样创建更舒服
    	s := make([]int, 5)
    
    	// 在切片中添加元素
    	s = append(s, 1, 2, 3)
    	fmt.Println("s切片的元素:", s)
    
    	// 输出切片的长度和容量
    	fmt.Println("长度为:", len(s), ",容量为:", cap(s))
    
    	// 访问切片的元素
    	fmt.Println("第一个元素为:", s[0])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出结果:

    [0 0 0 0 0]
    s切片的元素: [0 0 0 0 0 1 2 3]
    长度为: 8 ,容量为: 12
    第一个元素为: 0
    
    • 1
    • 2
    • 3
    • 4

    注意:通过这种方式创建的切片,其地址指向的数组是对外不可见,但在内存中就是会有占用这么一块内存空间。

    方式3:定义一个切片的同时,直接指定具体数组

    func main() {
        var sliceName []string = []string{"Tom", "Jack", "Mary"}
        // sliceName := []string{"Tom", "Jack", "Mary"}
        fmt.Println("sliceName", sliceName)
        fmt.Println("sliceName size=", len(sliceName))
        fmt.Println("sliceName cap=", cap(sliceName))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    输出结构:

    sliceName [Tom Jack Mary]
    sliceName size= 3
    sliceName cap= 3
    
    • 1
    • 2
    • 3

    切片的遍历

    func main() {
    	sliceName := []string{"Tom", "Jack", "Mary"}
    	// 使用常规方式
    	for i := 0; i < len(sliceName); i++ {
    		fmt.Printf("slice[%v] = %v", i, sliceName[i])
    	}
    	fmt.Println()
    	// 使用for--range方式遍历
    	for i, v := range sliceName {
    		fmt.Printf("slice[%v] = %v", i, v)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输出结果:

    slice[0] = Tomslice[1] = Jackslice[2] = Mary
    slice[0] = Tomslice[1] = Jackslice[2] = Mary
    
    • 1
    • 2
    2.4 注意事项和细节说明
    1. 切片初始化时,var slice = arr[startIndex:endIndex]

    ​ 说明:从arr数组下标为startIndex,取到下标为endIndex的元素(不含arr[endIndex])

    1. 切片初始化时,仍然不能越界。范围在[0-len(arr)]之间,但是可以动态增长。

      • var slice = arr[0:end] 可以简写: var slice = arr[:end]
      • var slice = arr[start:len(arr)] 可以简写:var slice = arr[start:]
      • var slice = arr[0:len(arr)] 可以简写:var slice = arr[:]
    2. cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素。

    3. 切片可以继续切片

    4. 用切片的内置函数append,可以第切片进行动态追加

      切片append操作的底层原理分析:

      1. 切片append操作的本质就是对数组扩容
      2. go底层会创建一下新的数组newArr(安装扩容后大小)
      3. 将slice原来包含的元素拷贝到新的数组newArr
      4. slice重新引用到newArr
      5. 注意newArr是在底层来维护的,程序员不可见

      案例说明:

      在这里插入图片描述

    通过这个图就可以看出,扩容后,go底层会创建行的数组,而且面对程序员不可见。

    2.5 string和slice关系
    1. string底层是一个byte数组,因此string也可以进行切片处理

    2. string和切片在内存的形式,以"abcd"画出内存示意图

      在这里插入图片描述

    3. string是不可变的,也就是说不能通过 str[0] = 'z’方式来修改字符串

    4. 如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string

    func main() {
    	//string底层是一个byte数组,因此string也可以进行切片处理
    	str := "hello@atguigu"
    	//使用切片获取到 atguigu
    	slice := str[6:] 
    	fmt.Println("slice=", slice)
    
    	//string是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串 
    	//str[0] = 'z' [编译不会通过,报错,原因是string是不可变]
    
    	//如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string
    	// "hello@atguigu" =>改成 "zello@atguigu"
    	// arr1 := []byte(str) 
    	// arr1[0] = 'z'
    	// str = string(arr1)
    	// fmt.Println("str=", str)
    
    	// 细节,我们转成[]byte后,可以处理英文和数字,但是不能处理中文
    	// 原因是 []byte 字节来处理 ,而一个汉字,是3个字节,因此就会出现乱码
    	// 解决方法是 将  string 转成 []rune 即可, 因为 []rune是按字符处理,兼容汉字
    
    	arr1 := []rune(str) 
    	arr1[0] = '北'
    	str = string(arr1)
    	fmt.Println("str=", str)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    2.6 切片练习题

    写一个函数,可以打印一个斐波那契的数列,要求为数组格式

    1. 可以接收一个 n int
    2. 能够将斐波那契的数列放到切片中
    3. 提示, 斐波那契的数列形式:arr[0] = 1; arr[1] = 1; arr[2]=2; arr[3] = 3; arr[4]=5; arr[5]=8

    思路

    1. 声明一个函数 fbn(n int) ([]uint64)
    2. 编程fbn(n int) 进行for循环来存放斐波那契的数列
    func fbn(n int) ([]uint64) {
    	//声明一个切片,切片大小 n
    	fbnSlice := make([]uint64, n)
    	//第一个数和第二个数的斐波那契 为1
    	fbnSlice[0] = 1
    	fbnSlice[1] = 1
    	//进行for循环来存放斐波那契的数列
    	for i := 2; i < n; i++ {
    		fbnSlice[i] = fbnSlice[i - 1] + fbnSlice[i - 2]
    	}
    
    	return fbnSlice
    }
    
    func main() {
    	fnbSlice := fbn(20)
    	fmt.Println("fnbSlice=", fnbSlice)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出结果:

    fnbSlice= [1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765]
    
    • 1

    3 map映射

    map是key-value数据结构,又称为字段或者关联数组。类似其他编程语言的集合,在编程中是经常用到的。

    3.1 快速入门

    切片的语法:var map变量名 map[keytype]valuetype

    1. key、value的类型:bool、数字、string、指针、channel 、还可以是只包含前面几个类型的接口、结构体、数组
    2. key通常为int 、string类型,value通常为数字(整数、浮点数)、string、map、结构体
    3. key:slice、map、function不可以,因为这几个没发用 == 来判断

    map的特点:

    1. map集合在使用前一定要make
    2. map的key-value是无序的
    3. key是不可以重复的,如果遇到重复,后一个value会替换前一个value
    4. value可以重复的
    func main() {
    	//map的声明
    	var a map[string]string
    	//在使用map前,需要先make , make的作用就是给map分配数据空间
    	a = make(map[string]string, 10)
        // 或者一步解决:a := make(map[string]string, 10)
    	a["no1"] = "宋江" //ok?
    	a["no2"] = "吴用" //ok?
    	a["no1"] = "武松" //ok?
    	a["no3"] = "吴用" //ok?
    	fmt.Println(a)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输出结果:map[no1:武松 no2:吴用 no3:吴用]

    声明map的三种方法:

    1. 如上所示,分开来使用
    2. 声明就直接make:var a = make(map[string]string, 10)
    3. 声明直接赋值:a := map[string]string{"no1": "北京","no2": "深圳",}

    还可以套多一层,例如下面:

    课堂练习:演示一个key-value 的value是map的案例
    比如:我们要存放3个学生信息, 每个学生有 name和sex 信息
    思路: map[string]map[string]string

    func main() {
    	studentMap := make(map[string]map[string]string)
    
    	studentMap["stu01"] = make(map[string]string, 3)
    	studentMap["stu01"]["name"] = "tom"
    	studentMap["stu01"]["sex"] = "男"
    	studentMap["stu01"]["address"] = "北京长安街~"
    
    	studentMap["stu02"] = make(map[string]string, 3) //这句话不能少!!
    	studentMap["stu02"]["name"] = "mary"
    	studentMap["stu02"]["sex"] = "女"
    	studentMap["stu02"]["address"] = "上海黄浦江~"
    
    	fmt.Println(studentMap)
    	fmt.Println(studentMap["stu02"])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    输出结果:

    map[stu01:map[address:北京长安街~ name:tom sex:] stu02:map[address:上海黄浦江~ name:mary sex:]]
    map[address:上海黄浦江~ name:mary sex:] 
    
    • 1
    • 2
    3.2 map的crud操作

    map增加和更新:

    map[“key”] = value // 如果key还没有,就是增加,如果key存在就是修改。

    map删除

    delete(map, “key”),delete是一个内置函数,如果key存在,就删除改key-value,如果key不存在,不操作,但是也不会报错。

    细节说明:

    1. 如果我们要删除map的所有key,没有一个专门的方法一次删除,可以遍历一下key,逐个删除
    2. 或者 map = make(…),make一个新的,让原来的成为垃圾,被gc回收

    map查找

    使用这种方式查找:要查找的值val, 结果(true/false) := map的名字["需要查找的key"]

    func main() {
    	heroes := make(map[string]string, 10)
    	heroes["no1"] = "宋江"
    	heroes["no2"] = "卢俊义"
    
    	val, findRes := heroes["no1"]
    	if findRes {
    		fmt.Println("找到了val=", val)
    	} else {
    		fmt.Println("没有no1这个key")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输出结果:找到了val= 宋江

    说明:如果heroes这个map中存在”no1“,那么findRes就会返回true,否则返回false

    3.3 map的遍历

    map的遍历只能使用for-range的结构遍历

    简单案例:

    func main() {
        heroes := make(map[string]string, 10)
        heroes["no1"] = "北京"
        heroes["no2"] = "上海"
    
        for k, v := range heroes {
           fmt.Printf("k=%v v=%v\n", k, v)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    输出结果:

    k=no1 v=北京
    k=no2 v=上海
    
    • 1
    • 2

    复杂案例:

    func main() {
        studentMap := make(map[string]map[string]string)
    
        studentMap["stu01"] = make(map[string]string, 3)
        studentMap["stu01"]["name"] = "tom"
        studentMap["stu01"]["sex"] = "男"
        studentMap["stu01"]["address"] = "北京长安街~"
    
        studentMap["stu02"] = make(map[string]string, 3) //这句话不能少!!
        studentMap["stu02"]["name"] = "mary"
        studentMap["stu02"]["sex"] = "女"
        studentMap["stu02"]["address"] = "上海黄浦江~"
    
        for k1, v1 := range studentMap {
           fmt.Println("k1=", k1)
           for k2, v2 := range v1 {
              fmt.Printf("\t k2=%v2 v2=%v \n", k2, v2)
           }
           fmt.Println()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出结果:

    k1= stu01
             k2=name2 v2=tom            
             k2=sex2 v2=男              
             k2=address2 v2=北京长安街~ 
                                        
    k1= stu02                           
             k2=name2 v2=mary           
             k2=sex2 v2=女              
             k2=address2 v2=上海黄浦江~ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    3.4 map切片

    切片的数据类型如果是map,则我们称为 slice of map,map切片,这样使用则map个数就可以动态变化了。

    案例演示:

    要求:使用一个map来记录monster的信息 name 和 age,

    也就是说一个monster对应一个map,并且妖怪的个数可以动态的增加=>map切片

    func main() {
    	monster := make([]map[string]string, 2) // 存入两个妖怪
    
    	monster[0] = make(map[string]string, 2)
    	monster[0]["name"] = "牛魔王"
    	monster[0]["age"] = "500"
    
    	monster[1] = make(map[string]string, 2)
    	monster[1]["name"] = "玉兔精"
    	monster[1]["age"] = "200"
    
    	// 因为monster只配置了两个容量,所以如果还要添加就会越界,必须用另外的方法
    	// 1.先定义monster信息
    	newMonster := map[string]string{
    		"name": "新的妖怪",
    		"age":  "200",
    	}
    	// 2.再通过append接上
    	monster = append(monster, newMonster, make(map[string]string))
    
    	fmt.Println(monster)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    输出结果:

    [map[age:500 name:牛魔王] map[age:200 name:玉兔精] map[age:200 name:新的妖怪] map[]]
    
    • 1
    3.5 map排序
    1. golang中没有一个专门的方法针对map的key进行排序
    2. golang中的map默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历。得到的输出可能不一样。
    3. golang中map的排序,需要使用切片储存map,然后再用sort包进行排序。

    在 Go 语言中,map 的 key 是无序的。这是由 Go 语言的实现决定的。map 的底层实现是哈希表,哈希表对 key 进行哈希运算,将 key 映射到底层数组的某个位置上,并存储对应的 value。由于哈希表的内部实现与 key 的顺序无关,因此 map 的 key 是无序的。

    map 的无序性可以带来一些好处,例如更高的访问速度和更小的内存占用。由于哈希表是按照 key 所计算的哈希值来访问元素的,因此访问元素的速度是非常快的,不会因为 key 的顺序而降低速度。此外,由于底层数组存储的是键值对,而不是一个个具体的元素,所以在空间占用上,与 key 的顺序无关,可以充分利用底层数组的空间。

    如果你需要有序的 key,可以使用 sort 包对 map 中的 key 进行排序。示例如下:

    package main
    
    import (
        "fmt"
        "sort"
    )
    
    func main() {
        a := map[string]string{
            "no1": "成都",
            "no3": "北京",
            "no2": "深圳",
        }
    
        keys := make([]string, len(a))
        i := 0
        for k := range a {
            keys[i] = k
            i++
        }
    
        sort.Strings(keys)
    
        for _, k := range keys {
            fmt.Println(k, a[k])
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在上述代码中,我们首先将 map a 中的 key 存储到一个切片中,并使用 sort 包对切片进行排序。然后,我们可以使用排序后的切片来遍历 map,并以有序的方式输出所有的 key 和对应的 value。

    补充:

    递增排序

    sort.Strings(keys)
    
    • 1

    递减排序

    sort.Slice(keys, func(i, j int) bool {
    	return keys[i] > keys[j]
    })
    
    • 1
    • 2
    • 3

    上面这个代码:使用 sort.Slice 函数对 keys 切片进行排序,并传入一个匿名的比较函数作为参数。在比较函数中,我们使用递减(大于)的方式进行比较,从而实现递减排序。

    3.6 使用细节
    1. map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map。
    2. map的容量达到后,再想map增加元素,会自动扩容,并不会发生panic,也就是说map能动态的增长键值对
    3. map的value也经常使用struct类型,更适合管理复杂的数据(比前面value是一个map更好),比如value为Student结构体。

    以下代码将对第2句话进行讲解和验证:

    package main
    
    import "fmt"
    
    func main() {
    	a := make(map[string]string, 2)
    	a["apple"] = "red"
    	a["banana"] = "yellow"
    	a["carrot"] = "orange"
    
    	fmt.Println(a)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上述代码创建了一个初始容量为 2 的空 map,并依次向其中添加三个键值对。根据提示的特性,当 map 容量达到后,再想往 map 中增加元素时,它会自动扩容。

    输出结果如下:

    map[apple:red banana:yellow carrot:orange]
    
    • 1

    从输出结果中可以看出,map 成功地添加了三个键值对,并没有发生 panic。这证明了 map 的确能够动态增长键值对。

    需要注意的是,尽管 map 可以动态增长键值对,但其增长策略是根据实现方式(哈希表)和负载因子(current size / current capacity)来决定的。当负载因子超过一定阈值时,map 会重新分配更大的内存空间,将已有的元素重新哈希到新的内存位置上,并拷贝到新的底层数组中。这个过程可能会导致一些性能消耗,因此,合理估算和设置初始容量可以提高 map 的性能。

    4 综合题目(难度较大,可略~)

    题目:某餐馆进行食物点菜记录,要求记录每桌点的菜品,桌号为1、2、3…,要求能够添加、查看和删除菜品,并且能够查询每桌点了多少菜。

    解决思路:

    1. 使用 map 建立桌号与菜品的对应关系,key 是桌号,value 是一个切片,表示该桌点的菜品。
    2. 使用 map 存储每桌点菜的数量,key 是桌号,value 是整数,表示该桌点了多少菜。

    实现步骤:

    1. 创建一个空的 map orders,用来记录每桌点的菜品。
    2. 创建一个空的 map total,用来记录每桌点了多少菜。
    3. 添加点菜功能:输入桌号和菜品名称,判断该桌号是否存在于 orders 中,如果存在,则在对应的切片中添加菜品,并在 total 中该桌号对应的值加一。如果不存在,则创建该桌号的切片,并添加菜品,同时在 total 中该桌号对应的值初始化为 1。
    4. 删除菜品功能:输入桌号和菜品名称,判断该桌号是否存在于 orders 中,如果存在,则在对应的切片中删除菜品,并在 total 中该桌号对应的值减一。如果删除后切片为空,则从 orderstotal 中删除该桌号。
    5. 查询菜品功能:输入桌号,从 orders 中查找该桌号对应的菜品切片,并输出菜品数量。

    下面是具体的实现代码:

    package main
    
    import "fmt"
    
    func main() {
    	orders := make(map[string][]string)
    	total := make(map[string]int)
    
    	// 添加点菜
    	addOrder(orders, total, "1", "鱼香肉丝")
    	addOrder(orders, total, "1", "宫保鸡丁")
    	addOrder(orders, total, "2", "水煮鱼")
    	addOrder(orders, total, "3", "红烧肉")
    	addOrder(orders, total, "3", "糖醋排骨")
    
    	// 输出每桌点菜结果
    	for table, dishes := range orders {
    		fmt.Printf("桌号:%s,点了以下菜品:", table)
    		for _, dish := range dishes {
    			fmt.Printf(" %s,", dish)
    		}
    		fmt.Printf(" 共计%d道菜\n", total[table])
    	}
    
    	fmt.Println()
    
    	// 删除一道菜
    	deleteOrder(orders, total, "1", "宫保鸡丁")
    
    	// 输出每桌点菜结果
    	for table, dishes := range orders {
    		fmt.Printf("桌号:%s,点了以下菜品:", table)
    		for _, dish := range dishes {
    			fmt.Printf(" %s,", dish)
    		}
    		fmt.Printf(" 共计%d道菜\n", total[table])
    	}
    
    	fmt.Println()
    
    	// 查询菜品数量
    	queryOrder(total, "2")
    	queryOrder(total, "3")
    }
    
    func addOrder(orders map[string][]string, total map[string]int, table string, dish string) {
    	_, ok := orders[table]
    	if ok {
    		orders[table] = append(orders[table], dish)
    		total[table]++
    	} else {
    		orders[table] = []string{dish}
    		total[table] = 1
    	}
    }
    
    func deleteOrder(orders map[string][]string, total map[string]int, table string, dish string) {
    	dishes, ok := orders[table]
    	if ok {
    		for i, d := range dishes {
    			if d == dish {
    				orders[table] = append(orders[table][:i], orders[table][i+1:]...)
    				total[table]--
    				if total[table] == 0 {
    					delete(orders, table)
    					delete(total, table)
    				}
    				break
    			}
    		}
    	}
    }
    
    func queryOrder(total map[string]int, table string) {
    	count, ok := total[table]
    	if ok {
    		fmt.Printf("桌号:%s,点了%d道菜\n", table, count)
    	} else {
    		fmt.Printf("桌号:%s,无点菜记录\n", table)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    代码很长,需要慢慢看~~~

    Over!!!结束啦~~下一步就是面向对象,冲冲冲!!!

  • 相关阅读:
    微信公众号分销商城源码系统+多元商家+收银台 带完整的搭建教程
    nginx共享内存机制详解
    达索系统仿真Simulia——BMW宝马案例学习 | 达索系统百世慧®
    小学一二年级出题器(JAVA加减乘除)
    电子表电路
    input输入多行文本:删除“首先 其次 此外 总的来说”
    Nacos作为注册中心和配置中心的使用总结
    vue3环境搭建
    SpringBoot学习笔记(五)——Git版本控制
    17、一起来学习MySQL 事务
  • 原文地址:https://blog.csdn.net/Hai_Helloyou/article/details/134022699