复合数据类型

复合数据类型

复合数据类型是由基本数据类型以各种方式组合而构成的. 数组和结构体都是聚合类型, 它们的值由内存中的一组变量构成. 数组的元素具有相同的类型, 而结构体中元素类型则可以不同. 数组和结构体的长度都是固定的, 反之, slicemap都是动态数据结构, 它们的长度都是元素添加到结构体中时可以动态增长.

数组

数组是具有固定长度且拥有零个或多个相同数据类型元素的序列. 由于数组的长度固定, 所以在Go里面直接很少使用. slice的长度可以增长和缩短, 在很多场合下使用得更多.

Go内置的函数len可以返回数组中元素元素的个数.

默认情况下, 一个新数组中的元素初始值为元素类型的零值, 对于数字来说, 就是0. 也可以使用数组字面量根据一组值来初始化一个数组. 在数组字面中, 如果使用...代替数组长度, 那么数组的长度由初始化数组的个数决定.

数组长度是数组类型的一部分, 所以[3]int[4]int是两种不同的数组类型. 数组的长度必须是常量表达式, 也就是说, 这个表达式的值在程序编译时就可以确定.

数组可以通过索引赋值, 没有指定值的索引位置的元素默认被赋予数组元素类型的零值.

如果一个数组的元素类型是可比较的, 那么这个数组也是可比较的, 这样我们就可以直接使用==操作符来比较两个数组, 比较的结果是两边元素的值是否完全相同. 只有长度相同的数组可以比较, 如果长度不同, 会认为是两个不同类型的数组.

Go语言中, 把数组和其他的类型都看成值传递, 对参数的任何修改都发生在复制的数组上. 而在其语言中, 数组时隐式地使用了引用或指针传递.

slice

slice表示一个拥有相同类型元素的可变长度的序列. slice通常写成[]T, 其中元素的类型都是T; 它看上去像没有长度的数组类型.

一个slice是一个轻量级的数据结构, 提供了访问数组子序列元素的功能, 而且slice的底层确实引用了一个数组对象. 一个slice有三个属性: 指针, 长度和容量. 指针指向数组的第一个可以从slice中访问的元素, 这个元素并不一定是数组的第一个元素. 长度是指slice中元素的个数, 它不能超过slice的容量. 容量的大小通常是从slice的起始元素到底层数组的最后一个元素间元素的个数. Go的内置函数lencap用来返回slice的长度和容量.

一个底层数组可以对应多个slice, 这些slice可以引用数组的任何位置, 彼此之间的元素还可以重叠.

slice操作符s[i:j](其中$0 \le i \le j \le cap(s)$) 创建了一个新的slice, 这个新的slice引用了序列s中ij-1索引位置的所有元素, 这里的s既可以是数组或是指向数组的指针, 也可以是slice.

如果切片操作超出了cap(s)的上限将导致一个panic异常, 但是超出了len(s)则是意味着扩展了slice, 因为新的slice的长度会变大.

因为slice包含了指向数组元素的指针, 所以将一个slice传递给函数的时候, 可以在函数内部修改底层数组的元素.

slice字面量看上去和数组字面量很像, 都是用逗号分隔并用花括号扩起来的一个元素序列, 但是slice没有指定长度. 这种隐式区别的结果分别是创建有固定长度的数组和创建指向数组的slice. 和数组一样, slice也按照顺序指定元素, 也可以通过索引了指定元素, 或者两种结合.

和数组不同的是, slice无法做比较, 因此不能用==来测试两个slice是否拥有相同的元素. 标准库里面提供了高度优化的函数bytes.Equal来比较两个字节slice. 但是对于其他类型的slice, 我们必须自己写函数来比较.

slice唯一允许的比较操作是和nil做比较. slice类型的零值是nil. 值是nilslice没有对应的底层数组. 值为nikslice长度和容量都是零, 但是也有非nilslice长度和容量都是零, 例如[]int{}make([]int,3)[3:]. 对于任何类型, 如果它们的值可以nil, 那么这个类型的值可以使用一种转换表达式, 例如[]int(nil).

所以, 如果想检查一个slice是否为空, 那么使用len(s) == 0, 而不是s == nil, 因为s != nil情况下, slice也可能是空.

