Dawn's Blogs

分享技术 记录成长

0%

RPC

简介

RPC(Remote Procedure Call,远程过程调用)允许一台计算机通过网络从远程计算机的程序中请求服务。RPC协议构建于TCP或UDP,或者是HTTP上。允许开发者直接调用另一台服务器上的程序,而开发者无需另外的为这个调用过程编写网络通信相关代码。

RPC允许跨机器跨语言调用计算机程序方法。

工作原理

运行时,一次客户机对服务器的 RPC 调用,其内部操作大致有如下:

  1. 调用客户端句柄:执行传送参数
  2. 调用本地系统内核发送网络消息
  3. 消息传送到远程主机
  4. 服务器句柄得到消息并取得参数
  5. 执行远程过程
  6. 执行的过程将结果返回服务器句柄
  7. 服务器句柄返回结果,调用远程系统内核
  8. 消息传回本地主机
  9. 客户句柄由内核接收消息
  10. 客户接收句柄返回的数据

img

RPC vs RESTful

RPC 可以通过 TCP、UDP 或者 HTTP 等进行传输消息,称为 RPC over TCP、RPC over HTTP。

首先比较 RPC over HTTP 和 RESTful:

  • RPC 的客户端和服务器是紧密耦合的,,客户端需要知道调用的过程的名字,过程的参数以及它们的类型、顺序等。一旦服务器更改了过程的实现, 客户端的实现很容易出问题。RESTful 的客户端和服务器是松散的关系,客户端并不关心服务器端的具体实现,比较灵活。
  • 它们操作的对象不一样。RPC 操作的是方法对象,而 RESTful 操作的是资源
  • RESTful 执行的是对资源的增删改查(CRUD),如果实现一个其他具体操作 RPC 的实现更加具有意义。

接着比较 RPC over TCP 和 RESTful:

  • RPC over TCP 减少了 HTTP 协议带来的额外开销(如HTTP 请求头)。
  • RPC over TCP 可以通过长连接减少连接的建立所产生的花费,当然 RESTful 也可以通过 keep-alive 实现长连接,但是它最大的一个问题是它的 request-response 模型是阻塞的(HTTP 1.0 和 HTTP 1.1 是阻塞的,HTTP 2.0 不是),发送一个请求得到响应后才能发送第二个,而 RPC 没有这个限制。

Go RPC

GO语言官方提供了net/rpc包实现RPC,使用encoding/gob进行编解码,支持TCP或者HTTP数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc实现的RPC方法没办法实现跨语言调用

net/rpc 介绍

net/rpc包允许PRC客户端程序通过网络或者其他IO连接,调用一个远程对象的公开方法。在PRC服务端,可将一个对象注册为可访问的服务,之后该对象的公开方法就能够以远程的方式提供访问。一个RPC服务端可以注册多个不通类型的对象,但不允许注册同一类型的多个对象。

只有满足如下标准的方法才能用于远程访问,其余方法会被忽略:

  • 方法是导出的(首字母大写)
  • 方法有两个参数,第一个参数是接收的参数,第二个参数是返回给客户端的参数
  • 方法的第二个参数是指针
  • 方法只有一个error接口类型的返回值

事实上,方法必须看起来像这样:

1
func (t *T) MethodName(argType T1, replyType *T2) error

方法的第一个参数代表调用者提供的参数第二个参数代表返回给调用者的参数。方法的返回值,如果非nil,将被作为字符串回传,在客户端看来就和errors.New创建的一样。如果返回了错误,回复的参数将不会被发送给客户端。

阅读全文 »

移动零

移动零

解题思路

  1. 利用冒泡,元素交换的基本思想:当num[i] == 0 && nums[i+1] != 0时交换元素。若本轮没有交换元素,可以提前跳出循环

  2. 双指针法,两次遍历。创建两个指针ij,第一次遍历的时候指针j记录有多少个非零元素。第一次遍历完后,j指针的下标就指向了最后一个非零元素下标。第二次遍历的时候,起始位置就从j开始,将剩下的这段区域内的元素全部置为0

  3. 双指针法,一次遍历。参照了快速排序的基本思想,把0作为中间点,把不等于0的放到中间点的左边,等于0的放到其右边。使用两个指针ij,只要nums[i]!=0,就交换nums[i]nums[j]

