DAY003 Go 数组、切片、Map

91 阅读14分钟

指针

两个符号

指针:数据在内存中的地址。

指针变量:存储的数据是指针。

符号名称作用
&取址符返回变量所在的地址
*取值符返回地址对应的值
package main

import "fmt"

func main() {
    var i = 10
    fmt.Println("i 的地址是:", &i) // 0xc00000a0e8
    // p 就是指针变量
    var p *int = &i
    fmt.Println("p 的值:", p)      // 0xc00000a0e8
    fmt.Println("p 的地址对应的值", *p) // 10
}

格式化打印

%p 地址的格式化打印

var x = 10
fmt.Printf("%p\n", &x) // 0xc000094098
// x 的值变了
x = 100
fmt.Printf("%p\n", &x) // 0xc000094098
fmt.Println(*&x)       // 100

指针的题目

var x = 10
// 这儿发生了值拷贝,x 和 y 互不影响
var y = x
// 地址拷贝,x 和 z 相互影响
// z 是一个指针变量
var z = &x
x = 20
fmt.Println(y)  // 10
fmt.Println(*z) // 20

// 可以通过 *指针变量,来修改指针变量对应的值
*z = 30
fmt.Println(x) // 30
var x = 10
// 地址拷贝,x 和 y 相互影响
// y 是一个指针变量
var y = &x
// z => 10,z 是一个普通值
var z = *y
x = 20
fmt.Println(x)  // 20
fmt.Println(*y) // 20
fmt.Println(z)  // 10
var a = 100
// b 是一个指针变量,a 和 b 相互影响
var b = &a
// c 是一个指针变量,c 和 b 相互影响,把 b 的地址(注意是 b 的地址,而不是 b 的内容)给了 c
var c = &b
**c = 200
fmt.Println(a) // 200

关于 new 符号

new 的返回值是一个指针,用来分配内存,make 用来初始化 slice、map 和 channel。

基本数据类型声明之后有一个默认零值,但是指针没有,例如:

var p *int
// 默认是 nil,是不能直接赋值的
fmt.Println(p) // <nil>
// fmt.Println(*p) // 报错,说明并没有开始初始空间
*p = 10 // 报错
var p *int
// 如何解决?
// 通过 new 函数返回一个指向新分配的 int 类型为初始 0 值的指针
p = new(int)
fmt.Println(p) // 0xc000094098
*p = 10
fmt.Println(*p) // 10

数组

特点:数组需要是一组相同类型的数据的有序集合;定义的时候需要定义大小;数组一旦定义了大小不可以改变。

声明数组

var names [5]string
fmt.Println(names, reflect.TypeOf(names)) // [    ] [5]string

var ages [5]int
fmt.Println(ages, reflect.TypeOf(ages)) // [0 0 0 0 0] [5]int
// 注意下面的 x 和 y 的数据类型是不同的
var x [3]int
var y [5]int

初始化数组

先声明再赋值

package main

import "fmt"

func main() {
    var arr1 [5]int
    arr1[0] = 100
    arr1[1] = 200
    arr1[2] = 300
    arr1[3] = 400
    arr1[4] = 500

    // 1. 打印数组
    fmt.Println(arr1)

    // 2. 打印数组的第某一项
    fmt.Println(arr1[1]) // 200
    // 3. 获取数组长度
    fmt.Println("数组的长度:", len(arr1)) // 5
    // 4. 获取数组容量
    fmt.Println("数组的容量:", cap(arr1)) // 5
    // 5. 修改数组中的元素
    arr1[1] = 10
    fmt.Println(arr1[1]) // 10
}

声明和赋值一起

package main

import "fmt"

func main() {
    // 1. 在定义数组的时候就直接初始化
    var arr1 = [5]int{1, 2, 3, 4, 5}
    fmt.Println(arr1)

    // 2. 使用 := 快速初始化
    arr2 := [5]int{1, 2, 3, 4, 5}
    fmt.Println(arr2)
}

自动统计长度

