【Go语言】复合数据类型(数组,切片,管道,哈希键值对)

一、数组 Array

数组是快连续的内存空间,在声明的时候必须指定长度,且长度不能改变,所以数组在声明的时候就可以把内存空间分配好,并赋上默认值,即完成了初始化

数组的地址,就是首元素地址

数组的初始化

func Var_array() {
    var arr1 [5]int = [5]int{}               // 声明数组时指定长度和类型,且长度和类型指定后不可修改
    var arr2 = [5]int{}                      //对数组进行初始化赋值时定义长度和类型
    var arr3 = [5]int{2, 3}                  // 给前两个元素赋值
    var arr4 = [5]int{2: 15, 4: 30}          //指定元素进行赋值
    var arr5 = [...]int{3, 2, 4, 15, 32, 22} //根据{}中的元素个数自动定义数组长度
    var arr6 = [...]struct {
        name string
        age  int8
        city string
    }{{"Tom", 23, "ShenZhen"},
        {"Janzen", 25, "ShangHai"},
        {"Sophia", 24, "ShenZhen"}} // 数组的元素类型由匿名结构体给定
}

二维数组初始化

func Var_array2() {
    var arr1 = [5][3]int{{23, 21, 2}, {2: 21}} //定义一个5行3列的二维数组,前两行元素为 {23,21,2} {0,0,21}
    var arr2 = [...][5]int{{1, 3}, {3: 3}}     // 二维数组,第一维可以使用[...],第二维不可使用[...]
}
PS F:\go\go_project\test> go run .\main.go
arr1 = [[23 21 2] [0 0 21] [0 0 0] [0 0 0] [0 0 0]]
arr2 = [[1 3 0 0 0] [0 0 0 3 0]]

 

数组里的元素访问

  • 通过index访问
    • 首元素 arr[0]
    • 末元素 arr[len(arr)-1]
  • 访问二维数组里的元素
    • 获取第三行第四列的元素 arr[2][3]

 

遍历数组

范例:

