第02部分 Go开发学习 第7章 最常用的复杂数据类型 - map、数组、 列表和切片

188 阅读12分钟

第02部分 Go开发学习 第7章 最常用的复杂数据类型 - map、数组、 列表和切片

第7章 最常用的复杂数据类型 - map、数组、 列表和切片

7.1 python的数组细节

对于Python列表-我们可以由此得出结论,对于每个新元素,我们需要另外八个字节来引用新对象。新的整数对象本身消耗28个字节。列表“ lst”的大小,不包含元素的大小,可以使用以下公式计算:

64 + 8 * len(lst) + *+ len(lst) ** 28

python中的list是python的内置数据类型,list中的数据类不必相同的,而array的中的类型必须全部相同。在list中的数据类型保存的是数据的存放的地址,简单的说就是指针,并非数据,这样保存一个list就太麻烦了,例如list1=[1,2,3,'a']需要4个指针和四个数据,增加了存储和消耗cpu。

array底层是C语言。有步长strides,dimensions,data三属性。固定类型的numpy类型的数组array缺乏这种灵活,但是更方便进行存储和处理数据。

96 + n * 8 Bytes

数组还有一个特性:数组的大小一开始需要指定好,后期动态扩充会拷贝值

对于机器学习的同学来说:数组用的更多,一般都是用numpy的array。算法上不会使用list

from array import array

#arr的类型 https://docs.python.org/3/library/array.html
a = array('b', [1, 3, 5])
for item in a:
    print(item)

#使用切片获取
numbers_list = [2, 5, 62, 5, 42, 52, 48, 5]
numbers_array = array('i', numbers_list)

print(numbers_array[2:5]) # 3rd to 5th
print(numbers_array[:-5]) # beginning to 4th
print(numbers_array[5:])  # 6th to end
print(numbers_array[:])   # beginning to end

#添加元素
numbers = array('i', [1, 2, 3, 5, 7, 10])

# changing first element
numbers[0] = 0
print(numbers)     # Output: array('i', [0, 2, 3, 5, 7, 10])

# changing 3rd to 5th element
numbers[2:5] = array('i', [4, 6, 8])
print(numbers)     # Output: array('i', [0, 2, 4, 6, 8, 10])

#通过extend和append方法扩展数组
numbers = array('i', [1, 2, 3])

numbers.append(4)
print(numbers)     # Output: array('i', [1, 2, 3, 4])

# extend() appends iterable to the end of the array
numbers.extend([5, 6, 7])
print(numbers)     # Output: array('i', [1, 2, 3, 4, 5, 6, 7])

#使用+连接数组
odd = array('i', [1, 3, 5])
even = array('i', [2, 4, 6])

numbers = array('i')   # creating empty array of integer
numbers = odd + even
print(numbers)

7.2 go语言的数组

机器学习的同学对python的array应该很了解,但是做web开发的同学对python的array使用的不多

Go 语言提供了数组类型的数据结构。 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。

数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的下标取值范围是从0开始,到长度减1。

数组一旦定义后,大小不能更改。

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。

相对于去声明 number0, number1, ..., number99 的变量,使用数组形式 numbers[0], numbers[1] ..., numbers[99] 更加方便且易于扩展。

数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。

图片.png

声明和初始化数组

需要指明数组的大小和存储的数据类型。

var balance [10] float32
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

var balance = []float32{1000.0, 2.0, 3.4, 7.0, 50.0}

balance[4] = 50.0

数组的其他创建方式:

var a [4] float32 // 等价于:var arr2 = [4]float32{}
fmt.Println(a) // [0 0 0 0]
var b = [5] string{"ruby", "王二狗", "rose"}
fmt.Println(b) // [ruby 王二狗 rose  ]
var c = [5] int{'A', 'B', 'C', 'D', 'E'} // byte
fmt.Println(c) // [65 66 67 68 69]
d := [...] int{1,2,3,4,5}// 根据元素的个数,设置数组的大小
fmt.Println(d)//[1 2 3 4 5]
e := [5] int{4: 100} // [0 0 0 0 100]
fmt.Println(e)
f := [...] int{0: 1, 4: 1, 9: 1} // [1 0 0 0 1 0 0 0 0 1]
fmt.Println(f)

访问数组元素

package main

import "fmt"

func main() {
   var n [10]int /* n 是一个长度为 10 的数组 */
   var i,j int

   /* 为数组 n 初始化元素 */         
   for i = 0; i < 10; i++ {
      n[i] = i + 100 /* 设置元素为 i + 100 */
   }

   /* 输出每个数组元素的值 */
   for j = 0; j < 10; j++ {
      fmt.Printf("Element[%d] = %d\n", j, n[j] )
   }
}

