Go语言圣经 .3.复合数据类型

复合数据类型

数组和结 构体都是有固定内存大小的数据结构。相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。

数组

数组是一个由固定长度的特定元素组成的序列

var a [3]int  //默认情况下,这个数组每个元素会被初始化为零值

var q [3]int = [3]int{1, 2, 3} //也可以使用这样的数组字面值语法初始化

在数组字面值中,如果数组的长度是省略号"...", 则表示数组的长度是根据初始化的值的个数来计算的。

q := [...]int{1,2,3}

数组的长度是数组类型的一个组成部分,所以 [3]int 和 [4]int 是两种不同的数组类型

也可以直接定义索引

r := [...]int{99: -1}  // 顶一个一个含有100个元素的数组r, 最后一个元素是-1,其他都是用0初始化

Slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型, 一般一个Slice写作 []T

一个slice由三个部分构成:指针、长度和容量

  • 指针指向数组的第一个可以从slice中访问的元素,这个元素不一定是数组的第一个元素
  • 长度是指slice中的元素個數,它不能超过slice的容量
  • 容量的大小通常是从slice的起始元素底层数组最后一个元素间元素的个数。

内置函数lencap用来返回slice的长度和容量。

import "fmt"

month := []string{1:"January", 2:"February", 3:"March", 4:"Apil", 5:"May", 6: "June", 7:"July", 8:"August", 9:"September", 10:"October", 11:"november", 12:"December"}

summer := month[6:9]
Q2 := month[4:7]

fmt.Printf("%v cap: %d len: %d \n", summer, cap(summer), len(summer))
// [June July August] cap: 7 len: 3

fmt.Printf("%v cap: %d len: %d \n", Q2, cap(Q2), len(Q2))
// [Apil May June] cap: 9 len: 3

fmt.Println(summer[:5]) // [June July August September October]

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

Slice无法使用==比较,Slice唯一允许的比较操作是和nil比较,例如:

if summer == nil { /* ... */ }

slice类型的零值是nil

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

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

append函数

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

func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
// slice 仍有增长空间,扩展slice内容
z = x[:zlen]
} else {
// slice已无空间,为它分配一个新的底层数组
// 为了达到分摊线性复杂性,容量扩展一倍
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
}
z[len(x)] = y
return z
}

slice就地修改

在原有内存空间修改slice

比如:

package main

import "fmt"

func NonEmpty(strings []string) []string {
i := 0
for _, s := range strings {
if s != "" {
strings[i] = s
i++
}
}
return strings[:i]
}

data := []string{"one", "", "three"}
fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
fmt.Printf("%q\n", data) // `["one" "three" "three"]`

array 和 slice 的区别

  • array固定长度,slice可变

  • array类型是聚合类型,slice是引用类型

    var a = [3]int{1,2,3}
    var b = []int{}
    fmt.Println(reflect.TypeOf(a)) // [3]int
    fmt.Println(reflect.Typeof(b)) // []int
    fmt.Println(reflect.Typeof(a[1:])) // []int
  • 声明:

    • array:var a = [3]int
    • slice: var a = []int{}

Map

一个map就是一个哈希表的引用,map的类型可以写成map[K]V, map中所有的key都有相同的类型,所有的value也都有相同的类型,但是key和value不一定是相同的类型。

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

ages := make(map[string]int)

也可以使用map字面值的语法创建map:

ages := map[string]int{
"alice": 21,
"micel": 34
}

empty_map := map[string]int{}

map的很多操作都是安全的,即使这些值不在map中也没有关系,如果一个查找失败返回的是对应值类型的零值。

ages := map[string]int{}
ages["you"] = 18
delete(ages, "you")

ages["bob"] = ages["bob"] + 1 // "bob": 1
ages["bob"] += 1
ages["bob"]++

// 有时候需要知道key是否真的存在于map中的话
age, ok := ages["bob"]
if !ok {/* ... */}
// 或者
if age, ok := ages["bob"]; !ok { /* ... */ }

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:

_ := $ages["bob"]  // 错误!

遍历map,遍历的时候顺序是随机的:

for k, v := range ages {
...
}

map类型的零值是nil, map的大部分操作都可以安全的在nil值的map上执行,但是向一个nil值的map存入元素将导致一个panic异常

var ages map[string]int

fmt.Println(ages == nil) // true
fmt.Println(len(ages) == 0) // true

ages["bob"] = 21 // panic 异常

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体, 每个值称为结构体的成员。

下面两个语句声明了一个叫Employee的命名的结构体类型,并且声明了一个Employee类型的

变量dilbert:

type Employee struct { 
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}

var dilbert Employee

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。 (该限制同样适应于数组。)但是S类型的结构体可以包含 *S 指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:

