Golang 学习之路七:数据(Array、Slice、Map、Struct)

来源:互联网 发布:程序员 量化交易 入门 编辑:程序博客网 时间:2024/06/03 19:28

Golang 学习:数据

一、Array

  Array 就是数组。它的定义方式:

var arr [n]type
  • 在[n]type中,n表示数组长度,type表示存储元素的类型。对数组的操作和其它语言类似,都是通过[]来进行读取或赋值。
      但Go 中的Array较其它语言有一些差异。
    • 数组是值类型,赋值和传参会复制整个数组,而不是指针。
    • 数组长度必须是常量,且是类型的组成部分。[2]int 和 [3]int 是不同的类型。
    • 支持 “==”、“!=”操作符,因为内存总是被初始化过的。
    • 指针数组[n]T,数组指针 [n]T.

    可以用复合语句初始化。

a := [3]int{1, 2} // 未初始化元素值为 0。b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。c := [5]int{2: 100, 4:200} // 使用索引号初始化元素。d := [...]struct {    name string    age uint8}{    {"user1", 10}, // 可省略元素类型。    {"user2", 20}, // 别忘了最后一行的逗号。}
  • 支持多维数组
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
  • 值拷贝行为会造成性能问题,通常建议使用slice或者数组指针。
func test(x [2]int) {    fmt.Printf("x: %p\n", &x)    x[1] = 1000}func main() {    a := [2]int{}    fmt.Printf("a: %p\n", &a)    test(a)    fmt.Println(a)}

输出结果:

a: 0x114c20e0x: 0x114c2120[0 0]  ///值拷贝
  • 内置函数 len 和 cap 都会返回数组长度(元素数量)。
a := [2]int{}println(len(a), cap(a)) // 2, 2

二、slice

  slice 有类似动态数组的功能。注意slice和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用…自动计算长度,而声明slice时,方括号内没有任何字符。

var fslice []int

  但是slice并不是数组或者数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

///runtime.hstruct Slice{ // must not move anything    byte* array; // actual data    uintgo len; // number of elements    uintgo cap; // allocated number of elements};
  • 引用类型。但自身是结构体,值拷贝传递。
  • 属性len表示可用元素数量,读写操作不能超过该限制。
  • 属性cap表示最大扩张容量,不能超出数组限制。
  • 如果slice==nil,那么len、cap结果都是等于0。
  • slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i。
data := [...]int{0, 1, 2, 3, 4, 5, 6}slice := data[1:4:5] // [low : high : max]

结构如下:

         +- low high-+   +- max                         len = high - low         |           |   |                              cap = max  - low     +---+---+---+---+---+---+---+          +---------+---------+---------+data | 0 | 1 | 2 | 3 | 4 | 5 | 6 |    slice | pointer | len = 3 | cap = 4 |     +---+---+---+---+---+---+---+          +---------+---------+---------+         |<-- len -->|   |                      |         |               |                      |         |<---- cap ---->|                      |         |                                      |         +---<<<- slice.array pointer ---<<<----+
  • 创建表达式使用的是元素索引号,而非数量。
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}expression   slice                  len    cap     comment------------+----------------------+------+-------+---------------------data[:6:8]   [0 1 2 3 4 5]          6      8       省略 low.data[5:]     [5 6 7 8 9]            5      5       省略 high、max。data[:3]     [0 1 2]                3      10      省略 low、max。data[:]      [0 1 2 3 4 5 6 7 8 9]  10     10      全部省略。
  • 读写操作实际目标是底层数组,只需注意索引号的差别。
func main() {    data := [...]int{0, 1, 2, 3, 4, 5}    s := data[2:4]    s[0] += 100    s[1] += 200    fmt.Println(s)    fmt.Println(data)}

输出结果:

[102 203][0 1 102 203 4 5]
  • 可以直接创建slice对象,自动分配底层数组。
