channel 类型

  • channel 用于进行多线程管道通信与同步。

定义 channel

1
a := make(chan <type>, [<size>])
1
2
a := make(chan int)  // 无缓冲区的 int 通道
b := make(chan int, 2) // 缓冲区大小为 2 的 int 通道
  • 注意:channel也是引用数据类型,直接使用 var a chan int 是错误的,其没有构造 chan 结构。

channel 读写

  • channel 具有缓冲区大小,读写会对 channel 的缓冲区进行操作。当缓冲区为空时,读操作阻塞,知道新的写操作完成;当缓冲区为满时,写操作阻塞。

  • 特殊地,当缓冲区大小为 0 时,所有读写操作都会阻塞等待直到 channel 的读写操作配对。

  • channel

1
<channel> <- <data>
1
a <- 1
  • channel
1
<data> := <-<channel>
1
x := <-a
  • channel 非阻塞读
1
2
3
4
5
6
7
// 详见 Golang 入门基础(五) - 控制语句 - 条件控制语句 - select-case 语句
select {
case x := <-a:
<block1>
default: // 读取失败
<block0>
}

channel 应用

map 类型

定义 map

1
2
a := make(map[<keyType>]<valueType>)
b := map[<keyType>]<valueType>{<key>: <value>...}
1
2
a := make(map[string]int)
b := map[string]int{"abc": 1, "dfe": 9}
  • 注意:map也是引用数据类型,直接使用 var a map[<keyType>]<valueType> 是错误的,其没有构造 map 结构。

操作 map

  • 插入或修改元素
1
a[<key>] = <value>  // 如果元素存在, 插入值, 否则修改值
  • 访问元素
1
x := a[<key>]  // 如果元素存在, 获取元素值, 否则返回 0 值
  • 删除元素
1
delete(<mapName>, <key>)  // 如果元素存在, 删除元素, 否则不进行操作
  • 遍历元素
1
2
3
for key, value := range <mapName> {
// ...
}
1
2
3
for key := range <mapName> {
// ...
}

map 原理

  • map 底层为优化的拉链式哈希表,因此 map 是无序的。关于拉链式哈希表不多作介绍。
  • map 对拉链式哈希表作的优化包括:
    • 链表内存访问较慢,map 使用数组链表加速访问。数组链表是指每次申请一定数量的连续内存,连续内存溢出时再由连续内存链接到下一个申请的连续内存,一块连续内存可以存储多个键值对,这样一块连续内存称为 overflow(不包括第一块连续内存)。
    • 键比对可能较慢(如以字符串作为键),map 使用二级哈希值进行比对,在链表中仅当二级指针相同时才进行键比对。
  • map 查找工作流程:
    • 使用 key 类型对应的哈希函数计算足够大的哈希值 hashCode,取 hashCodeB 位作为一级哈希值 firstlyHashCode,取 hashCode 的高 8 位作为二级哈希值 secondaryHashCode,其中 B 的值取决于哈希表扩容后的大小 AB=log2(A)
    • firstlyHashCode 作为索引,索引到一个数组链表。
    • secodaryHashCode 作为键依次于数组链表的第一块连续内存中的 secodaryHashCode 比较,如果相同则继续比较 key,如果相同则认为找到,否则继续向后找。
    • 如果当前连续内存中没有找到,那么前往当前内存的下一个 overflow 以相同方式查找,如果没有更多 overflow,认为不存在。
  • map 扩容策略:
    • 如果满足以下条件之一,map 将申请一块 2 倍的连续内存,并将原表数据逐一转储到新表。
      • 负载因子=键数量/桶数量A>6.5负载因子 = 键数量 / 桶数量A > 6.5
      • 溢出桶overflow数量>32768溢出桶overflow数量 > 32768
    • 扩容后,负载因子折半,溢出桶数量均摊折半。

函数

具名函数与匿名函数

  • 函数按分类可以分为具名函数与匿名函数,函数是一个对象即一种数据类型,具名函数是一种特殊的匿名函数。
  • 具名函数
1
2
3
func <functionName>([parameterList...]) [returnTypes...] {
<block>
}
1
2
3
func DivMod(x int, y int) (int, int) {
return x / y, x % y
}
  • 匿名函数
1
2
3
<functionName> = f([parameterList...]) [returnTypes...] {
<block>
}
1
2
3
DivMod := func(x int, y int) (int, int) {
return x / y, x % y
}

函数传参

  • 每项参数使用 parameterName parameterType 的格式,见 3.8.1 的例子。

  • Golang 中没有引用类型,避免参数拷贝可使用指针类型 *parameterType

  • Golangargs... 表示解包,...args 表示封包,对应 Python 中的 *args ,其实际构造一个切片。

1
2
3
4
5
6
7
8
9
Sum := func(a ...int) int {
s := 0
for _, i := range a {
s += i
}
return s
}
fmt.Println(Sum([]int{1, 2, 3, 4, 5}...))
fmt.Println(Sum(1, 2, 3, 4, 5))
  • Golang 可以进行批量参数类型定义。
1
2
func demo(a, b int, s, ss string) {}
// func demo(a int, b int, s string, ss string) {}
  • Golang 中没有函数默认值、函数重载。

函数返回

  • 如果未指定函数返回值,需用 return 返回相同的返回值列表。
  • 如果指定函数返回值名,直接使用 return 返回即可。此时函数返回值已定义,无需在函数体内定义。