// 3. Go 的编译器会自动根据数组的长度来给 ... 赋值,自动推导长度
var arr3 = [...]int{1, 2, 3, 4, 5, 5, 6, 7, 8, 8, 8}
fmt.Println(len(arr3))

如何给数组的某几个特定的 index 位置赋值

// 如何给数组的某几个特定的 index 位置赋值,其他没有操作到的索引对应到值是此类型的默认值
var arr4 [10]int
arr4 = [10]int{1: 100, 5: 500}
fmt.Println(arr4) // [0 100 0 0 0 500 0 0 0 0]

通过切片来取数组中的值

var names = [...]string{"张三", "李四", "王五", "赵六", "孙七"}
// 切片取值
fmt.Println(names[0:4]) // [张三 李四 王五 赵六]
fmt.Println(names[0:])  // [张三 李四 王五 赵六 孙七]
fmt.Println(names[:3])  // [张三 李四 王五]

遍历数组的两种方式

package main

import "fmt"

func main() {
    var arr1 = [5]int{1, 2, 3, 4, 5}
    // 1. 遍历数组
    for i := 0; i < len(arr1); i++ {
       fmt.Println(arr1[i])
    }
    // 2. for range
    // Goland 快捷方式 数组.for,推荐
    for _, value := range arr1 {
       fmt.Println(value)
    }
}

两种方式的注意点?如何理解 for range 循环时候的 value 是值的副本。

var arr1 = [5]int{1, 2, 3, 4, 5}
// 1. 遍历数组
for i := 0; i < len(arr1); i++ {
    arr1[i] = 8
}
fmt.Println(arr1) // [8 8 8 8 8]

// 2. for range
// Goland 快捷方式 数组.for,推荐
for _, value := range arr1 {
    value = 7
    fmt.Println(value)
}
fmt.Println(arr1) // [8 8 8 8 8]

数组是值类型特点

体现:赋值和函数传参。

package main

import "fmt"

func main() {
    // 数组类型的样子 [size]type
    arr1 := [4]int{1, 2, 3, 4}
    // 数组的值传递和 int 等基本类型一致
    arr2 := arr1
    arr2[0] = 12
    // 修改 arr2 后发现 arr1 并没有变化
    fmt.Println(arr1) // [1 2 3 4]
}

数组的冒泡排序

package main

import "fmt"

// 冒泡:两两比较,大的往后移或者前移,每次可以筛选出一个最大或者最小的数

func main() {
    arr := [...]int{1, 3, 4, 2, 0}
    for i := 0; i < len(arr)-1; i++ {
       // 筛选出来最大数字以后,我们下次不需要将它再计算了
       for j := 0; j < len(arr)-i-1; j++ {
          if arr[j] < arr[j+1] {
             arr[j], arr[j+1] = arr[j+1], arr[j]
          }
       }
    }
    fmt.Println(arr)
}

二维数组的定义和遍历

package main

import "fmt"

func main() {
    // 1. 如何定义二维数组?
    // 长度是 3,里面数组的长度是 4 的 int 型
    arr := [3][4]int{
       {0, 1, 2, 3},
       {4, 5, 6, 7},
       {8, 9, 10, 11},
    }
    // 2. 如何遍历二维数组
    // 2.1. 方式1
    for i := 0; i < len(arr); i++ {
       for j := 0; j < len(arr[i]); j++ {
          fmt.Println(arr[i][j])
       }
    }
    // 2.2 for range
    for _, v := range arr {
       for i := range v {
          fmt.Println(v[i])
       }
    }
}

切片

基本概念

// 1. 切片有点像动态数组,所以,定义切片的时候不需要指定长度
// 2. 它是对现有数组的引用
// 3. 切片本身不存储任何数据,都是底层的数组来存储的,所以修改了切片也就是修改了这个数组中的数据

// 从概念上面来说 slice 像一个结构体,这个结构体包含了三个元素:
// 1. 指针:「指向数组中」 slice 指定的开始位置
// 2. 长度:即 slice 的长度
// 3. 最大长度:也就是 slice 开始位置到数组的最后位置的长度

声明切片的两种方式