阅读全文 »

字符串解码

字符串解码

解题思路

利用两个numStack记录需要重复的次数kstrStack记录扫描到的字符串。从左向右扫描字符串:

  • 若遇到数字:压入numStack
  • 若遇到左括号或者字母:压入strStack
  • 若遇到右括号:需要字符串解码
    • strStack中的字符串依次出栈组成待重复的子串subStr,直到遇到左括号为止
    • 弹出numStack栈顶元素k,使subStr重复k次并重新压入strStack
  • 最后从底向顶扫描strStack,依次组成返回结果

解题代码

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
func decodeString(s string) string {
numStack := make([]int, 0) // 记录需要重复的次数
strStack := make([]string, 0) // 记录待解码的字符串
i := 0 // 扫描字符串的下标
for i < len(s) {
if '0' <= s[i] && s[i] <= '9' { // 若扫描到数字
k := int(s[i]-'0') // 重复次数k
j := i+1
for ; j < len(s) && '0' <= s[j] && s[j] <= '9'; j++ { // 转化为int型
k = k*10 + int(s[j]-'0')
}
// 重复次数入栈
numStack = append(numStack, k)
// 更新下标
i = j
} else if s[i] == ']' { // 扫描到右括号,需要解码
var subStr string // 待解码的字符串
// 依次出栈,直到遇到左括号
cur := strStack[len(strStack)-1]
for cur != "[" {
subStr = cur + subStr
strStack = strStack[:len(strStack)-1]
cur = strStack[len(strStack)-1]
}
strStack = strStack[:len(strStack)-1] // 弹出左括号
// 弹出一个重复数字
k := numStack[len(numStack)-1]
numStack = numStack[:len(numStack)-1]
// 重复k次,加入到栈中
var encoded string
for ; k > 0; k-- {
encoded = encoded + subStr
}
strStack = append(strStack, encoded)
i++
} else { // 扫描到字母、左括号时直接入栈
strStack = append(strStack, string(s[i]))
i++
}
}

var res string // 返回结果
// 从底向顶扫描栈,依次加入结果
for i := 0; i < len(strStack); i++ {
res += strStack[i]
}

return res
}

手撕堆排序

手撕堆排序

解题思路

堆排序过程:

  • 首先建成大根堆:从最后一个分支节点开始,向上调整
  • 依次将大根堆的根节点与最后一个节点交换,使大根堆的长度减一,再从新的根节点开始向下调整;直到大根堆只剩一个元素

解题代码

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
func sortArray(nums []int) []int {
len := len(nums)
heapSort(nums, len) // 堆排序

return nums
}

func heapSort(nums []int, len int) {
// 建成大根堆
buildHeap(nums, len)

for len > 1 {
// 大根堆的根节点放到最后
nums[0], nums[len-1] = nums[len-1], nums[0]
// 剩余节点重新调整为大根堆
len--
adjestHeap(nums, 0, len)
}

}

func buildHeap(nums []int, len int) {
for i := (len/2)-1; i >= 0; i-- { // 从最后一个分支节点开始,依次向上调整堆
adjestHeap(nums, i, len)
}
}

func adjestHeap(nums []int, i, len int) { // 以nums[i]为根节点调整成大根堆
for 2*(i+1) <= len { // 自上而下调整
leftChild := 2*(i+1)-1 // 左孩子下标
rightChild := leftChild+1 // 右孩子下标
var larger int // 左右孩子中较大者的下标
if rightChild < len && nums[rightChild] > nums[leftChild] {
// 右孩子较大
larger = rightChild
} else {
// 左孩子较大
larger = leftChild
}
if nums[i] < nums[larger] {
// 孩子节点更大,则交换
nums[i], nums[larger] = nums[larger], nums[i]
}
i = larger
}
}

二叉搜索树的第k大节点

二叉搜索树的第k大节点

解题思路