func Foreach_array() {
    // 定义一个一维数组,数组的元素类型由匿名结构体给定
    var arr1 = [...]struct {
        name string
        age  int8
        city string
    }{{"Tom", 23, "ShenZhen"}, {"Janzen", 25, "ShangHai"}, {"Sophia", 24, "ShenZhen"}}

    // 使用index,value 方式遍历数组,如果不需要使用index值,
   // 如果不需要 index 值,可写成 for _,ele := range arr1{} ;
   // for i := range arr1{} 等价于 for i,_ := range arr1{}
for i, ele := range arr1 { fmt.Printf("index=%d, value=%v\n", i, ele) } // 使用下标方式遍历数组 for i := 0; i < len(arr1); i++ { //声明并赋值i, 当i小于arr1长度时,执行循环,代码块完成后执行i++ fmt.Printf("index=%d, value=%v\n", i, arr1[i]) } //定义一个5行3列的二维数组,前两行元素为 {23,21,2} {0,0,21} var arr2 = [5][3]int{{23, 21, 2}, {2: 21}} // 使用双层嵌套循环实现遍历二维数组 for row, array := range arr2 { for col, ele := range array { fmt.Printf("arr2[%d][%d] = %v\n", row, col, ele) } } }

输出结果:

  

 cap 和 len

  • cap 代表 capacity 容量
  • len 代表 length 长度
  • len 代表目前数组里有几个元素,cap 代表给数组分配的内存空间可以容纳多少个元素
  • 由于数组初始化之后长度不会改变,不需要给它预留内存空间,所以 len(arr) == cap(arr)

数组传参

  • 数组的长度和类型都是数组的一部分,函数传递数组类型时这两部分必须吻合(函数内定义接收传参的数组长度和类型,必须与实际传入的数组格式一致)
  • go语言没有按引用传参,全部是按值传参,即传参实际传递的是数组的拷贝,当数组长度很大时,仅传参消耗就很大(函数内修改传入的数组,不影响函数外部原数组值)
  • 在函数内修改函数外部的数组,可以通过传递数组的指针实现,传参消耗相对较小(数组在内存中的地址)

 范例:

// 函数定义传参数组格式,求数组内的平均值
func Avg_arry(arr [5]float64) float64 {
    var sum float64
    for _, ele := range arr {
        sum += ele
    }
    avg := sum / float64(len(arr))
    return avg
}

func main() {
    arr := [5]float64{3.13, 3, 15, 23, 1}
    avg := data_type.Avg_arry(arr)
    fmt.Println(avg)

}

输出结果:

  

 

范例:

// 验证函数传递数组作为参数,在函数内对数组的修改
func Arr_array(arr [5]int) {
    fmt.Printf("arr's ptr = %p\n", &arr)
    fmt.Printf("old arr[0] = %d\n", arr[0])
    arr[0] += 10  //等价于  (*arr)[0] += 10
    fmt.Printf("new arr[0] = %d\n", arr[0])
}

func main() {
    crr := [5]int{13, 3, 15, 23, 1}
    fmt.Printf("crr's ptr = %p\n", &crr)
    data_type.Arr_array(crr)
    fmt.Printf("crr[0] = %d\n", crr[0])

}

 输出结果:

  

 

范例:

// 验证函数传递数组指针作为参数,在函数内对数组的修改
func Ptr_array(arr *[5]int) {
    fmt.Printf("arr's ptr = %p\n", arr)
    fmt.Printf("old arr[0] = %d\n", arr[0])
    arr[0] += 10
    fmt.Printf("new arr[0] = %d\n", arr[0])
}

func main() {
    crr := [5]int{13, 3, 15, 23, 1}
    fmt.Printf("crr's ptr = %p\n", &crr)
    data_type.Ptr_array(&crr)
    fmt.Printf("crr[0] = %d\n", crr[0])

}

 输出结果:

  

 

二、切片 Slice

  • 切片具备三个元素:array (unsafe.Pointer)、len (int)、cap(int)
  • 切片指针 和 底层数组首元素(底层数组)指针 不是同一个指针

切片的初始化

范例:

func Slice_var() {
    var s1 []int
    s2 := []int{}
    s3 := make([]int, 3)
    s4 := make([]int, 3, 5)
    s5 := []int{1, 2, 3, 4, 5}
    s6 := [][]int{
        {1, 2, 3}, {2, 3, 1, 1},
    }

    fmt.Printf("s1 = %v,\tlen = %d,\tcap=%d\n", s1, len(s1), cap(s1))
    fmt.Printf("s2 = %v,\tlen = %d,\tcap=%d\n", s2, len(s2), cap(s2))
    fmt.Printf("s3 = %v,\tlen = %d,\tcap=%d\n", s3, len(s3), cap(s3))
    fmt.Printf("s4 = %v,\tlen = %d,\tcap=%d\n", s4, len(s4), cap(s4))
    fmt.Printf("s5 = %v,\tlen = %d,\tcap=%d\n", s5, len(s5), cap(s5))
    fmt.Printf("s6 = %v,\tlen = %d,\tcap=%d\n", s6, len(s6), cap(s6))

}

func main() {
    Slice_var()
}

 

输出结果:

   

 

append

  • 切片相对于数组最大的特点就是可以追加元素,可以自动扩容
  • 追加的元素放到预留的内存空间里,同时 len 加 1
  • 如果预留空间已用完,则会重新申请一块更大的底层数据内存空间,capacity 变成之前的2倍(cap < 1024)或 1.25倍(cap > 1024)。把原内存空间的数据拷贝过来,在新的内存空间 上执行append操作

范例:

 

func Append_slice() {
    s := make([]int, 3, 5)
    fmt.Printf("s = %v,\tlen = %d,\tcap=%d,\t(%p)\n", s, len(s), cap(s), &s)
    s = append(s, 100)
    fmt.Printf("s = %v,\tlen = %d,\tcap=%d,\t(%p)\n", s, len(s), cap(s), &s)
}

 

输出结果:

  

 

范例:

func coef_cap(n int) {
    s := make([]int, 0, 5)
    prevCap := cap(s)
    prevPtr := &s[0]
    for i := 0; i < n; i++ {
        s = append(s, 100)
        currCap := cap(s)
        currPtr := &s[0]
        if currCap > prevCap {
            fmt.Printf("cap %d --> cap %d\t", prevCap, currCap)
            fmt.Printf("ptr %p --> ptr %p\n", prevPtr, currPtr)
            prevCap = currCap
            prevPtr = currPtr
        }
    }
}

func main() {
    coef_cap(5000)
}

 

输出结果:

   

 

截取子切片

  • arr := make([]int, 4, 6) //母切片
    crr := arr[1:3] //子切片
  • 刚开始,子切片和母切片的底层数据共享内存空间,修改子切片会反映到母切片上,在子切片上执行 append 操作,会把新增的元素放到母切片的预留空间的内存上
  • 当子切片不断执行 append,消耗光了母切片的预留内存空间,子切片会跟母切片发生内存分离,此后两个切片没有任何关系

  

范例:

func sub_slice(n int) {
    arr := make([]int, 4, 6)
    arr[3] = 10
    fmt.Printf("arr=%v,(%p)\n", arr, &arr[0])
    crr := arr[1:3] //前闭后开
    crr[0] = 8
    crr[1] = 7
    fmt.Printf("arr=%v,(%p)\tcrr=%v(%p)\n", arr, &arr[0], crr, &crr[0])
    for i := 0; i < n; i++ {
        crr = append(crr, 1)
        fmt.Printf("arr=%v(%p)\tcrr=%v(%p)\n", arr, &arr[0], crr, &crr[0])
    }
    arr = append(arr, 2)
    crr[2] = 99
    fmt.Printf("arr=%v(%p)\tcrr=%v(%p)\n", arr, &arr[0], crr, &crr[0])
}

func main() {
    sub_slice(8)
}

 

输出结果:

   

 

子切片调用导致的内存泄露情况

由于子切片与母切片共享底层内存空间,所以当子切片被调用时,母切片也处于被子切片调用状态,因此程序不会回收母切片占用空间,就会导致母切片过度占用内存空间引发内存泄露情况。

可能引发内存泄露的代码片段

// 下方程序会导致 foo() 中的 arr 长时间处于被调用状态,导致内存泄露

func foo () []int {
    arr := make([]int,1<<20) // 占用1M内存的切片
    /*
    arr 进行数据处理
    */
    return arr[3:10]
}

func main() {
    crr := foo()
    fmt.Println(crr)
    /*
    程序代码片段2
    */
}

 

解决方案:

1、通过将所需调用的子切片重新赋值给一个新的切片,从而减少对原切片的持续占用

func foo () []int {
    arr := make([]int,1<<20) // 占用1M内存的切片
    /*
    arr 进行数据处理
    */
    brr := make([]int,0,10)
    for i:=3;i<10;i++ {
        brr = append(brr,arr[i])
    }
    return brr
}

func main() {
    crr := foo()
    fmt.Println(crr)
    /*
    程序代码片段2
    */
}

 

2、针对指针切片时,需要舍弃的元素数量较小时,可以将舍弃元素赋值为 nil 实现对舍弃元素的内存空间释放

type user struct {
    Nmae string
    Age  int8
}

func foo2() []*user {
    arr := make([]*user, 20) // 占用1M内存的切片
    /*
        arr 进行数据处理,舍弃末尾3个元素 arr[:len(arr)-3]
    */
    arr[len(arr)-3] = nil
    arr[len(arr)-2] = nil
    arr[len(arr)-1] = nil
    return arr
}

func main() {
    crr := foo2()
    fmt.Println(crr)
    /*
        程序代码片段2
    */
}

 

 

切片传参 

 go语言函数传参,传递的都是值,即传切片会把切片的{array.Pointer,len,cap} 这3个字段拷贝一份到函数内部

由于传递的是底层数组的指针,所以可以直接修改切片底层数组里的元素,会反映到原切片上

范例:

func update_slice(arr []int) {
    arr[0] = 21
    fmt.Printf("arr=%v(%p)\tarray.Pointer = %p\n", arr, &arr, &arr[0])
}

func main() {
    crr := []int{1, 2, 3, 4}
    fmt.Printf("crr=%v(%p)\tarray.Pointer = %p\n", crr, &crr, &crr[0])
    update_slice(crr)
    fmt.Printf("crr=%v(%p)\tarray.Pointer = %p\n", crr, &crr, &crr[0])
}

 

输出结果:

  

 

三、哈希 map

map 底层原理是 hash table,根据key查找value的时间复杂度为O(1)

map 中的 key 可以是任意能够使用 == 进行比较的类型,不能是 函数、map、切片,以及包含上述3种类型成员变量的 结构体 struct。

map 中的 value 可以是任意类型

slot 槽位中只保存目标数据的指针

冲突:当多个 key值 的 哈希取模 结果相同时,会在发生冲突的槽位上以链表方式保存多个 key 的指针,查询时对冲突槽位进行遍历查询

扩容:当map中出现过多冲突数据时,会发生槽位扩容,当触发槽位扩容时,会对所有数据重新进行哈希取模计算,并重新分配槽位。发生扩容时严重影响系统性能,尽可能避免map发生扩容情况

 map的初始化

范例:

 

func map_var() {
    var m1 map[string]int // 声明map,指定key 和 value 的类型
    fmt.Printf("m1 = %v\n", m1)
    m2 := make(map[string]int) // 初始化map,容量为0
    fmt.Printf("m2 = %v\n", m2)
    m3 := make(map[string]int, 5) // 初始化map,容量为5
    fmt.Printf("m3 = %v\n", m3)
    m4 := map[string]int{"语文": 95, "数学": 80} //初始化map时直接赋值
    fmt.Printf("m4 = %v\n", m4)
}

func main() {
    map_var()
}

输出结果:

  

  

添加和删除key

范例:

func map_fixed() {
    m := map[string]int{"语文": 95, "数学": 80}
    fmt.Printf("m = %v\n", m)
    m["英语"] = 99 // key 不存在,直接添加key-value
    fmt.Printf("m = %v\n", m)
    m["英语"] = 89 // key 存在,直接更新key-value
    fmt.Printf("m = %v\n", m)
    delete(m, "数学") // 删除 key-value
    fmt.Printf("m = %v, len = %d\n", m, len(m))

}

func main() {
    map_fixed()
}

 

输出结果:

  

 

查看key对应的value值

范例:

//不推荐采用 value:=m["key"] 方式获取
func
select_vale_1() { m := map[string]int{"语文": 95, "数学": 80} value1 := m["数学"] // key 值存在时,返回对应的value value2 := m["英语"] // key 值不存在时,返回value定义的默认值 fmt.Printf("m[\"数学\"] = %v\n", value1) fmt.Printf("m[\"英语\"] = %v\n", value2) } func main() { // map_var() // map_fixed() select_vale_1() }

 

输出结果:

   

范例:

func select_vale_2(key string) {
    m := map[string]int{"语文": 95, "数学": 80}
    if value, exists := m[key]; exists {
        fmt.Printf("m[\"%s\"] = %d\n", key, value)
    } else {
        fmt.Printf("m[\"%s\"] 不存在\n", key)
    }

}

func main() {
    select_vale_2("语文")
    select_vale_2("英语")
}

 

输出结果:

   

 

遍历map 

map 的 key值 排布相对顺序是固定的,但每次遍历出来的排序次序可能存在不一致(环形排列,随机起始)

范例:

func map_for() {
    m := make(map[string]int, 5)
    m["A"] = 1
    m["S"] = 2
    m["D"] = 3
    m["W"] = 4
    fmt.Printf("m = %v\n", m)

    for key, value := range m {
        fmt.Printf("key = %s , value = %d\n", key, value)
    }
    fmt.Println("=================")
    for key, value := range m {
        fmt.Printf("key = %s , value = %d\n", key, value)
    }

    for key := range m {
        fmt.Printf("key = %s\n", key)
    }

    for _, value := range m {
        fmt.Printf("value = %d\n", value)
    }
}

func main() {
    map_for()
}

 

输出结果:

   

 

范例:

func main() {

    m := map[string]int{"A": 21, "B": 23, "F": 65, "T": 31}
    fmt.Println(m)

    // 不会实际修改 m 里的内容
    for key, value := range m {
        value += 2
        fmt.Printf("value = %d , key-value = %d\n", value, m[key])
    }
    fmt.Println(m)

    // 遍历修改 m 里的内容
    for key := range m {
        m[key] += 2
    }
    fmt.Println(m)
}

 

输出结果:

    

 

 

四、管道 channl

  •  管道的底层是一个环形队列(先进先出),send(插入)和 recv(取走)从同一个位置沿同一个方向顺序执行
  • sendx 表示最后一次插入的元素位置,recvx 表示最后一次取走的元素位置

channl 初始化

 范例:

func channl_var() {
    var ch chan int // 声明管道
    fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch)

    ch = make(chan int, 8) //初始化,环形队列中可容纳8个int
    fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch)

}