从数组或切片上取得数据;直接声明切片。

package main

import "fmt"

func main() {
    // 1. 定长的是数组
    arr := [4]int{1, 2, 3, 4}
    fmt.Println(arr)

    // 2. 不定长的是切片
    var s1 []int // 变长,长度是可变的
    fmt.Println(s1)

    // 3. 如何判断切片为空?
    // 初始的切片中,默认是 nil
    if s1 == nil {
       fmt.Println("切片是空的")
    }
    
    // 这样是会报错的,因为上面只是声明了切片,并没有初始化,可以通过后面的 make 进行初始化
    // s1[0] = 3

    // 4. 定义切换的时候可以指定初始数据
    s2 := []int{1, 2, 3, 4}
    fmt.Println(s2)
    fmt.Printf("%T, %T\n", arr, s2) // [4]int, []int

    // 5. 获取切片中的某个数据
    fmt.Println(s2[1])
}

思考数组和切片的关系。

var arr = [5]int{1, 2, 3, 4, 5}
var s1 = arr[1:4]
fmt.Println(s1, reflect.TypeOf(s1)) // [2 3 4] []int

var s2 = arr[2:5]
fmt.Println(s2, reflect.TypeOf(s2)) // [3 4 5] []int

var s3 = s2[0:2]
s3[0] = 100

fmt.Println(s1, s2, s3) // [2 100 4] [100 4 5] [100 4]

通过 make 初始化切片

思考:容量为 10,长度为 5,我能存放 6 个数据吗?

package main

import "fmt"

func main() {
    // 创建一个切片,长度,容量
    // make([]Type,length,capacity)

    s1 := make([]int, 5, 10)
    fmt.Println(len(s1), cap(s1)) // 5 10
    // 思考:容量为 10,长度为 5,我能存放 6 个数据吗?
    s1[0] = 10
    // 直接赋值的方式不行
    // s1[5] = 200
    
    // 通过 append 操作会自动扩容,这样是可以的
    // 切片的底层还是数组
    fmt.Println(s1)
}

append 添加会自动扩容

package main

import "fmt"

func main() {
    s1 := make([]int, 0, 5)
    s1 = append(s1, 1, 2)
    // 问题:容量只有 5 个,那能放超过 5 个的吗?
    // 可以,通过 append 操作切片是会自动扩容的
    s1 = append(s1, 3, 4, 5, 6, 7)
    fmt.Println(s1) // [1 2 3 4 5 6 7]
}

如何合并两个切片

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{4, 5, 6}
    s1 = append(s1, s2...)

    fmt.Println(s1) // [1 2 3 4 5 6]
}

遍历切片的两种方式

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3, 4}
    
    // 方式 1
    for i := 0; i < len(s1); i++ {
       fmt.Println(s1[i])
    }
    
    // 方式 2
    for i, v := range s1 {
       fmt.Println(s1[i], v)
    }
}

append 操作切片扩容分析(重)

  • 向切片中添加数据的时候,如果没有超过容量,直接添加,如果超过了这个容量,就会自动扩容,cap 成倍的增加,拷贝。
  • 切片一旦扩容(超出了原来的容量),就是重新指向一个新的底层数组。
package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}
    fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:3,cap:3
    fmt.Printf("%p\n", s1)                          // 0xc00012a000

    s1 = append(s1, 4, 5)
    fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:5,cap:6
    // 扩容后,s1 重新指向了新地址
    fmt.Printf("%p\n", s1)                          // 0xc000132000

    s1 = append(s1, 6, 7, 8)
    fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:8,cap:12
    // 扩容后,s1 重新指向了新地址
    fmt.Printf("%p\n", s1)                          // 0xc0001220c0

    s1 = append(s1, 9, 10)
    fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:10,cap:12
    // 没有超出原来的容量,地址不变
    fmt.Printf("%p\n", s1)                          // 0xc0001220c0

    s1 = append(s1, 11, 12, 13, 14)
    fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:14,cap:24
    fmt.Printf("%p\n", s1)                          // 0xc000134000
}

通过数组来创建切片