内置函数make可以创建一个具有指定元素类型, 长度和容量的slice. 其中容量参数可以省略, 在这种情况下, slice的长度和容量相等.

1
2
make([]T, len)
make([]T, len, cap) // 和 make([]T, cap)[:len]功能相同

深度研究一下, 其实make创建了一个无名数组并返回了它的一个slice; 这个数组仅可以通过slice来访问. 在上面的第一行代码中, 所返回的slice引用了整个数组. 在第二行代码中, slice只引用了数组的前len个元素, 但是它的容量和数组的长度, 这为未来的slice元素留出空间.

append函数

内置函数append用来将元素追加到slice的后面.

更新slice变量不仅对调用append函数是必要的, 实际上对应任何可能导致长度, 容量或底层数组变化的操作都是必要的.

函数参数声明中的省略号...表示该函数可以接受可变参数列表, 实参后面的省略号表示将一个slice转换为参数列表.

slice内存技巧

输入的slice和输出的slice共享一个底层数组. 这可以避免分配另一个数组, 不过原来的数据可能会被覆盖.

map

Go语言中, map是散列表的引用, map的类型是map[K]V, 其中KV是字典的键和值对应的数据类型. 键的类型K, 必须可以通过操作符==来进行比较的数据类型, 所以map可以检测某一个键是否已经存在. 虽然浮点型是可以比较的, 但是比较浮点型的相等性不是一个好主意.

内置函数make可以用来创建一个map:

1
args := make(map[string]int) // 创建一个从string到int的map

也可以使用map的字面量来创建一个带初始键值对元素的字典:

1
2
3
4
5
6
7
8
9
args := map[string]int{
"alice": 31,
"charlie": 34,
}

等价于
args := make(map[string]int) // 等价于map[string]int{}
args["alice"] = 31
args["charlie"] = 34

可以使用内置函数delete来从字典中根据键移除一个元素:

1
delete(args, "alice")

即使键不在map中, 上面的操作也都是安全的. map使用给定的键来查找元素, 如果对应的元素不存在, 就返回值类型的零值.

快捷赋值方式(如x+=y和x++)对map中元素同样适用.

我们无法获得map元素的地址的一个原因是map的增长可能会导致已有元素元素被重新散列到新的存储位置, 这样就可能使得获得的地址无效.

可以使用for循环(结合range关键字)来便历map中所有的键和对应的值.

如果需要按照某种顺序来遍历map中的元素, 我们必须显式地来给键排序, 然后再取值. 如果一开始就知道了元素的个数, 直接指定一个slice的长度会更加高效.

map类型的零值是nil, 大多数的map操作都可以安全地在map的零值nil上执行, 包括查找元素, 删除元素, 获取map元素的个数(len), 执行range循环, 应为这和空map的行为一致. 但是向零值map中设置元素会导致错误.

判断一个元素知否在map中, 还是值是零值.

1
if age, ok := args["bob"]; !ok { /* ... */}

slice一样, map不可比较, 唯一合法的比较就是和nil做比较. 为了判断两个map是否拥有相同的键和值, 必须写一个循环.

Go没有提供集合类型, 但既然map的键都是唯一的, 就可以用map来实现这个功能.

map的值类型本身可以是复合数据类型, 例如mapslice.

结构体

结构体是将零个或多个任意类型的命名变量组合在一起的聚合数据类型. 每个变量都叫做结构体的成员.

结构体可以通过点号的方式来访问成员. 点号同样可以用在结构体指针上.

成员变量的顺序对于结构体同一性很重要. 顺序不一样, 结构体就不一样.

命名结构体类型S不可以定义一个拥有相同结构体类型S的成员变量, 也就是一个聚合类型不可以包含它自己(同样的限制对数组也是适用). 但是S中可以定义一个S的指针类型, 即*S, 这样我们就可以创建一些递归数据结构, 比如链表和树.

结构体的零值由结构体成员的零值组成. 通常情况下, 我们希望零值是一个默认自然的, 合理的值.

任何没有成员变量的结构体称为空结构体, 写作struct{}. 它没有长度, 也不携带任何信息, 但是有时候会很有用.

结构体字面量

结构体类型的值可以通过结构体字面量来设置, 即通过设计结构体的成员变量来设置.