func main() {
    channl_var()
}

输出结果:

send 和 recv

范例:

func channl_send_recv() {
    ch := make(chan int, 8)
    fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch)
    ch <- 1 // 往管道中写入(send)元素
    fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch)
    v := <-ch // 往管道中取走(recv)元素
    fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch)
    fmt.Printf("v type is %T, v = %v\n", v, v)
    ch <- 2
    ch <- 3
    fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch)
    v = <-ch
    v = <-ch
    fmt.Printf("ch type is %T, len = %d, cap = %d, ch = %v\n", ch, len(ch), cap(ch), ch)
}

func main() {
    channl_send_recv()
}

输出结果:

 当管道内的元素,达到最大容量时(len=cap),再进行元素插入会发生阻塞导致后续的代码执行失败

 当管道内的元素,全部被取完时(len = 0),再进行元素取出也会发生阻塞导致后续的代码执行失败

范例:

func channl_send_out() {
    ch := make(chan int, 8)
    for i := 0; i < 8; i++ {
        ch <- 10
    }
    fmt.Printf("ch type is %T, len = %d, cap = %d\n", ch, len(ch), cap(ch))

    ch <- 11
    fmt.Printf("ch type is %T, len = %d, cap = %d\n", ch, len(ch), cap(ch))
}