package main

import "fmt"

func main() {
    // 数组 [0,10)
    // 1. 通过数组来创建切片,先定义一个数组
    arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Printf("原数组:%p\n", &arr) // 原数组 0xc0000160f0
    
    // 2. 数组截取的结果其实一个切片
    s1 := arr[:5]                // [1 2 3 4 5]
    s2 := arr[3:8]               // [4 5 6 7 8]
    s3 := arr[5:]                // [6 7 8 9 10]
    s4 := arr[:]                 // [1 2 3 4 5 6 7 8 9 10]
    
    // 3. 查看容量和长度
    fmt.Printf("s1 len:%d, cap:%d\n", len(s1), cap(s1)) // s1 len:5, cap:10
    fmt.Printf("s2 len:%d, cap:%d\n", len(s2), cap(s2)) // s2 len:5, cap:7,从3开始截取的,所以容量是 7
    fmt.Printf("s3 len:%d, cap:%d\n", len(s3), cap(s3)) // s3 len:5, cap:5
    fmt.Printf("s4 len:%d, cap:%d\n", len(s4), cap(s4)) // s4 len:10, cap:10
    
    // 3. 查看切片的内存地址
    fmt.Printf("%p\n", s1)      // 指向了原数组,0xc0000160f0
    fmt.Printf("%p\n", s2)      // 数组是有序的,由于截断了,指向了新数组,0xc000016118
    fmt.Printf("%p\n", s3)      // 数组是有序的,由于截断了,指向了新数组,0xc000016118
    fmt.Printf("%p\n", s4)      // 指向了原数组,0xc0000160f0
}

切片的修改和扩容

package main

import "fmt"

func main() {
    // 数组 [0,10)
    // 1. 通过数组来创建切片,先定义一个数组
    arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Printf("原数组:%p\n", &arr) // 原数组 0xc0000160f0
    
    // 2. 通过截取数组生成切片
    s1 := arr[:5]                   // [1 2 3 4 5]
    s2 := arr[3:8]                  // [4 5 6 7 8]

    // 3. 修改数组的内容,切片也会随之发生了变化,因为切片并不保存数据,是对原数组的引用
    arr[2] = 100
    fmt.Println(s1) // [1 2 100 4 5]

    // 4. 修改切片的内容,数组也随之发生了变化
    s2[2] = 80
    fmt.Println(arr) // [1 2 100 4 5 80 7 8 9 10]
    
    // 5. 切片扩容,注意这儿添加了 6 个,添加完毕后超过了数组原来的容量
    // 如果在切片容量内添加,会导致原来的数组数据发生修改
    // 如果添加后超过了切片的容量,原数组的数据不会发生变化,但其实会导致底层数组拷贝产生一个新的切片
    s1 = append(s1, 11, 12, 13, 14, 15, 16)
    fmt.Println("切片扩容之后的新数组:", arr) // [1 2 100 4 5 80 7 8 9 10]
    
    // 7. 扩容后的新切片,指向,拷贝原数组的数据生成的一个新数组,和原来的数组不再是同一个地址了
    fmt.Printf("切片扩容之后的 s1 地址:%p\n", s1) // 0xc000132000
}

切片是引用类型的特点

package main

import "fmt"

func main() {
    arr := [2]int{1, 2}
    s1 := []int{1, 2, 3, 4}
    s2 := s1
    s1[0] = 100
    // 虽然修改的是 s1,发现 s2 也发生了变化
    fmt.Println(s2) // [100 2 3 4]
    // s1 和 s2 指向同一个地址
    // 由于切片是引用类型,可以直接输出获取地址,数组是值类型,要通过 & 获取地址
    fmt.Printf("%p, %p, %p\n", s1, s2, &arr)
}

深拷贝和浅拷贝

  • 值类型的数据,默认都是深拷贝,例如 array、int、float、string、bool、struct 等。

  • 引用类型的数据,默认都是浅拷贝,例如 slice、map、chan,拷贝的是地址,会导致多个变量指向同一块内存。

如何实现切片的深拷贝?

package main