数组的长度

通过将数组作为参数传递给len函数,可以获得数组的长度。

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    fmt.Println("length of a is",len(a))

}

您甚至可以忽略声明中数组的长度并将其替换为…让编译器为你找到长度。这是在下面的程序中完成的。

package main

import (  
    "fmt"
)

func main() {  
    a := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a)
}

遍历数组:

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ { //looping from 0 to the length of the array
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}

使用range遍历数组:

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    sum := float64(0)
    for i, v := range a {//range returns both the index and value
        fmt.Printf("%d the element of a is %.2f\n", i, v)
        sum += v
    }
    fmt.Println("\nsum of all elements of a",sum)
}

如果您只需要值并希望忽略索引,那么可以通过使用_ blank标识符替换索引来实现这一点。

for _, v := range a { //ignores index  
}

数组是值类型

数组是值类型 Go中的数组是值类型,而不是引用类型。这意味着当它们被分配给一个新变量时,将把原始数组的副本分配给新变量。如果对新变量进行了更改,则不会在原始数组中反映。

package main

import "fmt"

func main() {  
    a := [...]string{"USA", "China", "India", "Germany", "France"}
    b := a // a copy of a is assigned to b
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}

数组的大小是类型的一部分。因此[5]int和[25]int是不同的类型。因此,数组不能被调整大小。不要担心这个限制,因为切片的存在是为了解决这个问题。

package main

func main() {  
    a := [3]int{5, 78, 8}
    var b [5]int
    b = a //not possible since [3]int and [5]int are distinct types
}
var al []int     //创建slice
//sl := make([]int,10)  //创建有10个元素的slice
sl:=[3]int{1,2,3} //创建有初始化元素的slice
s2:=[4]int{1,2,3,4} //创建有初始化元素的slice
fmt.Printf("%T, %T, %T", al, sl, s2)

7.3. go语言的切片

1. 什么是切片

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大

Go的切片类型为处理同类型数据序列提供一个方便而高效的方式。 切片有些类似于其他语言中的数组,但是有一些不同寻常的特性。 本文将深入切片的本质,并讲解它的用法。

2. 定义切片

// 第一种
var identifier []type
//第二种 使用make
var slice1 []type = make([]type, len)
简写成
slice1 := make([]type, len)
#使用make来创建slice,map,chanel说明如下

#第三种,通过对数组操作返回
course := [5]string{"django", "tornado", "scrapy", "python", "asyncio"}
subCourse := course[1:2]
fmt.Printf("%T", subCourse)

3. 切片初始化

s :=[] int {1,2,3 } 
直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.cap=len=3
s := arr[:] 

创建 slice 的方式有以下几种:

序号方式代码示例
1直接声明var slice []int
2newslice := *new([]int)
3字面量slice := []int{1,2,3,4,5}
4makeslice := make([]int, 5, 10)
5从切片或数组“截取”slice := array[1:5]slice := sourceSlice[1:5]
  1. 用法
  2. 底层存储

blog.csdn.net/kikajack/ar…

  1. go和python的切片区别

blog.csdn.net/qq_15437667…

  1. slice扩容机制

append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ... 传入 slice,直接追加一个切片。

append函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。

使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。

这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。

当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍 原 slice 容量超过 1024,新 slice 容量大家可以通过源码了解, 文章推荐:juejin.cn/post/684490…

7.4. 切片原理

7.5. go语言的map

map创建

要求所有的key的数据类型相同,所有value数据类型相同(注:key与value可以有不同的数据类型)

// 1 字面值
{
    m1 := map[string]string{
        "m1": "v1", // 定义时指定的初始key/value, 后面可以继续添加
    }
    _ = m1

}

// 2 使用make函数
{
    m2 := make(map[string]string) // 创建时,里面不含元素,元素都需要后续添加
    m2["m2"] = "v2"               // 添加元素
    _ = m2

}

// 定义一个空的map
{
    m3 := map[string]string{}
    m4 := make(map[string]string)
    _ = m3
    _ = m4
}

map中key的类型

map中的每个key在keys的集合中是唯一的,而且需要支持 == or != 操作

key的常用类型:int, rune, string, 结构体(每个元素需要支持 == or != 操作), 指针, 基于这些类型自定义的类型

// m0 可以, key类型为string, 支持 == 比较操作
{
    var m0 map[string]string // 定义map类型变量m0,key的类型为string,value的类型string
    fmt.Println(m0)
}