1
2
3
4
5
6
7
8
9
Sum := func(a ...int) (s int) {
s = 0 // 无需重复定义
for _, i := range a {
s += i
}
return // 无需重复指定返回值
}
fmt.Println(Sum([]int{1, 2, 3, 4, 5}...))
fmt.Println(Sum(1, 2, 3, 4, 5))

方法

  • Golang 实际没有类与对象的概念,方法是 Golang 的类面向对象实现,它实现了对象与函数的静态绑定。其调用方式与面向对象相同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 函数实现
type File struct {
fd int
flag uint
}

func open(file *File, fp string, mode string) {
// ...
}

func close(file *File) {
// ...
}

func main() {
file := File{}
open(&file, "data.txt", "w")
close(&file)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方法实现
type File struct {
fd int
flag uint
}

func (file *File) open(fp string, mode string) error {
// ...
}

func (file *File) close(fp string, mode string) error {
// ...
}

func main() {
file := File{}
file.open("data.txt", "w")
file.close()
}
  • 方法的对象必须是本包结构体。例如 func (builder strings.Builder) <functionName>([parameterList...]) [returnTypes...] 是错误的,func (x int) <functionName>([parameterList...]) [returnTypes...] 是错误的。
  • 值对象与指针对象是不同的,但它们的调用方式是相同的。以值对象传递,将对对象的拷贝操作;以指针对象传递,将直接操作对象。

接口

  • 接口也是 Golang 中类面向对象的实现,接口的存在类似于面向对象中的虚基类。接口是方法的集合,任何包含所有接口定义的方法的结构体都满足接口。接口可以被赋值为任何满足接口的结构体(隐式类型转换),但不能使用非接口定义的字段、方法。

定义接口

  • 定义接口。任何满足接口所有方法的结构体都可以赋值到接口。
1
2
3
4
5
type Human interface {
who() string
say(data interface{}) // 参数为匿名空接口类型
gender() bool
}
  • 定义空接口。任何结构体都可以赋值到空接口,空接口在底层原理上与一般接口不同。
1
2
type Any interface {
}
  • 叠加接口
1
2
3
4
5
6
7
8
9
10
11
12
type Writer interface {
write(data []byte) (int, error)
}

type Reader interface {
read(n int) ([]byte, error)
}

type WriteReader interface {
Writer
Reader
}
  • 匿名接口。在第一个示例中,interface{} 即为匿名接口。
1
var i interface{} = interface{}("Hello")

动态类型

  • 接口可以接收一切符合接口的类型,因此可以用于动态类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package main

import (
"fmt"
"unsafe"
)

type Human interface {
who() string
say(data interface{}) // 参数为匿名空接口类型
gender() bool
}

type Man struct {
name string
}

func (man Man) who() string {
fmt.Println(unsafe.Pointer(&man), unsafe.Sizeof(man))
return man.name
}

func (man Man) say(data interface{}) {
fmt.Println("He is", man.name, data)
}

func (man Man) gender() bool {
return true
}

type Woman struct {
name string
}

func (woman Woman) who() string {
return woman.name
}

func (woman Woman) say(data interface{}) {
fmt.Println("She is", woman.name, data)
}

func (woman Woman) gender() bool {
return false
}

type Me struct {
name string
}

func (me *Me) who() string {
return me.name
}

func (me *Me) say(data interface{}) {
fmt.Println("He is", me.name, data)
}

func (me *Me) gender() bool {
return true
}

func (me *Me) sleeping() bool {
return true
}

func main() {
var human Human

human = Man{"Mike"}
fmt.Println(human.who())
human.say("123")
fmt.Println("Gender:", human.gender())
fmt.Println()

human = Woman{"Jane"}
fmt.Println(human.who())
human.say("456")
fmt.Println("Gender:", human.gender())
fmt.Println()

human = &Me{"Jamhus"} // 指针类型, 因为其方法全部为指针类型
fmt.Println(human.who())
human.say("789")
fmt.Println("Gender:", human.gender())
fmt.Println()
}

类型断言

  • Golang 中接口支持返回到原始类型,判断接口原始类型的方法称为类型断言。类型断言使 Golang 中的动态类型更加灵活。

类型判断

  • value 为接收原始类型的变量,其类型为 originalTypeok 为判断 interfaceVariable 的原始类型是否为 originalType,判断为否,value 将没有意义。
1
2
<value>, <ok> := <interfaceVariable>.(<originalType>)
// 类型断言还有一种写法 <value> := <interfaceVariable>.(<originalType>) 此写法判断为否将抛出异常, 详见 Golang 入门基础(五) - 控制语句 - 异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
human := Human(Me("Jamhus"))

value1, ok := human.(Man)
if ok {
fmt.Println("It's Man.", value1)
} else {
fmt.Println("It's not Man.")
}

value3, ok := human.(Me)
if ok {
fmt.Println("It's Me.", value3)
} else {
fmt.Println("It's not Me.")
}
1
2
It's not Man.
It's Me. {Jamhus}

类型分支

1
2
3
4
5
6
7
8
switch human.(type) {
case Man:
fmt.Println("It's Man", human.(Man))
case Woman:
fmt.Println("It's Woman", human.(Woman))
case Me:
fmt.Println("It's Man", human.(Me))
}