目录
第四章 复合数据类型
数组
- 数组是由固定长度的特定元素组成的序列,一个数组可以由零个或多个元素组成
- 索引下标的范围是从0开始到数组长度减1的位置
- 默认情况下,数组的每个元素都被初始化为对应类型的零值
- ...省略号表示数组的长度是根据初始化值的个数决定的
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
- 数组的长度是数组类型的一个组成部分,[3]int和[4]int不是相同的数组类型
- 数组的长度必须是常量表达式
- 数组间可以相互比较,按序列顺序,逐个元素比较,但需满足:类型相同,记住[3]int和[4]int不是相同类型
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
- Slice(切片)是变长序列,每个元素都是相同类型的,一般写作[]T
- Slice提供访问数组子序列(或全部)元素的功能,它的底层引用了一个数组对象
- Slice由三个部分构成
1.指针:指向第一个slice元素对应的底层数组元素地址
2.长度:长度不能超过容量,可用len函数求得
3.容量:容量一般是从slice开始位置到底层数据的结尾位置的长度,可用cap函数求得 - Slice可以由数组的切片操作得到,切片操作s[i:j]
1.若省略i,会用0代替
2.若省略j,会用len(s)代替
3.切片操作得到的是一个“左闭右开的区间”
4.如果超出cap(s)会导致panic
5.如果超出len(s)而没超出cap(s),会扩展(这里指的扩展是在原来的slice下扩展,但是直接访问超出len(s)的元素将会产生panic)
func main() {
a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
s := a[:4]
s2 := s[:7]
fmt.Println(s, s2)//[1 2 3 4] [1 2 3 4 5 6 7]
fmt.Println(cap(s)) //10
fmt.Println(s[7]) //panic:index out of range
fmt.Printf("%T\n", s) //[]int
}
- 字符串和[]byte的切片操作相似,都是x[m:n],都是返回一个原始字节序列的子序列,底层都是共享之前的底层数组
- 需要注意的是字符串的切片操作还是字符串,实际上都是字符串而不是切片,也就是常量,可以使用len但是不能用cap
s1 := "hello,world"
s2 := s1[:5]
fmt.Println(cap(s1)) //panic
fmt.Println(len(s1)) //11
fmt.Println(cap(s2)) //panic
fmt.Println(len(s2)) //5
fmt.Printf("%T\n", s2) //string
- 相比较字符串,数组的切片,可以修改数据,就像普通数组一样可以修改数据
- 在使用切片修改数据时,必须注意会改变底层数组的数据,因为修改的是同一片内存空间(这其实在笔记Slice部分一开始就说明了,构成中的指针指向第一个slice元素对应的底层数组元素地址)
- 当然,可以使用字面量来创建slcie。其底层数组就是字面量指出的匿名数组了
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
- 和数组不同,slice之间不能直接相互比较
- 对于[]byte的比较,有bytes.Equal函数来判断两个[]byte是否相等
- 其它的类型,需要自己定义比较的方法
- 为什么不直接支持slice的比较操作呢?
1.slice是间接引用,slice可以包含自身
2.一个固定的slice在不同的时间可能包含不同的元素,因为底层数组的元素可能会被修改 - slice唯一合法的比较是和nil比较,一个零值的slice等于nil,这代表slice没有底层数组,长度和容量都是0
- 也有非nil值,但长度容量都为0的slice,例如[]int{}或make([]int, 3)[3:],这是允许的
- 在实际应用中,必须搞清楚slice的空究竟代表什么:
1.为nil?
2.不为nil,但是len=0? - 以上两种情况,len都是为0,故判断slice为空的通用方式可以以采用len(s)==0
- 可以使用内置的make函数来创建指定的slice
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
append函数
- 内置的append函数用于向slice追加元素,或者追加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)
// "[1 2 3 4 5 6 1 2 3 4 5 6]"
- 但是append不能保证内存是否有重新分配,也不能确定新的slice和原始的是否引用相同的底层数组空间
- 书中给出一个slice的结构参考,便于理解
type IntSlice struct {
ptr
*int
len, cap int
}
Slice内存技巧
- 这一部分都是介绍例子,详细看原书
- 一个有用的问题:通过函数传参的slice会如何呢?
- 书中给出的例子证明了,对于[]string传参后,作为参数的[]string和原来的[]string是指向同一个底层string数组的,但如果使用了append处理后,就不一定了
- 建议:每次处理完slice,最好覆盖掉原来的slice
data := []string{"hello", " ", ",", "world" }
data = nonempty(data)
- 内置copy函数,可以把后面的slice向前依次移动一位:
copy(slice[i:], slice[i+1:])
Map
- Map是一个哈希表的引用,可以写作map[K]V,也就是key,value:
1.key都有相同的类型,value都有相同的类型,key和value可以是不同类型
2.key的类型必须支持==操作,但不推荐用浮点数作为key,浮点数有可能出现NaN和任何浮点数都不相等
3.可以使用len函数获取map的长度 - 要创建一个map,推荐使用内置make,同样的,这里把slice也放进来作为对比
var a1 []int //只是声明这么一个类型,对应的零值为nil
a2 := make([]int, 3) //这里创建了一个[]int数据,并且长度为3,每个元素对应零值
0
var b1 map[string]int //只是声明,对应零值为nil,不能添加值
b2 := make(map[string]int) //这里创建了map[string]int数据,并且可以添加值了
- 从上面例子看出引用类型的声明行为往往不如人意,需要特别注意
- map添加元素很简单:
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
- 删除元素使用内置的delete函数
delete(ages, "alice") // remove element ages["alice"]
- 查询(访问)元素
age1 := ages["bob"] //如果存在,返回值;否则,返回相应的零值
age2, ok := ages["bob"] //如果存在,返回value, true;否则,返回零值,false
- 对map中的value的取址是不允许的
- 原因在于,map的动态的,元素增长可能导致重新分配空间,从而可能导致之前的地址无效
_ = &ages["bob"] // compile error: cannot take address of map element
- 使用range循环遍历map,遍历顺序不确定,每次都是随机的: k, v := range map
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
- 如果需要排序,需要显式使用sorts包对key的值进行排序,相当于创建一个和key相同类型的slice
import "sort"
var names []string
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
Set类型(Go并不直接提供)
- 通常使用map[T]bool来作为类型T的集合,当然,value的类型bool可以是其它有意义的值
一些比较灵活的用法
var graph = make(map[string]map[string]bool)
func addEdge(from, to string) {
edges := graph[from]
if edges == nil {
edges = make(map[string]bool)
graph[from] = edges
}
edges[to] = true
}
func hasEdge(from, to string) bool {
return graph[from][to]
}
结构体
- 是由零个或多个任意类型的值聚合成的实体
- 结构体的成员,不像map的成员不能取址(这个还要看作用域,包一级,导出性)
- 结构体的零值是相应成员的都是零值
对于空结构体struct{},它的大小为0,应该尽量避免
结构体面值
- 不指定成员,将按声明顺序赋值或者初始化
type Point struct{ X, Y int }
p := Point{1, 2}
- 指定成员赋值或者初始化
p := Point{X: 1, Y: 2}
p2 := Point{
X; 1,
Y: 2,
}
- 注意到p2,{必须和Point同行,Y:2结尾需要逗号
- 还需注意,外部包如果要调用某个包的结构体,需要导出才行(大写开头)
package p
type T struct{ a, b int } // a and b are not exported
package q
import "p"
var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b
var _ = p.T{1, 2}// compile error: can't reference a, b
结构体比较
- 如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的
结构体嵌入和匿名成员
- 下面给出两个例子,注意嵌入的方式,还有访问形式
type Point struct {
X, Y int
}
type Circle struct {
Center Point
Radius int
}
type Wheel struct {
Circle Circle
Spokes int
}
var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
以上代码的等价形式(注释部分写法依然有效)如下:
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
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{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
- JavaScript对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议(由包encoding/json支持),类似的协议还有:
1.XML (encoding/xml)
2.ASN.1 (encoding/asn1)
3.Google的Protocol Buffers (github.com/golang/protobuf) - JSON是对javascript中各种类型的值进行Unicode文本编码
- JSON数组用方括号表示,对应Go的数组和切片
- JSON对象是一个字符串到值的映射,也就是一系列name:value对,用花括号包含并以逗号分隔,可以对应Go的map类型
- 例子
boolean true
number -273.15
string "She said \"Hello, BF\""
array ["gold", "silver", "bronze"]
object {"year": 1980,
"event": "archery",
"medals": ["gold", "silver", "bronze"]}
encoding/json的基本操作:
1.Marshal
2.Unmarshal
3.MarshalIndent
4.Decoder
5.Encoder详细的在反射章节补充
文件和HTML模板
- 这一部分不作记录,原因在于
1.后端一般这是数据的处理,怎么呈现数据,这个交给前端反而更方便,而且减少了后端的数据处理压力(但真的需要后端处理时,再看也不迟)
2.这里面的实现,都是靠go语言的基础来做的,接口,反射这些还没懂之前,看这个意义不大