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

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: 0x114c20e0
x: 0x114c2120
[0 0]  ///值拷贝
  • 内置函数 len 和 cap 都会返回数组长度(元素数量)。
a := [2]int{}
println(len(a), cap(a)) // 2, 2

二、slice

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

var fslice []int

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

///runtime.h
struct 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 += 100
fmt.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] = 100
s2 := s1[2:6] // [100 5 6 7]
s2[3] = 200
fmt.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)
}

输出结果:

0x11482170
0x114821b0
[] [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 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 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 true
0
2
a 1
b 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或使用指针达到目的。
// 完整代替 value
func 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 = 1
f.attr.perm = 0755
var 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 Manager
m.id = 1
m.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: Jack
println(u.Resource.name) // people
// println(u.id) // Error: ambiguous selector u.id
println(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: 8
m.id : 0x2102271b0, offset: 0
m.name : 0x2102271b8, offset: 8
m.title: 0x2102271c8, offset: 24

五、总结

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

六、参考资料

  1. Go 学习笔记(雨痕)
  2. Go Web 编程
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值