第四章 复合数据类型
四种符合数据类型:数组、slice、map和结构体
- 数组和结构体是聚合类型
- 数组是由同构的元素组成
- 结构体则是由异构的元素组成
slice
和map
则是动态的数据结构,它们将根据需要动态增长
4.1数组
划重点
- 因为数组的长度是固定的,因此在Go语言中很少直接使用数组
Slice
(切片),它是可以增长和收缩动态序列,slice
功能也更灵活,其基于数组- 数组的每个元素都被初始化为元素类型对应的零值
- 数组的长度位置出现的是
“...”
省略号,则表示数组的长度是根据初始
化值的个数来计算,[...]int{1, 2, 3}
表示[3]int{1, 2, 3}
- 数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。
- 可以通过索引和对应值列表的方式初始化,这个和C不太一样
type Currency int const ( USD Currency = iota // 美元 EUR // 欧元 GBP // 英镑 RMB // 人民币 ) symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"} fmt.Println(RMB, symbol[RMB]) // "3 ¥"
r := [...]int{99: -1} //100个元素,最后一个是-1,其他是0
- 相同数组类型(相同元素类型、相同长度)可以使用
==
、!=
进行比较。
a := [2]int{1, 2} b := [...]int{1, 2} c := [2]int{1, 3} fmt.Println(a == b, a == c, b == c) // "true false false" d := [3]int{1, 2} fmt.Println(a == d) // compile error: cannot compare [2]int == >[3]int
- 函数参数变量接收的是一个复制的副本,并不是原始调用的变量。传递大的数组类型是低效的。
- 由于数组是僵化的,数组很少用作函数参数,一般用slice代替数组。
常用库及方法
len()
crypto/sha256
sha256.Sum256
sha512.Sum512
flag
flag.Int
ioutil
ioutil.ReadAll
4.2Slice
划重点
- slice的语法和数组很像,只是没有固定长度而已
- slice是一个轻量级的数据结构,底层实现引用数组对象,提供了访问数组子序列(或者全部)元素的功能
- slice三要素:指针、长度和容量。len(slice)<=cap(slice)
- 多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠
- s[i:j],其中0 ≤ i≤ j≤ cap(s),引用s[i, j)即s[i, j-1],其中i,j可以省略
- 如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice
- 因为slice值包含指向第一个slice元素的指针,因此向函数传递slice只是对底层的数组创建了一个新的slice别名
- slice初始化时不会指明序列的长度,但会隐式地创建一个合适大小的数组
- slice之间不能比较,不能使用
==
,数组支持 - slice的元素是间接引用的,所以即使有
==
也仅仅是浅相等,但是会和数组形成混淆,故直接取消slice的==
,但是summer == nil
是允许的。
var s []int // len(s) == 0, s == nil s = nil // len(s) == 0, s == nil s = []int(nil) // len(s) == 0, s == nil s = []int{} // len(s) == 0, s != nil
len(s) == 0
来判断slice是否为空,而不应该用s == nil
来判断nil
值的slice的行为和其它任意0
长度的slice一样make
函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度
make([]T, len) make([]T, len, cap) // same as make([]T, cap)[:len]
常用库及方法
bytes
bytes.Equal
make
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
4.2.1append函数
划重点
- 每次
append
,如果空间足够则直接扩展slice,否则重新分配再复制。 - 内置的
copy
函数可以方便地将一个slice复制另一个相同类型的slice,dst = src
,返回成功复制的元素的个数。 - 内置的
append
函数则可以追加多个元素,甚至追加一个slice
。
var x []int x = append(x, 1) x = append(x, 2, 3) x = append(x, 4, 5, 6) x = append(x, x...) // append the slice x fmt.Println(x)
常用库及方法
4.2.2Slice内存技巧
划重点
常用库及方法
utf8.DecodeRune
unicode.IsSpace
4.3Map
划重点
- 一个无序的key/value对的集合,key唯一,通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value
- map就是一个哈希表的引用,map类型可以写为
map[K]V
,K/V
可以是不同的类型,但是K
必须支持==
操作的类型。 - 创建map,
make(map[string]int)
,map[string]int{}
。var ages map[strig]int
,此时ages
为零值nil
,len(ages)
为0:
make(map[string]int) // mapping from strings to ints
ages := map[string]int{ "alice": 31, "charlie": 34, }
//等价于
ages := make(map[string]int)
ages[“alice”] = 31
ages[“charlie”] = 34
- 内置的
delete
函数可以删除元素,delete(ages, "alice")
,这些操作是安全的,即使这些元素不在map中,如果一个查找失败将返回value类型对应的零值 map
中的元素并不是一个变量,不能对map
的元素进行取址操作。原因是map会由于重新分配更大的内存空间,导致之前的地址无效
_ = &ages["bob"] // compile error: cannot take address of map >element
- 遍历
map
中全部的key/value
,使用range
,Map的迭代顺序是不确定,这是故意的,强制要求遍历不依赖具体哈希函数实现,要顺序遍历,必须x先获取key
值的slice,再sort
key
值 if age, ok := ages["bob"]; !ok { /* ... */ }
,判断一个是真不存在,还是因为返回的是零值。- 和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较,比较需要通过循环实现
map
的value
类型也可以是一个聚合类型,比如是一个map
或slice
常用库及方法
sort
sort.Strings
bufio.NewScanner
bufio.NewScanner.Scan
bufio.ScanWords
bufio.NewScanner.Split
bufio.NewScanner.Text
bufio.NewReader
bufio.NewReader.ReadRune()
unicode.ReplacementChar
unicode.Properties
unicode.In
utf8
utf8.UTFMax
4.4结构体
划重点
- 结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体,每个值称为结构体的成员。
- 指向结构体的指针也可以使用
.
操作 - 调用函数返回的是值,并不是一个可取地址的变量???
- 结构成员顺序不同,表示定义不同的结构体类型
- 结构体成员名字是以大写字母开头的,那么该成员就是导出,一个结构体可以同时包含可导出和不可导出成员
- 一个聚合的值不能包含它自身。(该限制同样适应于数组。)但是
S
类型的结构体可以包含*S
指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。 - 结构体没有任何成员的话就是空结构体,写作struct{},它的大小为0。
常用库及方法
bytes.Buffer
sync.Mutex
4.4.1结构体面值
划重点
- 面值初始化方法有两种:1.
Point{1, 2}
2.Point{X: 1, Y: 2}
- 不能企图在外部包中用赋值的技巧来偷偷地初始化结构体中未导出的成员
- 考虑效率的话,较大的结构体通常会用指针的方式传入和返回
- 如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量
- 结构体通常通过指针处理,可以使用
pp := &Point{1, 2}
来创建并初始化一个结构体变量,并返回结构体的地址。等价于pp := new(Point) *pp = Point{1, 2}
常用库及方法
gif
gif.GIF
4.4.2结构体比较
划重点
- 结构体的每一个成员均可比较,则结构体也可比较,可使用
==
或!=
运算符,可用于map的key类型:make(map[address]int)
4.4.3结构体嵌入和匿名成员
划重点
- 结构体嵌入机制可以通过简单的点运算符x.f来访问匿名成员链
中嵌套的x.d.e.f成员,也就是直接访问叶子属性而不需要给出完整的路径
var w Wheel w.Circle.Center.X = 8 w.Circle.Center.Y = 8 w.Circle.Radius = 5 w.Spokes = 20 可以直接通过赋值: var w Wheel w.X = 8 // equivalent to w.Circle.Point.X = 8 w.Y = 8 // equivalent to w.Circle.Point.Y = 8 w.Radius = 5 // equivalent to w.Circle.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 //下面的方法是ok的 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) }
- Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值
- 匿名成员有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员
- 任何命名的类型都可以作为结构体的匿名成员,不是必须是结构体。这么做的目的是:可以同时用点运算符访问匿名类型的方法集。
- 匿名成员类型可以将一个有简单行为的对象组合成复杂的行为的对象。面向对象编程的核心。
4.5JSON
划重点
- JavaScript对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议。类似的XML、ASN.1和Google的ProtocolBuffers。
- 基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列。
- JSON数组是一个有序的值序列,写在一个方括号中并以逗号分隔,JSON数组可以用于编码Go语言的数组和slice。
- JSON对象是字符串到值的映射,写成以系列的name:value对形式,用花括号包含并以逗号分隔。
- JSON的对象类型可以用于编码Go语言的map类型(key类型是字符串)和结构体。
object {"year": 1980}
- 编组(marshaling),默认Go语言结构体的成员名字作为JSON的对象,导出的结构体成员才会被编码
- 结构体成员Tag是和在编译阶段关联到该成员的元信息字符串,比如
Year int `json:"released"
- 结构体成员Tag通常是一系列用空格分隔的key:"value"键值对序列
- 解码(unmarshaling),通过定义含有需要成员的结构体,可以有选择的解码JSON中感兴趣的成员
常用库及方法
encoding/json
json.Marshal
json.MarshalIndent
json.Unmarshal
json.Decoder(流式解码器)
json.Encoder(流式编码器)
encoding/xml
encoding/asn1
github.com/golang/protobuf
struct tag
omitempty
url
url.QueryEscape
fmt
fmt.Errorf
time
time.Now
now.AddDate
time.Time.After
time.Time.Before
4.6文本和HTML模板
划重点
- 提供了一个将变量值填充到一个文本或HTML格式的模板的机制。
- 一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的 {{action}} 对象。
- 模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流
if-else
语句和range
循环语句 |
操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念- 生成模板的输出需要两个处理步骤:
- 分析模板并转为内部表示
- 基于指定的输入执行模板
report, err := template.New("report"). Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ)
常用库及方法
text/template
template.Must
template.New
Template.Funcs
Template.Parse
template.FuncMap
report.Execute
html/template
template.Must
template.New
Template.Funcs
Template.Parse
template.FuncMap
report.Execute
template.HTML