Golang 学习笔记 2-3 复合数据类型引用类型
Table of Contents
1 slice
1.1 slice 基础
slice 表示一个拥有相同类型元素的变长序列,可以用来访问数组的部分元素。 命令 []T
可以创建 slice,其中的元素类型都是 T
。
slice 的源代码定义是这样的:
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice 有三个属性:
- 指针: 指向数组中第一个可以被 slice 访问的元素(不一定是数组的第一个元素)。
- 长度: 指 slice 中元素的个数,不能超过 slice 的容量。
- 容量: 大小通常是从 slice 的起始元素到底层数组的最后一个元素间元素的个数。内置了
len
和cap
用来返回 slice 的长度和容量。
一个底层数组可以对应多个 slice,这些 slice 可以引用数组的任何位置。即, 引用同一个底层数组的多个 slice 的元素可以重叠 。
因为 slice 包含了指向数组元素的指针,所以将一个 slice 传递给函数后,可以在函数内修改底层数组。也就是说, 创建slice 等于创建了数组的一个别名 。
1.2 slice 的子 slice
slice 操作符 new = s[i:j]
( 0 <= i <= j <= cap(s)
) 创建了一个新的 slice,新的 slice new
引用了序列 s
中第 i
到 j-1
位置的所有元素。 new
属性:
- 长度
len(new)
为j-i
,即,引用的长度。 - 容量
cap(new)
为cap(s) - i
,即,底层数组被引用的位置到底层数组尾部元素的个数。
这里的 s
既可以是数组或指向数组的指针,也可以是 slice。
注: 所有语言设计中的子串都使用半开区间 [i,j)
,包前不包后。
slice 引用的终点超过被引用对象的容量 cap(s)
,程序宕机。slice 引用的终点超过被引用对象的长度 len(s)
则最终 slice 会比原 slice 长:
months := [...]string{
1: "January",
2:"February",
3: "March",
4: "April",
5: "May",
6: "June",
7: "July",
8: "August",
9: "September",
10: "October",
11: "November",
12: "December"}
Q2 := months[4:7]
fmt.Println(Q2, cap(Q2)) // `[April May June] 9`
summer := months[6:9] // `[June July August]`
fmt.Println(summer[:20]) // 宕机, 20 > cap(months)
endlessSummer := summer[:5] // 在 slice 容量范围内扩展了 slice
fmt.Println(endlessSummer) // `[June July August September October]`
求字符串字串的操作和对字节 slice ( []byte
) 做 slice 操作都写作 x[m:n]
,都返回原始字节的一个子序列,引用底层的方式也是相同的,所以两个操作都消耗常量时间。不同在于: 如果 x 是字符串,那么 x[m:n]
会返回字符串;如果 x 是字节 slice,那么返回字节 slice 。返回类型不同。
a := [...]int{1, 2, 3, 4, 5, 6, 7} // 数组
a1 := a[6:]
fmt.Println(a1) // `[7]`
fmt.Println(reflect.TypeOf(a1).Kind()) // `slice`
b := "holly shit" // 字符串
b1 := b[5:]
fmt.Println(b1) // `" shit"`
fmt.Println(reflect.TypeOf(b1).Kind()) // `string`
c := []byte{1,2,3,4,5} // 字节 slice
c1 := c[3:]
fmt.Println(c1) // `[4 5]`
fmt.Println(reflect.TypeOf(c1).Name()) // `slice`
要注意初始化 slice s 的表达式和初始化数组 a 的表达式的区别。slice 字面量看上去和数组字面量很像,都是用逗号分隔用花括号括起来的一个元素序列,如 {"a", "b", "c"}
,但是 slice 没有指定长度 []string
,而数组指定了长度 [3]string
。这种隐式区别的结果分别是,创建有固定长度的数组和创建指向数组的 slice。
sa := []string{"a", "b", "c"}
sb := [3]string{"a", "b", "c"}
fmt.Println(reflect.TypeOf(sa), reflect.TypeOf(sa).Kind())
// `[]string slice`
fmt.Println(reflect.TypeOf(sa), reflect.TypeOf(sb).Kind())
// `[3]string array`
和数组不同的是,slice 只可以和 nil
比较,所以不能用 ==
来比较两个 slice 是否拥有相同的元素。标准库里提供了优化的函数 bytes.Equal
来比较两个字节 slice ( []byte
)。但是对于其他类型的 slice 必须自己写比较函数。
slice 类型的零值是 nil
。值为 nil
的 slice 没有底层数组,长度和容量都是 0。但是也有非 nil
的 slice 的长度和容量都是 0,比如 []int{}
和 make([]int, 3)[3:]
。所以, 想要检查一个 slice 是否为空,要用 len(s) == 0
调内置函数的方式 。
内置函数 make
可以创建一个具有指定元素类型,长度和容量的 slice;这个数组仅可以通过这个 slice 来访问。
make([]T, len)
make([]T, len, cap) // 效果等同于 make([]T, len)[:len]
1.3 append 函数
内置函数 append
用来 将元素追加到 slice 的后面 。
如果要向 slice s
的子串 new := s[i:j]
中追加元素:
如果
new[len(s)-1] == s[cap(s)-1]
成立, 即new
的最后一个元素与s
的最后一个元素相同,且s
为顶层 slice ,系统会在内存中新建一个cap(array) > cap(new)
的底层数组 ( 新的底层数组容量大于原来的空间 ),将 new 的内容复制到 array 中去,再在 array 的最后追加元素。即,在元素追加后,array 与 new 指向不同的底层数组。c := []byte{1,2,3,4,5} fmt.Println(cap(c)) // 5 fmt.Printf("%p\n", c) // 0xc00020b370 c = append(c, 6) fmt.Printf("%p\n", c) // 0xc00020b380 fmt.Println(cap(c)) // 16
当
j - 1 < cap(s) - 1
成立,即, slicenew
的最后一个元素位于s
的最后一个元素之前,且s
为顶层 slice 。则追加的元素会代替s[j]
,即,new
引用s
中的最后一个元素的后一个元素,不会申请新的内存。s := []int{1,2,3,4,5} new:= s[1:3] fmt.Println(new, len(new), cap(new)) // `[2 3] 2 4` new = append(new,777) s[1] = 999 fmt.Println(s, new) // `[1 999 3 777 5] [999 3 777]`
如果
new[len(s)-1] == s[cap(s)-1]
成立, 即new
的最后一个元素与s
的最后一个元素相同,且s
的父 slicesfather
的容量大于s
( cap(sfather) > cap(s) ) ,则追加的元素会代替sfather[cap(new)]
,即new
引用sfather
中的最后一个元素的后一个元素,不会申请新的内存。c := []byte{1, 2, 3, 4, 5, 6, 7, 8} d := c[2:5] e := d[:] fmt.Println(c) // `[1 2 3 4 5 6 7 8]` fmt.Printf("%p\t %d\n", c, cap(c)) // `0xc00021f510 8` fmt.Println(d) // `[3 4 5]` fmt.Printf("%p\t %d\n", d, cap(d)) // `0xc00021f512 6` fmt.Println(e) // `[3 4 5]` fmt.Printf("%p\t %d\n", e, cap(e)) // `0xc00021f512 6` e = append(e, 99) fmt.Println(e) // `[3 4 5 99]` fmt.Printf("%p\t %d\n", c, cap(c)) // `0xc00021f510 8` fmt.Println(d) // `[3 4 5]` fmt.Printf("%p\t %d\n", d, cap(d)) // `0xc00021f512 6` fmt.Println(c) // `[1 2 3 4 5 99 7 8]` fmt.Printf("%p\t %d\n", e, cap(e)) // `0xc00021f512 6`
所以, 在调用 append
后并不能保证操作前与操作后的 slice 指向同一个底层数组。同样,也不能假设在旧的 slice 上操作会不会影响新的 slice 。所以, 通常要把调用结果再次赋值给传入 append 函数的 slice 。
runes = append(runes, r)
对于任何函数,只要有可能改变 slice 的长度或容量,有或者让 slice 指向不同的底层数组,都需要更新 slice 变量。
要注意,虽然底层数组的元素是间接引用的,但是 slice 的指针、长度和变量不是。要更新一个slice指针,长度或容量必须使用上面的显式赋值。从这个角度看,slice 并不是严格的纯引用类型,而像下面这种聚合类型。
type IntSlice struct{
ptr *int
len,cap int
}
append
可以向 slice 中添加多个变量,甚至添加另一个 slice 里的所有元素。
var x []int
x = append(x, 1)
x = append(x, 1, 2)
x = append(x, 1, 2, 3)
x = append(x, x...) // 追加 x 的所有元素
fmt.Println(x) // `[1 1 2 1 2 3 1 1 2 1 2 3]`
2 map
map 是 go 语言里的键值对数据类型,但是不允许获取 map 的值的地址,map 不可比较,遍历顺序也是随机的。
2.1 map 基础概念
map(散列表)是一个拥有键值对元素的无序集合。键的值唯一,键对应的值可以通过键来获取、修改和移除。 无论这个散列表有多大,这些操作基本通过常量时间的键比较就可以完成。
map 的类型是 map[K]V
其中 K
和 V
是字典的键和值对应的数据类型。map 中所有的键都有相同的数据类型,所有的值也要有相同的数据类型,键和值的类型不一定相同。
键的类型 K
必须是可以用 ==
进行比较的类型 ( 不要用浮点类型做 `K` )。值的类型没有任何限制。
2.2 map 上的操作
内置函数 make
可以用来 创建并初始化一个 map:
ages := make(map[string]int) // 创建一个 string 到 int 的 map
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
// 上面的写法等价于
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
// 因此,新的空 map 的另一种表达式是
ages := map[string]int{}
内置函数 delete
可以从字典中 根据键移除一个元素,即使键不存在,也不会报错:
delete(ages, "alice") // 移除 ages["alice"]
map 使用给定的键来查找元素,如果对应的元素不存在,就返回类型的零值。并不会把这个值加入 map,除非显式地创建。
fmt.Println(ages["bob"] + 1) // `1`
fmt.Println(ages["bob"]) // `0`
ages["bob"] = ages["bob"] + 1 // 显式地创建了 ages["bob"]
fmt.Println(ages["bob"]) // `1`
// 即使不存在 ages["bob"] 上面的代码也可以执行
// 当键 K 不存在时,键 K 对应的值为 V 类型的零值
map 元素不是一个变量, 不允许获取 map 元素的地址 。因为 map 的增长可能导致已有元素被重新散列到新的存储位置,这样获取的地址就没有意义了。
_ = &ages["bob"] // 编译错误,无法获取 map 元素的地址
可以使用 for 循环来遍历 map 的所有元素,就像 slice 一样。但是 map 的迭代顺序是不固定的 ,不同的实现方法会使用不同的散列算法得到不同的元素顺序。 在实践中,可以认为这种顺序是随机的:
for name, age := range ages {
fmt.Println("%s\t%d\n", name, age)
}
/*
bob 1
alice 31
charlie 34
*/
for name, age := range ages {
fmt.Println("%s\t%d\n", name, age)
}
/*
alice 31
charlie 34
bob 1
*/
// 两次执行遍历输出,得到了不一样的输出结果。
如果要按照某种顺序来遍历 map 则需要显式地给键排序。 例如,如果键是字符串类型,则可以用 sort
包中的 Strings
函数来进行键的排序,这是常见的模式:
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])
}
// 因为一开始就知道 slice names 的长度
// 所以直接指定 slice 的容量会让代码快一些
// var_name := make([]T, len, cap)
names := make([]string, 0, len(ages))
map 类型的零值是 nil
*,也就是没有引用任何散列表。和 slice 一样,除了与 nil
之外,map 不可比较。要比较两个 map 是否拥有相同的键和值必须自己实现这个功能。 大多数 map 操作都可以安全地在 map 的零值 nil
上进行。但是向零值 map 中设置元素会报错:
var ages map[string]int // 仅声明
fmt.Println(ages == nil) // `true`
fmt.Println(len(ages) == 0) // `true`
ages["carol"] = 21 // 报错,为零值 map 中的项赋值
ages := map[string]int // 报错,map[string]int is not an expression
ages := map[string]int{} // 声明并赋值
通过下标访问 map 总是会有值。如果 K 在 map 中,会得到 K 对应的值;如果 K 不在 map 中,会得到类型的零值。 如果需要知道一个 K 在不在 map 中,需要处理值访问操作返回的另一个参数。
age, ok := ages["bob"]
if !ok { /* "bob" 不是 map 中的键, age == 0, ok == false */ }
// 通常会将两条语句合并
if age, ok := ages["bob"]; !ok { /* ... */}
2.3 用 map 实现的高级功能
实现集合类型的实现方式。使用 slice 这种不可比较的类型做键(让不可比较变得可比较)。
2.3.1 集合类型
Go 没有提供集合类型,但是既然 map 的键都是唯一的,就可以利用 map 来实现这个功能。
func main(){
seen := make(map[string]bool)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
if !seen[line] { // 如果这行不存在, 那么会得到 V 的零值 false
seen[line] = true // 把这行加入 map 中
fmt.Println(line) // 输出所加入的行
}
}
if err : input.Err(); err != nil {
fmt.Fprintf(os.stderr, "dedup: %v\n", err)
os.Exit(1)
}
}
注意: 可以将这种忽略 value 的 map 当作一个字符串集合。
2.3.2 slice 做键
原理: 按照统一格式转换 slice 为字符串作键 K;当两个 lisce 相等时,两个 slice 按照统一格式转换成的字符串相等,即键 K 相等 。同样的思路适合于任何不可比较的类型。
slice 无法比较,但 K 必须是可比较的。所以,可以定一个帮助函数 func k
将每一个键都映射到字符串,当且仅当 x 和 y 相等的时候,才认为 k(x) == k(y)
成立。然后就可以创建一个 map,键是字符串类型,当每个元素被访问的时候,调用这个帮助函数。
也就是说,slice 内部元素是有顺序的,所以可以将每个 slice 转换成格式化形式的字符串:
list := []string{"Jan", "Feb"}
s := fmt.Sprintf("%q", list)
// fmt.Sprintf() 会将格式化输出转为字符串返回
fmt.Println(s)
// s 为字符串, 值为 `["Jan", "Feb"]`
sb := fmt.Sprintf("this month is %s", list[0])
fmt.Println(sb) // "this month is Jan"
for i, v := range s{
fmt.Printf("index:%d, value:%s, valueNumber:%v\n", i, string(v), v)
}
/* 这个循环产生如下输出
index:0, value:[, valueNumber:91
index:1, value:", valueNumber:34
index:2, value:J, valueNumber:74
index:3, value:a, valueNumber:97
index:4, value:n, valueNumber:110
index:5, value:", valueNumber:34
index:6, value: , valueNumber:32
index:7, value:", valueNumber:34
index:8, value:F, valueNumber:70
index:9, value:e, valueNumber:101
index:10, value:b, valueNumber:98
index:11, value:", valueNumber:34
index:12, value:], valueNumber:93
*/
下面给一个例子,使用 map 来记录一个 Add
这个函数被调用的次数。
var m = make(map[string]int)
func k(list p[string]) string { return fmt.Sprintf("%q", list) }
func test(){
jf := []string{"Jan", "Feb"}
ma := []string{"Mar", "Apr"}
m[k(jf)] = 12
m[k(ma)] = 34
fmt.Printf("%d, %d", m[k(jf)], m[k(ma)]) // 12, 34`
}
3 json
3.1 json 描述结构体
可以简单理解成 map 数据类型的字符串化,通常用来描述结构体对象。 json.Marshal
函数会将结构体解析为 json 格式,调用后生成一个字节 slice([]byte)。
type Message struct {
Name string
Body string
Time int64
}
msgs := []Message{
{"Alice", "Hello", 1294706395881547000},
{"Bob", "Hi", 1294706395881547009},
}
data, err := json.Marshal(msgs)
if err != nil{
fmt.Println(err)
}
fmt.Println(data)
fmt.Printf("%s\n", data)
// [91 123 34 78 97 109 101 34 58 34 65 108 105 99 101 34 44 34 66 111 100 121 34 58 34 72 101 108 108 111 34 44 34 84 105 109 101 34 58 49 50 57 52 55 48 54 51 57 53 56 56 49 53 52 55 48 48 48 125 44 123 34 78 97 109 101 34 58 34 66 111 98 34 44 34 66 111 100 121 34 58 34 72 105 34 44 34 84 105 109 101 34 58 49 50 57 52 55 48 54 51 57 53 56 56 49 53 52 55 48 48 57 125 93]
// [{"Name":"Alice","Body":"Hello","Time":1294706395881547000},{"Name":"Bob","Body":"Hi","Time":1294706395881547009}]
上面不论是哪个输出都不容易阅读,有一个 函数 json.MarshalIndent
可以输出整齐格式化过的结果。第一个参数是定义输出的前缀字符串,另一个定义缩进 。
data, err := json.MarshalIndent(msgs, "", " ")
fmt.Printf("%s\n", data)
/*
[
{
"Name": "Alice",
"Body": "Hello",
"Time": 1294706395881547000
},
{
"Name": "Bob",
"Body": "Hi",
"Time": 1294706395881547009
}
]
*/
函数 Marshal
使用 go 结构体成员名称作为 json 对象里字段的名称,只有可导出的成员可以转换为 json 字段。如果要自定义成员变量解析为 json 格式时的名称,可以使用 成员标签 来实现。
type Message struct {
Name string `json:"msg_name"`
Body string `json:"msg_body"`
Time int64 `json:"send_time"`
}
msgs := []Message{
{"Alice", "Hello", 1294706395881547000},
{"Bob", "Hi", 1294706395881547009},
}
dataA, err := json.Marshal(msgs)
dataB, err := json.MarshalIndent(msgs, "", " ")
if err != nil{
fmt.Println(err)
}
fmt.Printf("%s\n", dataA)
fmt.Printf("%s\n", dataB)
// [{"msg_name":"Alice","msg_body":"Hello","send_time":1294706395881547000},{"msg_name":"Bob","msg_body":"Hi","send_time":1294706395881547009}]
/*
[
{
"msg_name": "Alice",
"msg_body": "Hello",
"send_time": 1294706395881547000
},
{
"msg_name": "Bob",
"msg_body": "Hi",
"send_time": 1294706395881547009
}
]
*/
Marshal
的逆操作将 json 字符串解码为 go 的数据结构,这个函数叫 Unmarshal
。
msgData := []byte(`[{"msg_name":"Alice","msg_body":"Hello","send_time":1294706395881547000},{"msg_name":"Bob","msg_body":"Hi","send_time":1294706395881547009}]`)
var msgs []Message
if err := json.Unmarshal(msgData, &msgs); err != nil{
fmt.Println(err)
}
fmt.Println(msgs)
// [{Alice Hello 1294706395881547000} {Bob Hi 1294706395881547009}]
3.2 json 转 map
一个例子:
// setting.json
{
"task": {
"maxTask": "1000000"
},
"redis": {
"ip": "127.0.0.1",
"port": "6379",
}
}
var config map[string]map[string]string
settings, ioReadErr := ioutil.ReadFile("settings.json")
if ioReadErr != nil {
fmt.Println(ioReadErr)
}
fmt.Println(settings) // seetings 是 []byte
// [123 10 32 32 32 32 34 116 97 115 107 34 58 32 123 10 32 32 32 32 32 32 32 32 34 109 97 120 95 116 97 115 107 34 58 32 34 49 48 48 48 48 48 48 34 10 32 32 32 32 125 44 10 32 32 32 32 34 114 101 100 105 115 34 58 32 123 10 32 32 32 32 32 32 32 32 34 105 112 34 58 32 34 49 50 55 46 48 46 48 46 49 34 44 10 32 32 32 32 32 32 32 32 34 112 111 114 116 34 58 32 34 54 51 55 57 34 10 32 32 32 32 125 10 125]
jsonErr := json.Unmarshal(settings, &config)
if jsonErr != nil {
fmt.Println(jsonErr)
}
fmt.Println(config)
// map[redis:map[ip:127.0.0.1 port:6379] task:map[max_task:1000000]]