type tree struct {
value int
left, right *tree
}

func Sort(values []int) []int{
var root *tree
for _, v := range values {
root = add(root, v)
}
appendValues(values[:0], root)
return values
}

func appendValues(values []int, t *tree) []int {
if t != nil {
values = appendValues(values, t.left)
values = append(values, t.value)
values = appendValues(values, t.right)
}
return values
}

func add(t *tree, value int) *tree {
if t == nil {
// 等于 return &tree{value: value}
t = new(tree)
t.value = value
return t
}
if value < t.value {
t.left = add(t.left, value)
} else {
t.right = add(t.right, value)
}
return t
}

结构体字面值

结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。

type Point struct {X, Y int}

p := Point{1, 2}
p2 := Point{X: 1, Y: 2}

结构体比较

如果结构体的全部成员是可以比较的,则结构体也是可以比较的。

结构体嵌入和匿名成员

type Point struct { 
X, Y int
}

type Circle struct {
Center Point
Radius int
}

type Wheel struct {
Circle Circle
Spokes int
}

但是因此访问内嵌的成员会变得非常繁琐。

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就 叫匿名成员

type Circle struct { 
Point
Radius int
}

type Wheel struct {
Circle
Spokes int
}

然后我们可以直接访问叶子属性而不需要给出完整的路径(有点像类的继承)

var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20

不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:

w = Wheel{8, 8, 5, 20} // compile error: unknown fields 
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

结构体字面值声明必须遵循其本来的结构:

w = Wheel{Circle{Point{8, 8}, 5}, 20} 
w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

JSON

type Movie struct { 
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}

其中Year和Color成员后面的字符串面值是结构体成员Tag

data, err := json.MarshalIndent(movies, "", " ") 
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

[
{
"Title": "Casablanca",
"released": 1942,
"Actors": [ "Humphrey Bogart", "Ingrid Bergman" ]
},
{
"Title": "Cool Hand Luke",
"released": 1967,
"color": true,
"Actors": [ "Paul Newman" ]
},
{
"Title": "Bullitt",
"released": 1968,
"color": true,
"Actors": [ "Steve McQueen", "Jacqueline Bisset" ]
}
]

从输出可以看出团体成员Tag影响了JSON的值, 一个构体成员Tag是和在编译阶段关联到该成员的元信息字符串

文本和html模板

练习

练习 4.1: 编写一个函数,计算两个SHA256哈希码中不同bit的数目。(参考2.6.2节的 PopCount函数。)


练习 4.2: 编写一个程序,默认情况下打印标准输入的SHA256编码,并支持通过命令行flag 定制,输出SHA384或SHA512哈希算法。


练习4.3: 重写函数reverse, 使用数组指针作为参数而不是slice


练习4.4: 编写一个函数rotate,实现一次遍历就可以完成元素旋转


练习4.5: 编写一个就地处理函数,用于去除[]string slice中相邻的重复字符串元素


练习4.6: 编写一个就地处理函数, 用于将一个UTF-8编码的字节slice中所有相邻的Unicode空白字符(查看unicode.IsSpace)缩减为一个ASCII空白字符


练习4.7:修改函数reverse,来翻转一个UTF-8编码的字符串中的字符串元素,传入参数是该字符串对应的字节slice类型([]byte)。你可以做到不需要重新分配内存就实现该功能吗?


练习 4.8: 修改charcount程序,使用unicode.IsLetter等相关的函数,统计字母、数字等 Unicode中不同的字符类别。

练习 4.9: 编写一个程序wordfreq程序,报告输入文本中每个单词出现的频率。在第一次调用 Scan前先调用input.Split(bufio.ScanWords)函数,这样可以按单词而不是按行输入。

练习 4.10: 修改issues程序,根据问题的时间进行分类,比如不到一个月的、不到一年的、 超过一年。

练习 4.11: 编写一个工具,允许用户在命令行创建、读取、更新和关闭GitHub上的issue,当 必要的时候自动打开用户默认的编辑器用于输入文本信息。

练习 4.12: 流行的web漫画服务xkcd也提供了JSON接口。例如,一个 https://xkcd.com/571/info.0.json 请求将返回一个很多人喜爱的571编号的详细描述。下载每 个链接(只下载一次)然后创建一个离线索引。编写一个xkcd工具,使用这些离线索引,打 印和命令行输入的检索词相匹配的漫画的URL。

练习 4.13: 使用开放电影数据库的JSON服务接口,允许你检索和下载 https://omdbapi.com/ 上电影的名字和对应的海报图像。编写一个poster工具,通过命令行输入的电影名字,下载对 应的海报。

练习 4.14: 创建一个web服务器,查询一次GitHub,然后生成BUG报告、里程碑和对应的用户信息。