func main() {    s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。    fmt.Println(s1, len(s1), cap(s1))    s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。    fmt.Println(s2, len(s2), cap(s2))    s3 := make([]int, 6) // 省略 cap,相当于 cap = len。    fmt.Println(s3, len(s3), cap(s3))}

输出结果:

[0 1 2 3 0 0 0 0 100] 9 9[0 0 0 0 0 0] 6 8[0 0 0 0 0 0] 6 6
  • 使用make动态创建slice,避免数组必须必须用常量做长度的麻烦。还可以用指针直接访问底层数组,退化成普通数组操作。
s := []int{0, 1, 2, 3}p := &s[2] // *int, 获取底层数组元素指针。*p += 100fmt.Println(s)

输出结果:

[0 1 102 3]
  • [][]T是指元素类型为[]T
data := [][]int{    []int{1, 2, 3},    []int{100, 200},    []int{11, 22, 33, 44},}
  • 可以直接修改 struct array/slice 成员
func main() {    d := [5]struct {        x int    }{}    s := d[:]    d[1].x = 10    s[2].x = 20    fmt.Println(d)    fmt.Printf("%p, %p, %p\n", &s, &d, &d[0])}

输出结果:

[{0} {10} {20} {0} {0}]0x116820e0, 0x1167f800, 0x1167f800

上面结果解释:
三个点:引用 、自身是结构体地址变化 、值拷贝传递

1. reslice

  reslice 是基于已有的 slice创建新 slice对象,以便在cap允许范围内调整属性。

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}s1 := s[2:5] // [2 3 4]s2 := s1[2:6:7] // [4 5 6 7]s3 := s2[3:6] // Error

结构如下:

 +---+---+---+---+---+---+---+---+---+---+data | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |     +---+---+---+---+---+---+---+---+---+---+      0       2           5              +---+---+---+---+---+---+---+---+s1            | 2 | 3 | 4 |   |   |   |   |   | len = 3, cap = 8              +---+---+---+---+---+---+---+---+               0       2               6   7                      +---+---+---+---+---+s2                    | 4 | 5 | 6 | 7 |   | len = 4, cap = 5                      +---+---+---+---+---+                       0           3   4   5                                  +---+---+---+s3                                | 7 | 8 | X | error: slice bounds out of range                                  +---+---+---+
  • 新旧对象依旧指向原底层数组。
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}s1 := s[2:5] // [2 3 4]s1[2] = 100s2 := s1[2:6] // [100 5 6 7]s2[3] = 200fmt.Println(s)

输出结果:

[0 1 2 3 100 5 6 200 8 9]

2. append

  • 向 slice 尾部添加数据,返回新的 slice 对象。
func main() {    s := make([]int, 0, 5)    fmt.Printf("%p\n", &s)    s2 := append(s, 1)    fmt.Printf("%p\n", &s2)    fmt.Println(s, s2)}

输出结果:

0x114821700x114821b0[] [1]
  • 简单点说,就是在array[slice.high]写数据。
func main() {    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}    s := data[:3]    s2 := append(s, 100, 200)    fmt.Println(data)    fmt.Println(s)    fmt.Println(s2)}

输出结果:

[0 1 2 100 200 5 6 7 8 9][0 1 2][0 1 2 100 200]
  • 一旦超出原slice.cap的限制,就会重新分配底层数组,即便原数组并未填满。