import "fmt"

// 方法 1,通过 for 循环

func main() {
    s1 := []int{1, 2, 3, 4}
    s2 := make([]int, 0)
    for i := 0; i < len(s1); i++ {
       s2 = append(s2, s1[i])
    }
    fmt.Printf("%p, %p\n", s1, s2)

    //s2 = append(s2, s1...)
    //fmt.Printf("%p, %p\n", s1, s2)
}
package main

import "fmt"

// 方法 2,通过 copy 方法

func main() {
    s1 := []int{1, 2, 3, 4}
    s2 := make([]int, len(s1)) // 关键:目标切片长度必须 >= 源切片长度
    copy(s2, s1)               // 将 s1 的内容拷贝到 s2
    fmt.Println(s2)            // 输出 [1 2 3 4]
}

映射

基本概念

Map 是一种无序的键值对的集合,所以我们可以像迭代数组和切片那样迭代它,Map 也是引用类型。

package main

import "fmt"

func main() {
    // 1. 基本操作,map[key]value
    var map1 map[int]string // 只是声明了但是没有初始化,默认是 nil
    if map1 == nil {
       fmt.Println("map 没有初始化默认是 nil")
    }
    
    // 2. 需要进一步使用 make 方法创建
    var map2 = make(map[string]string)
    fmt.Println(map1) // map[],不能赋值,例如 map1[1] = "value",这时候会报错 panic
    fmt.Println(map2) // map[]

    // 3. 声明和初始化可以一起
    var map3 = map[string]int{"Go": 100, "Java": 10, "C": 60}
    fmt.Println(map3)
    fmt.Printf("%T\n", map3) // map[string][int]
}

操作 Map

package main

import "fmt"

func main() {
    // 1. 创建 map
    var map1 map[int]string // nil,不能使用的,只是声明了,还没有初始化
    
    // 2. 进一步通过 make 初始化
    map1 = make(map[int]string)
    map1[100] = "JavaScript"
    map1[200] = "Java"

    fmt.Println(map1)
    fmt.Println(map1[200])
    fmt.Println(map1[1]) // 不存在,默认值 string ""

    // 3. 取值的时候可以判断
    value, ok := map1[1]
    if ok {
       fmt.Println("map key 存在,value 是:", value)
    } else {
       fmt.Println("map key 不存在!")
    }

    // 4. 下面这句话表示,如果存在 key 就是修改数据,否则就是创建新的 key 和数据
    map1[100] = "xxx"

    // 5. 删除 map 中的数据
    delete(map1, 100)

    // 6. map 的大小
    fmt.Println(len(map1))
}

遍历 Map

package main

import "fmt"

/*
1. map 是无序的,每次打印出来的值可能都不一样,它不能通过 index 获取,只能通过 key 来获取
2. map 的长度是不固定的,是引用类型
3. len 可以用于 map 查看 map 中数据的数量,但是 cap 无法使用
4. map 的 key 可以是布尔类型、整数、浮点数、字符串
*/
func main() {
    var map1 = map[string]int{"Go": 50, "Java": 89, "C": 87, "Python": 90}
    for k, v := range map1 {
       fmt.Println(k, v)
    }
}

将 Map 放到切片中

package main

import "fmt"

func main() {
    // 1. 创建 3 个人对象
    user1 := make(map[string]string)
    user1["name"] = "ifer"
    user1["age"] = "19"

    user2 := make(map[string]string)
    user2["name"] = "elser"
    user2["age"] = "32"

    user3 := map[string]string{"name": "xxx", "age": "88"}

    // 2. 创建有 Map 组成的切片
    // 默认长度=0,容量=3,没有问题!
    // 初始不能指定长度等于 3,否则会添加三个空数据
    userDatas := make([]map[string]string, 0, 3)

    // 3. 扩容切片
    userDatas = append(userDatas, user1)
    userDatas = append(userDatas, user2)
    userDatas = append(userDatas, user3)

    // 4. 遍历切片,输出一个一个的 Map
    for _, user := range userDatas {
       fmt.Println(user)
    }
}