求出右子树的节点个数rightNodeNum

  • k == rightNodeNum+1当前根节点就是第k大的节点,返回根节点的值
  • k <= rightNodeNum,说明第k大的节点在右子树中,递归寻找右子树的第k大节点
  • 否则第k大的节点在左子树中,递归寻找左子树的第(k-1-rightNodeNum)大的节点

解题代码

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
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func kthLargest(root *TreeNode, k int) int {
rightNodeNum := nodeNum(root.Right) // 右子树节点个数
if k == rightNodeNum+1 {
// 当前根节点就是第k大节点
return root.Val
} else if k <= rightNodeNum {
// 第k大节点在右子树中
return kthLargest(root.Right, k)
} else {
// 第k大节点在左子树中
// 在左子树中找第(k-1-rightNodeNum)大的节点
return kthLargest(root.Left, k-1-rightNodeNum)
}
}

func nodeNum(root *TreeNode) int { // 求以root为根节点的树的节点总数
if root != nil {
left := nodeNum(root.Left) // 左子树的节点个数
right := nodeNum(root.Right) // 右子树的节点个数

return 1+left+right
} else {
return 0
}
}

二叉树的后序遍历

二叉树的后序遍历

解题思路

  1. 递归法
  2. 非递归法

解题代码

  1. 递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func postorderTraversal(root *TreeNode) []int {
res := []int{}
var postorder func(root *TreeNode)
postorder = func(root *TreeNode) {
if root != nil {
postorder(root.Left)
postorder(root.Right)
res = append(res, root.Val)
}
}
postorder(root)

return res
}
  1. 非递归
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
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func postorderTraversal(root *TreeNode) []int {
res := make([]int, 0)
stack := make([]*TreeNode, 0)
cur := root
var pre *TreeNode

for len(stack) > 0 || cur != nil {
for cur != nil {
stack = append(stack, cur)
cur = cur.Left
}
if len(stack) > 0 {
cur = stack[len(stack)-1]
if cur.Right == nil || pre == cur.Right {
// 若栈顶节点右子树为空,或者刚刚遍历过右子树
// 遍历栈顶节点并弹出
res = append(res, cur.Val)
pre = cur
stack = stack[:len(stack)-1]
cur = nil
} else {
// 否则进入右子树
cur = cur.Right
}
}
}

return res
}

接口

接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力

概述

接口是抽象的数据类型,仅仅定义了行为(方法)。一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。

方法

方法是与类型相关联的函数

方法声明

一个方法的声明如下,其中参数p,叫做方法的接收器(receiver):

1
2
3
4
5
6
7
8
9
10
import "math"

type Point struct {
X, Y float64
}

// 一个方法的声明
func (p Point) Distance(q Point) float64 {
return math.Htpot(q.X-p.X, q.Y-p.Y)
}

可以给同一个包内的任意命名类型(基础数据类型需要起别名)定义方法,只要这个命名类型的底层类型不是指针或者interface

指针类型的接收器

也可以定义接收器对象为指针类型,这样可以避免拷贝、或者更新接收器对象:

1
2
3
4
5
// 接收器为指针类型
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}

注意:

  • 不管方法的接收器是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会自动做隐式类型转换

  • nil可以作为接收器

嵌入结构体扩展类型

可以嵌入其他结构体,嵌入匿名结构体时,可以直接访问叶子属性而不需要给出完整的路径、也可以拥有匿名结构体的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
type Point struct{
X, Y float64
}

type RGBA struct {
R, G, B, A uint8
}

type ColoredPoint struct {
Point // 嵌入匿名结构体
Color color.RGBA
}

同时,可以把ColoredPoint类型当作接收器来调用Point里的方法,即使ColoredPoint里没有声明这些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}

var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}

fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"

/*
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
*/

一个ColoredPoint并不是一个Point,但他**”has a”Point**,并且它有从Point类里引入的DistanceScaleBy方法。Distance函数的参数类型是Point,所以必须显式地选择q.Point

方法值和方法表达式

方法值

常用p.Distance()调用方法,实际上将其分成两步来执行也是可能的。p.Distance叫作选择器,选择器会返回一个方法值:一个将方法Point.Distance绑定到特定接收器变量的函数。