func main() {
    channl_send_out()
}

输出结果:

遍历管道

范例:

func channl_for_1() {
    ch := make(chan int, 8)
    for i := 0; i < cap(ch); i++ {
        ch <- i
    }

    L := len(ch)
    for i := 0; i < L; i++ {
        ele := <-ch
        fmt.Printf("ele = %v, ch len=%d\n", ele, len(ch))
    }
    fmt.Printf("len ch = %d\n", len(ch))
}

func channl_for_2() {
    ch := make(chan int, 8)
    for i := 0; i < cap(ch); i++ {
        ch <- i
    }

    close(ch) //使用此种方法遍历管道之前需要先关闭管道,禁止写入新的元素
    for ele := range ch {
        fmt.Printf("ele = %v, ch len=%d\n", ele, len(ch))
    }
    fmt.Printf("len ch = %d\n", len(ch))
}

func main() {
    fmt.Println("========for 1========")
    channl_for_1()
    fmt.Println("\n========for 2========")
    channl_for_2()
}

输出结果:

 

引用类型

  • slice、map、channl 都是go中的3种引用类型,都可以通过 make 函数进行初始化(申请内存分配)
  • 因为它们都包含一个指向底层数据结构的指针,所以称之为“引用”类型
  • 引用类型未初始化时都是 nil ,可以对它们使用 len() 函数,返回 0
posted @ 2023-07-26 23:51  Janzen_Q  阅读(31)  评论(0编辑  收藏  举报