func main() {    data := [...]int{0, 1, 2, 3, 4, 10: 0}    s := data[:2:3]    fmt.Println(&s[0], &data[0])    s = append(s, 100, 200)      // 一次 append 两个值,超出 s.cap 限制。    fmt.Println(s, data)         // 重新分配底层数组,与原数组无关。    fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。}

输出结果:

0x11488330 0x11488330[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]0x1148bb40 0x11488330

输出结果分析:

append 后的 s 重新分配了底层数组,并复制了数据。
- 通常以 2 倍的容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的len属性,改用索引号进行操作。及时释放不再使用的slice对象,避免持有过期数组,造成GC无法回收。

func main() {    s := make([]int, 0, 1)    c := cap(s)    for i := 0; i < 50; i++ {        s = append(s, i)        if n := cap(s); n > c {            fmt.Printf("cap: %d -> %d\n", c, n)            c = n        }    }}

输出结果:

cap: 1 -> 2cap: 2 -> 4cap: 4 -> 8cap: 8 -> 16cap: 16 -> 32cap: 32 -> 64

3. copy

  函数 copy 在两个slice间赋值数据,复制长度以len小的为准。两个slice可指向同一底层数组,允许元素区间重叠。

func main() {    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}    s := data[8:]    s2 := data[:5]    fmt.Println(s, s2)    copy(s2, s) // dst:s2, src:s    fmt.Println(s2)    fmt.Println(data)}

输出结果:

[8 9] [0 1 2 3 4][8 9 2 3 4][8 9 2 3 4 5 6 7 8 9]

应该及时将所需的数据 copy 到较小的slice,以便释放较大的底层数组的内存

三、Map

  map 就是 python 中的字典的概念相同。在Go中Map是引用类型,哈希表。键必须是支持相等运算符(==,!=)类型,比如:number、string、pointer、array、struct,以及对应的interface。值可以是任意类型,没有限制。

func main() {    m := map[int]struct {        name string        age  int    }{        1: {"user1", 10}, // 可省略元素类型。        2: {"user2", 20},    }    println(m[1].name)}

输出结果:

user1
  • 预先给make函数一个合理的元素数量参数,有助于提升性能。可以避免以后的扩张操作。
m := make(map[string]int, 1000)

常见的操作:

func main() {    m := map[string]int{        "a": 1,    }    if v, ok := m["a"]; ok { // 判断 key 是否存在。        println(v, ok)    }    println(m["c"])       // 对于不存在的 key,直接返回 \0,不会出错。    m["b"] = 2            // 新增或修改。    delete(m, "c")        // 删除。如果 key 不存在,不会出错。    println(len(m))       // 获取键值对数量。cap 无效。    for k, v := range m { // 迭代,可仅返回 key。随机顺序返回,每次都不相同。        println(k, v)    }}

输出结果:

1 true02a 1b 2

不能保证迭代顺序,这些适合 python 特性相同
- 从map中取回的是一个value临时复制品,对其成员修改是没有任何意义的。

func main() {    type user struct{ name string }    m := map[int]user{ // 当 map 因扩张而重新哈希时,各键值项存储位置都会发生改变。 因此,map        1: {"user1"}, // 被设计成 not addressable。 类似 m[1].name 这种期望透过原 value    } // 指针修改成员的行为自然会被禁止。    m[1].name = "user2"    println(m[1].name)}

运行结果:

cannot assign to struct field m[1].name in map
  • 可以完整替换value或使用指针达到目的。
// 完整代替 valuefunc main() {    type user struct{ name string }    m := map[int]user{        1: {"user1"},    }    tmp := m[1]    tmp.name = "user2"    m[1] = tmp    println(m[1].name)}
// 使用指针func main() {    type user struct{ name string }    m := map[int]*user{        1: &user{"user1"},    }    m[1].name = "user2" // 返回的是指针复制品。透过指针修改原对象是允许的。    println(m[1].name)}
  • 允许在迭代期间安全删除键值。但是如果有新增操作,意外不可知预料。
func main() {    for i := 0; i < 5; i++ {        m := map[int]string{            0: "a", 1: "a", 2: "a", 3: "a", 4: "a",            5: "a", 6: "a", 7: "a", 8: "a", 9: "a",        }        for k := range m {            m[k+k] = "x"            delete(m, k)        }        fmt.Println(m)    }}

输出结果:

map[14:x 8:x 18:x 32:x 12:x 20:x]map[10:x 2:x 14:x 16:x 18:x 24:x 8:x]map[12:x 14:x 10:x 18:x 32:x 4:x]map[4:x 6:x 12:x 20:x 16:x 28:x 18:x]map[8:x 12:x 18:x 10:x 14:x 16:x 2:x]

四、Struct

  Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。struct是值类型,赋值和传参会复制全部内容.可以用”_”定义补位字段,支持指向自身的类型的指针成员。

type Node struct {    _    int    id   int    data *byte    next *Node}func main() {    n1 := Node{        id:   1,        data: nil,    }    n2 := Node{        id:   2,        data: nil,        next: &n1,    }}
  • 顺序初始化必须包含全部字段,否则会报错。
type User struct {    name string    age int}u1 := User{"Tom", 20}u2 := User{"Tom"} // Error: too few values in struct initializer
  • 支持匿名结构,可用作结构成员或定义变量。
type File struct {    name string    size int    attr struct {        perm int        owner int    }}f := File{    name: "test.txt",    size: 1025,    // attr: {0755, 1}, // Error: missing type in composite literal}f.attr.owner = 1f.attr.perm = 0755var attr = struct {    perm int    owner int}{2, 0755}f.attr = attr
  • 支持”==”、”!=”相等操作符,可用作map键类型。
func main() {    type User struct {        id   int        name string    }    m := map[User]int{        User{1, "Tom"}: 100,    }    println(m[User{1,"Tom"}]) // 输出 100}
  • 可以定义字段标签,用反射读取。标签是类型的组成部分
var u1 struct { name string "username" } //"username" 标签var u2 struct { name string }u2 = u1 // Error: cannot use u1 (type struct { name string "username" }) as        //        type struct { name string } in assignment
  • 空结构”节省“内存:实现 set 数据结构,或者实现没有状态只有方法的“静态类”。
var null struct{}set := make(map[string]struct{})set["a"] = null

1. 匿名字段

  • 匿名字段不过是一种语法糖,从根本上说,就是一个与成员类型同名(不包含包名)的字段。被匿名嵌入的可以是任意类型,也可以是指针。
type User struct {    name string}type Manager struct {    User    title string}m := Manager{    User: User{"Tom"}, // 匿名字段的显式字段名,和类型名相同。    title: "Administrator",}
  • 可以像普通字段那样访问匿名字段成员,编译器从外向内逐级查找所有的匿名字段,直到发现目标或出错。
ype Resource struct {    id int}type User struct {    Resource    name string}type Manager struct {    User    title string}var m Managerm.id = 1m.name = "Jack"m.title = "Administrator"
  • 外层同名字段会遮蔽嵌入字段成员,相同层次的同名字段也会让编译器⽆所适从。解决⽅法是使用显式字段名。
type Resource struct {    id int    name string}type Classify struct {    id int}type User struct {    Resource // Resource.id 与 Classify.id 处于同⼀一层次。    Classify    name string // 遮蔽 Resource.name。}u := User{    Resource{1, "people"},    Classify{100},    "Jack",}println(u.name) // User.name: Jackprintln(u.Resource.name) // people// println(u.id) // Error: ambiguous selector u.idprintln(u.Classify.id) // 100
  • 不能同时嵌入某一类型和其指针类型,因为它们名字相同。
type Resource struct {    id int}type User struct {    *Resource    // Resource // Error: duplicate field Resource    name string}u := User{    &Resource{1},    "Administrator",}println(u.id)println(u.Resource.id)

2. 面向对象

  • 面向对象三大特征里,Go 仅支封装,尽管匿名字段的内存布局和行为类似继承。没有class 关键字,没有继承、多态等等。
type User struct {    id int    name string}type Manager struct {    User    title string}m := Manager{User{1, "Tom"}, "Administrator"}// var u User = m // Error: cannot use m (type Manager) as type User in assignment// 没有继承,自然也不会有多态。var u User = m.User // 同类型拷贝。
  • 内存布局和 C struct 相同,没有任何附加的 object 信息。
    |<-------- User:24 ------->|<-- title:16 -->|    +--------+-----------+------------+         +---------------+m   |    1   |   string  |   string   |         | Administrator |   [n]byte    +--------+-----------+------------+         +---------------+                  |             |                       |                  | +--->>>------------------>>>--------+                  |                  +--->>>-------------------->>>-----+                                                     |                  +--->>>-------------------->>>-+   |                  |                              |      |    +--------+-----------+                      +---------+u   |   1    |   string  |                      |   Tom   | [n]byte    +--------+-----------+                      +---------+    |<-id:8->|<-name:16->|
  • 可用 unsafe 包相关函数输出内存地址信息。
m : 0x2102271b0, size: 40, align: 8m.id : 0x2102271b0, offset: 0m.name : 0x2102271b8, offset: 8m.title: 0x2102271c8, offset: 24

五、总结

  本部分主要介绍了 Go 数据:array、slice、map、struct。从介绍中,基本可以看到Go与C语言的一些相同之处,也可以看到它从现代语言中提取的优点。学习、记录、灵活使用!

六、参考资料

  1. Go 学习笔记(雨痕)
  2. Go Web 编程
0 0
原创粉丝点击