这个函数可以不通过指定其接收器即可被调用,因为已经在前文中指定过了,只要传入函数的参数即可

1
2
3
4
5
6
7
8
9
10
11
12
p := Point{1, 2}
q := Point{4, 6}

distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)

scaleP := p.ScaleBy // method value
scaleP(2) // p becomes (2, 4)
scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)

方法表达式

当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数”值”,这种函数会将其第一个参数用作接收器

1
2
3
4
5
6
7
8
9
10
11
p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"

当你根据一个变量来决定调用同一个类型的哪个函数时,方法表达式就显得很有用了。可以根据选择来调用接收器各不相同的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Point struct{ X, Y float64 }

func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }

type Path []Point

func (path Path) TranslateBy(offset Point, add bool) {
var op func(p, q Point) Point
if add {
op = Point.Add
} else {
op = Point.Sub
}
for i := range path {
// Call either path[i].Add(offset) or path[i].Sub(offset).
path[i] = op(path[i], offset)
}
}

封装

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为封装

Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种基于名字的手段使得在语言中最小的封装单元是package,而不是像其它语言一样的类型,如一个struct类型的字段对同一个包的所有代码都有可见性。

封装的好处

  • 隐藏实现细节

  • 可以对数据(某些字段)进行验证,保证安全合理

封装的步骤

  1. 将结构体、字段的首字母小写,使之不能导出
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写,类似于构造函数
  3. 提供访问getter或修改setter内部变量的函数
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
package person

import "fmt"

// 定义不能导出的结构体
type person struct {
name string
age int
sal float64
}

// 定义工厂模式的函数 首字母大写 类似构造函数
func NewPerson(name string) *person{
return &person{
name:name,
}
}

// 提供一个setter 设置年龄
func (user *person) Setage(age int) {
if age >0 && age < 150 {
user.age = age
}else {
fmt.Println("年龄数值不对!")
}
}

// 获取年龄
func (user *person) Getage() int{
return user.age
}

// 更改姓名
func (user *person) Updatename(name string) string{
user.name=name
return user.name
}

SQL查询优化大致上可以分为两种

  • 物理查询优化:通过索引表连接方式等技术进行优化
  • 逻辑查询优化:通过SQL等价变换来提升查询效率

索引创建的基本原则

联合索引优于单值索引

WHERE过滤条件中有涉及到多个列,那么对多个列创建联合索引的查询效率高于对单个列创建的索引查询效率

最佳左前缀法则

在MySQL建立联合索引时,会遵循最佳左前缀匹配原则,在检索数据时从联合索引的最左边开始匹配。也就是说,过滤条件要使用索引必须按照索引建立时的顺序,一旦某个顺序与联合索引的顺序不匹配,后面的字段无法使用索引

引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。

阅读全文 »

搜索二维矩阵 II

搜索二维矩阵 II

解题思路

  1. 对每一行进行二分查找

  2. 从二维矩阵的右上角开始,会发现每次都是向左数字会变小,向下数字会变大,有点和二分查找树相似。所以可以把target与当前值进行比较:

    • 如果target大于当前值,向
    • 如果target小于当前值,向
    • 如果target等于当前值,返回true
阅读全文 »

函数

错误处理

对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息

如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为ok

1
2
3
4
5
value, ok := cache.Lookup(key)

if !ok {
// ...cache[key] does not exist…
}

通常,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息。因此,额外的返回值不再是简单的布尔类型,而是error类型

在Go中,函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常(exception),这使得Go有别于那些将函数运行失败看作是异常的语言。

在Go中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在else语句块中,而应直接放在函数体中。Go中大部分函数的代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑

错误处理策略

当一次函数调用返回错误时,调用者有应该选择何时的方式处理错误。根据情况不同,有5种不同的处理策略。

  1. 传播错误,这是最常用的方式。函数中某个子程序的失败,会变成该函数的失败。可以用fmt.Errorf函数重新构造错误信息返回
1
2
3
4
5
resp, err := http.Get(url)
if err != nil{
// 执行失败,直接返回
return nil, err
}
  1. 如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// WaitForServer attempts to contact the server of a URL.