1
2
type Point struce{ X, Y int}
p := Point{1, 2}

有两种格式的结构体字面量. 第一中格式如上, 它要求按照正确的顺序, 为每个成员变量指定一个值, 这种格式一般只在定义结构体的包内部使用, 或者是在较小的结构体中使用, 这些结构体的成员排列比较规则. 第二种形式, 通过指定部分或全部成员变量的名称和值来初始化结构体变量. 如果在这种初始化方式中某个成员变量没有指定, 那么它的值就是该成员变量类型的零值. 因为了指定了成员变量的名字, 所以它们的顺序是无所谓的.

这两种初始化方式不可以混合使用, 另外也无法使用第一种初始化方式来绕过不可导出的变量无法在其他包中使用的规则.

如果考虑效率的haunted, 较大的结构体通常会使用指针的方式传入和返回.

如果要在函数内部修改结构体成员的话, 用指针传入是必须的; 因为在Go语言中, 所有的函数参数都是值拷贝传入的, 函数参数将不再是函数调用时的原始变量.

结构体比较

如果结构体的所有成员变量都是可以比较的, 那么这个结构体就是可比较的. 两个结构体的比较可以使用==或者!=. 其中==操作按照顺序比较两个结构体变量的成员变量.

结构体嵌套和匿名成员

Go允许我们定义不带名称的结构体成员, 只需要指定类型即可; 这种结构体成员称做匿名成员. 这个结构体成员的类型必须是一个命名类型或指向命名类型的指针. 正因为有了这种结构体嵌套的功能, 我们才能直接访问到我们需要的变量而不是指定一大串中间变量.

匿名成员的名称其实是对应类型的名字, 只是这些名字在点号访问时时可选的. 结构体字面值并没有简短表示匿名成员的语法.

因为”匿名成员”拥有隐式的名字, 所以你不能在一个结构体里面定义两个相同类型的匿名成员, 否则会引起冲突. 由于匿名成员的名字是由它们的类型决定的, 因此它们的可导出性也是由它们的类型决定的.

以快捷方式访问匿名成员的内部变量同样适用于访问匿名成员的内部方法. 因此, 外围的结构体类型获取的不仅是匿名成员的内部变量, 还有相关的方法.

JSON

javaScript对象表示法(JSON)是一种发送和接收格式化信息的标准. JSONJavaScript值的Unicode编码, 这些值包括字符串, 数字, 布尔值, 数组和对象. JSON里面的\Uhhhh转义数字来表示一个UTF-16编码(译注: UTF-16UTF-8一样是一种变长的编码, 但是有大端和小端之分).

文本和HTML模版

通常情况下, Printf函数就够用了, 但是有的情况下格式化会很复杂, 并且要求格式和代码彻底分离. 这个可以通过text/template包和/html/template包里面的方法来实现, 这两个包提供了一种机制, 可以将程序变量的值植入到文本或者是HTML模版中.

常用函数

1
2
3
4
5
6
7
8
9
10
11
// 解码UTF-8编码, 并返回三个值, 解码的字符, UTF-8编码中字节的长度和错误值. 这里唯一可能出现的错误是文件结束(EOF), 如果输入的不是合法的UTF-8字符, 那么返回的字符是unicode.ReplacementChar并且长度是1
in.ReadRune()

// 副词#使得%v用和`Go`语言类似的语法打印. 对于结构体类型来说, 将包含每个成员的名字
fmt.Printf("%#v", w)

# marshal编组, 将go对象编组为JSON对象, 这个是紧凑形式, 格式化方法, json.MarshlIndent(movies, "", " "), 只有可以导出的成员可以转换为JSON字段
json.marshal(movies)

# unmarshal解码, 将JSON转换为go对象. 通过合理地定义go的数据结构, 我们可以选择将哪部分JSON数据解码到结构体中, 哪些数据可以丢弃, 解码忽略大小写
json.Unmarsh

技巧

将一个slice左移n个元素的简单方法是连续调用reverse函数三次. 第一次反转前n个元素, 第二次反转剩下的元素, 最后一次对整个slice再做一次反转(如果将元素右移n个元素, 那么先做第三次调用)

-------------本文结束感谢您的阅读-------------