// m1 不可以, []byte是slice,不支持 == != 操作,不可以作为map key的数据类型
{
    //var m1 map[[]byte]string // 报错: invalid map key type []byte
    //fmt.Println(m1)

    // 准确说slice类型只能与nil比较,其他的都不可以,可以通过如下测试:
    // var b1,b2 []byte
    // fmt.Println(b1==b2) // 报错: invalid operation: b1 == b2 (slice can only be compared to nil)
}

// m2 可以, interface{}类型可以作为key,但是需要加入的key的类型是可以比较的
{
    var m2 map[interface{}]string
    m2 = make(map[interface{}]string)
    //m2[[]byte("k2")]="v2" // panic: runtime error: hash of unhashable type []uint8
    m2[123] = "123"
    m2[12.3] = "123"
    fmt.Println(m2)
}

// m3 可以, 数组支持比较
{
    a3 := [3]int{1, 2, 3}
    var m3 map[[3]int]string
    m3 = make(map[[3]int]string)
    m3[a3] = "m3"
    fmt.Println(m3)
}

// m4 可以,book1里面的元素都是支持== !=
{
    type book1 struct {
        name string
    }
    var m4 map[book1]string
    fmt.Println(m4)
}

// m5 不可以, text元素类型为[]byte, 不满足key的要求
{
    // type book2 struct {
    //  name string
    //  text []byte //没有这个就可以
    // }
    //var m5 map[book2]string //invalid map key type book2
    //fmt.Println(m5)
}

map的增删改

Xnip2021-03-31_10-37-14.png

// 创建
m := map[string]string{
    "a": "va",
    "b": "vb",
}
fmt.Println(len(m)) // len(m) 获得m中key/value对的个数

// 增加,修改
{
    // k不存在为增加,k存在为修改
    m["c"] = ""
    m["c"] = "11"                      // 重复增加(key相同),使用新的值覆盖
    fmt.Printf("%#v %#v\n", m, len(m)) // map[string]string{"a":"va", "b":"vb", "c":"11"} 3
}

// 查
{
    // v := m[k] // 从m中取键k对应的值给v,如果k在m中不存在,则将value类型的零值赋值给v
    // v, ok := m[k] // 从m中取键k对应的值给v,如果k存在,ok=true,如果k不存在,将value类型的零值赋值给v同时ok=false
    // 查1 - 元素不存在
    v1 := m["x"] //
    v2, ok2 := m["x"]
    fmt.Printf("%#v %#v %#v\n", v1, v2, ok2) // "" "" false

    // 查2 - 元素存在
    v3 := m["a"]
    v4, ok4 := m["a"]
    fmt.Printf("%#v %#v %#v\n", v3, v4, ok4) //"va" "va" true
}

// 删, 使用内置函数删除k/v对
{
    // delete(m, k) 将k以及k对应的v从m中删掉;如果k不在m中,不执行任何操作
    delete(m, "x")                     // 删除不存在的key,原m不影响
    delete(m, "a")                     // 删除存在的key
    fmt.Printf("%#v %#v\n", m, len(m)) // map[string]string{"b":"vb", "c":"11"} 2
    delete(m, "a")                     // 重复删除不报错,m无影响
    fmt.Printf("%#v %#v\n", m, len(m)) /// map[string]string{"b":"vb", "c":"11"} 2
}

map的遍历

  • 遍历的顺序是随机的
  • 使用for range遍历的时候,k,v使用的同一块内存,这也是容易出现错误的地方
m := map[string]int{
        "a": 1,
        "b": 2,
    }
    for k, v := range m {
        fmt.Printf("k:[%v].v:[%v]\n", k, v) // 输出k,v值
    }
map遍历易错点举例

由于遍历的时候,遍历v使用的同一块地址,同时这块地址是临时分配的。虽然v的地址没有变化,但v的内容在一直变化,当遍历完成后,v的内容是map遍历时最后遍历的元素的值(map遍历无序,每次不确定哪个元素是最后一个元素)。当程序将v的地址放入到slice中的时候,slice在不断地v的地址插入,由于v一直是那块地址,因此slice中的每个元素记录的都是v的地址。因此当打印slice中的内容的时候,都是同一个值

m := map[string]int{
    "a": 1,
    "b": 2,
}
var bs []*int
for k, v := range m {
    fmt.Printf("k:[%p].v:[%p]\n", &k, &v) // 这里的输出可以看到,k一直使用同一块内存,v也是这个状况
    bs = append(bs, &v) // 对v取了地址
}

// 输出
for _, b := range bs {
    fmt.Println(*b) // 输出都是1或者都是2
}