// It tries for one minute using exponential back-off.
// It reports an error if all attempts fail.
func WaitForServer(url string) error {
const timeout = 1 * time.Minute
deadline := time.Now().Add(timeout)

for tries := 0; time.Now().Before(deadline); tries++ {
_, err := http.Head(url)
if err == nil {
return nil // success
}
log.Printf("server not responding (%s);retrying…", err)
time.Sleep(time.Second << uint(tries)) // exponential back-off
}

return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}
  1. 如果错误发生后,程序无法继续运行,可以输出错误信息并结束程序。需要注意的是,这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。
1
2
3
4
5
6
7
// (In function main.)
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
// 以上两条等价于
// log.Fatalf("Site is down: %v\n", err)
}
  1. 我们只需要输出错误信息就足够了,不需要中断程序的运行。我们可以通过log包提供函数或者标准错误流输出错误信息
1
2
3
4
if err := Ping(); err != nil {
log.Printf("ping failed: %v; networking disabled",err)
// fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
}
  1. 直接忽略错误

文件结尾错误

io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在io包中定义:

1
var EOF = errors.New("EOF")

调用者只需通过简单的比较,就可以检测出这个错误。下面的例子展示了如何从标准输入中读取字符,以及判断文件结束。

1
2
3
4
5
6
7
8
9
10
11
12
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // finished reading
}
if err != nil {
return fmt.Errorf("read failed:%v", err)
}
// ...use r…
}

因为文件结束这种错误不需要更多的描述,所以io.EOF固定的错误信息——“EOF”。对于其他错误,我们可能需要在错误信息中描述错误的类型和数量,这使得我们不能像io.EOF一样采用固定的错误信息

函数值

在GO语言中,函数像其他值一样,拥有类型、可以被赋值给其他变量。

函数值属于引用类型

函数类型的零值是nil,函数值是可以和nil进行比较的。但是函数值之间是不可比较的(也就是说,函数值不能作为map的key)

1
2
3
4
var f func(int) int
if f != nil {
f(3)
}

函数值作为函数的参数可以定义函数的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func f(pre, post func()) {
if pre != nil {
// 在执行f之前调用pre()
pre()
}
/*
中间的函数体
.....
*/
if post != nil {
// 在执行f之后调用post
post()
}
}

匿名函数

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数

更为重要的是,通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}

func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}

squares的例子证明,函数值不仅仅是一串代码,还记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量,这意味着匿名函数和squares中,存在变量引用。这就是函数值属于引用类型函数值不可比较的原因。

同时,这个例子说明了,变量的生命周期不由它的作用域决定:squares返回后,变量x仍然隐式的存在于f中。

当匿名函数需要被递归调用时,我们必须首先声明一个变量,再将匿名函数赋值给这个变量:

1
2
3
4
5
6
7
8
var preorder func(root *TreeNode)
preorder = func(root *TreeNode) { // 二叉树的前序遍历
if root != nil {
visit(root)
preorder(root.Left)
preorder(root.Right)
}
}

可变参数

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数:

1
2
3
4
5
6
7
8
9
10
11
func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}

fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"

调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数。如果原始参数已经是切片类型,只需在最后一个参数后加上省略符

1
2
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的

1
2
3
4
func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n", f) // "func(...int)"
fmt.Printf("%T\n", g) // "func([]int)"

defer语句

当defer语句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反

释放资源

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。如文件操作:

1
2
3
4
5
6
7
8
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
defer f.Close()
if err != nil {
return nil, err
}
return ReadAll(f)
}

记录何时进入、退出函数

调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。下面的例子中,bigSlowOperation函数开始时会执行trace函数,当bigSlowOperation退出前会执行trace返回的函数。只需要一条语句,就可以控制住函数的入口和出口

1
2
3
4
5
6
7
8
9
10
11
12
13
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the extra parentheses
// ...lots of work…
time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}

func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg,time.Since(start))
}
}

观察、修改返回值

defer语句中的函数会在return语句更新返回值变量后再执行,又因为在函数中定义的匿名函数可以访问该函数包括返回值变量在内的所有变量,所以,对匿名函数采用defer机制,可以使其观察函数的返回值:

1
2
3
4
5
6
7
8
func double(x int) (result int) {
defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
return x + x
}

_ = double(4)
// Output:
// "double(4) = 8"

被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值:

1
2
3
4
5
func triple(x int) (result int) {
defer func() { result += x }()
return double(x)
}
fmt.Println(triple(4)) // "12"

异常处理

panic异常

当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。


panic异常和其他语言异常的不同:

虽然Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。

对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制,即返回一个额外的返回值,通常是最后一个,来传递错误信息

recover捕获异常

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。举个例子,当web服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭;如果不做任何处理,会使得客户端一直处于等待状态。

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

1
2
3
4
5
6
7
8
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}

应该有选择性的recover。换句话说,只恢复应该被恢复的panic异常。为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为error处理,如果不是,则按照正常的panic进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func f() (res string, err error) {
type someKindError struct{}
defer func(){
switch p := recover(); p {
case nil: // no panic
case someKindError{}: // expected panic
err = fmt.Errorf("Take place someKindError.....")
default:
panic(p) // unexpected panic
}
}()
// 其他工作
// ....
if 某种类型的异常发生了{
panic(someKindError{})
}
}

Map

GO语言中,map是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value

一个map就是一个哈希表的引用,所以map作为函数的参数时,是引用传递的。map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。

内置的delete函数可以删除元素(元素不在map中也没关系):

1
delete(hashMap, "dawn")	// 从map中移除key为dawn的元素

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作。禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效:

1
p := &hashMap["dawn"]	// error

和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。

遍历顺序随机

map遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序

1
2
3
4
5
6
7
8
9
10
11
var names []string

for name := range ages {
names = append(names, name)
}
// 显式的对key排序
sort.Strings(names)
// 依据顺序的key对map顺序遍历
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}

判断元素是否存在

通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值。如果需要区分已存在的零值和不存在的零值,可以进行以下操作,第二个布尔值ok用于报告元素是否真的存在

1
2
3
if age, ok := ages["dawn"]; !ok {
// ...
}

绕过key必须是可比较的限制

有时候需要map的key是slice类型,但是map的key必须是可比较的,可以通过两步来绕过此限制:

  • 定义一个函数k,将slice转为string类型,作为map的key来使用
  • 创建一个key为string的map,在每次对map操作时先用k辅助函数将slice转化为string类型
1
2
3
4
5
6
7
func k(list []string) string {	// 将slice转为string
return fmt.Sprintf("%q", list)
}

var hashMap = make(map[string]int)
keySlice := []string{/* ... */}
hashMap[k(keySlice)]++ // 对map操作时先将slice转为string

使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。

结构体

结构体是一种聚合数据类型,结构体类型的零值是每个成员都是零值。

同时结构体在作为函数参数时默认是值传递的,也就是说函数中使用的是结构体的拷贝

结构体的比较

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。在相等比较时,将比较两个结构体的每个成员

所以,可比较的结构体可以作为map的key类型

1
2
3
4
5
6
type address struct {
hostname string
port int
}

counts := make(map[address]int) // address结构体作为key

结构体嵌入和匿名成员

一个结构体可以嵌入另一个结构体中,使得结构体的类型变得清晰了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 坐标点
type Point struct {
X, Y int
}
// 圆
type Circle struct {
Center Point // 圆心
Radius int // 半径
}
// 轮子
type Wheel struct {
Circle Circle
Spokes int // 径向辐条的数量
}

但是这会使得访问每个成员变得繁琐:

1
2
3
4
5
var w Wheel
w.Circle.Center.X = 1
w.Circle.Center.Y = 1
w.Circle.Radius = 2
w.Spokes = 10

所以可以使用匿名成员:

1
2
3
4
5
6
7
8
type Circle struct {
Point // 匿名结构体
Radius int
}
type Wheel struct {
Circle // 匿名结构体
Spokes int
}

得意于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

1
2
3
4
5
var w Wheel
w.X = 1
w.Y = 1
w.Radius = 2
w.Spokes = 10d

简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法