2020 生活规划

指导原则

适当精简,包括日常生活用品,生活中从事的事情,工作中要处理的事情,自己阅读、观影等时间话费,感情方面也是重要一环,把精力放在有价值的关系上。

  • 身体健康
  • 读书、读论文
  • 多游览各方
  • 创作(文学、摄影)

爱丁堡租房

生活就是一场旅行,这算是一个重大决定。到爱丁堡工作已经一周,时差基本倒过来了。
第一周住在酒店,最紧迫的事情就是租房,因为后续用来发工资的银行卡、办理警察局的手续都需要一个租房地址。

出国前了解过这边的租房市场,主要通过 Zoopla 或者 Rightmove 等软件。不过都是隔岸观火,还是要实地考察才靠谱。
这边看房不像国内有一个看房小哥,一下午能带你看十几套房子。
我在软件上看到想去瞧一下的房子,都是要打电话去预约、APP 内发消息等。
等收到电话回访或者邮件回复后,约定一个看房时间(viewing arrangement),然后去看房。
一般会有很多人一起看房,热门的房型可能会有十几人。不过中介机构也不会无限量地拉人看房,有时就告诉你没有看房名额了。
等看房满意后就可以邮件或者网站填写申请,如果房主(landlord)同意,那么就可以交定金成交了。

房租一般根据地理位置、装修等价格会有一些差别。
本来最初的目标是租一个一人间,比较难租到。后来和同事商量后,租了一个两人间。

听同事讲爱丁堡房产的租售比不错,也许可以考虑在这边投资一下房子。

To Bangkok

普通落地签证/Visa on Arrival (VOA)

中国公民适用普通落地签政策,相关政策链接:

http://www.thaiembassy.com/thailand/visa-on-arrival.php

  1. 《申请表》(Application forVisa on Arrival)、1张白/蓝底4*6cm照片、15天内往返机票、有效期超过6个月的护照。(注:表格可下载并事先用英文填好;在泰如无联系人,可填写拟住酒店地址、电话)。
  2. 签证申请费每人1000铢。现场拍照另交费。申请落地签时,需备1万铢、每个家庭2万铢或等值外币以证明旅游期间有经济能力。泰移民官将抽查。

落地签需要的条件

  • 持有泰国政府批准国家的护照者。
  • 护照必须是真实的,不低于 30 天有效期。
  • 泰国旅游的期限不超过 15 天。
  • 有到达泰国时,不超过 15 天的回程机票。
  • 报实际和能查询的泰国地址。
  • 非佛历 2522 年的入境黑名单者
  • 签证费仅收泰铢(现金)/不可退款
  • 入境时个人必须随身携有外币不低于 10,000 泰铢, 家庭不低于 20,000 泰铢

参考:https://ask.qyer.com/question/788823.html

另外,2019/01/14 到 2020/04/30 落地签时免费的。

签证有效期

落地签有效期15天(包括入境当天),如为以旅游为目的则不可延期。离境时签证过期需向移民局缴纳罚款,1天500铢,最高不超2万铢。

电子落地签证/E-Visa on Arrival(E-VOA)

根据实际经验,落地泰国后如果走普通落地签的通道,队伍比较长,特别是航班密集落地的时候。
还有一种可以在网上预先申请的电子落地签,较为方便。

优点:

  1. 仅需要护照信息
  2. 不需要提供实体照片

费用:普通落地签费用 + 提供服务的公司的服务费用

参考:

  1. 不排队,1分钟快速入境!泰国电子落地签证(EVOA)申请攻略
  2. 不要办e-visa落地签,没有任何意义- 泰国- 论坛- 穷游网

有两家公司可以提供这项服务:

  1. VFS Global: http://www.evisathailand.com
    • 普通服务:525 THB
  2. evisathailand: https://thailandevoa.vfsevisa.com
    • 普通服务:600 THB
    • 快速服务(24 小时出结果):2500 THB

实际操作

使用 https://thailandevoa.vfsevisa.com 申请,交了普通服务费 600 THB 。

换汇

  1. 机场 ATM 每次要收 150B 手续费
  2. 国内银行预约(https://ask.qyer.com/question/3452511.html)
    • 国内一般只要提前一天预约就可以,不一定要本人换,其他人也可以帮你换,如果分行储备很充足,当天也可以换到,你试试不一定来不及吧
    • 机场也有换,就是汇率不是很划算,如果担心那边没人可以再出发的机场换,你早点到机场,先换一点够用就行,剩下你过了关再换。
    • 华夏银行卡 http://wenzhang.16fan.com/a/98638.html
  3. 换汇处,之后去 super rich 换汇率最划算

实际操作

就在机场换了一点,汇率不太好。
同伴从其他地方换了泰铢使用,我主要负责各种信用卡付款,所以最后使用现金也不是太多。

实用信息

  1. VFS Global 和 evisathailand 是两个不同的电子落地签代办公司。这两家公司在素万那普机场有各自独立的窗口,在普通的落地签证队伍旁边。我深夜到达的时候人很少,只有几个人在电子落地签队伍,入关很快。
  2. VFS Global 最后打出来的纸上没有二维码,evisathailand 的有。
  3. 入关只需要那张纸、护照和入境卡三样东西。
  4. Grab 很方便,到市区 300 多泰铢吧。
  5. 中国银行长城 Visa 白金信用卡汇率还不错。
  6. 不要尝试曼谷的中餐馆,贵而且不好吃。
  7. 大皇宫人很多,暹罗博物馆还不错,学生 50 泰铢(英文学生证就可以),其他外国人 200 泰铢。

Go to Rust (一)

这几天看了 Rust 文档,把一些概念整理一下。

  • 通过 cargo 新建一个项目,然后去管理其生命流程,这种现代做法很方便。
  • 对于 statementexpression 的使用方式和 scheme 有些类似,可以返回最后一个 expression 的值。
  • 可能返回错误的地方使用 Result 类型,很类似 Haskell 的处理流程。
  • 模式匹配的方式很像 Ocaml 。
  • ownership 机制很新颖,限制能够带来巨大的力量。让我想到了《全职猎人》中对某项能力增加限制条件可以增加这个能力的威力的设定。
  • Option 类型就是 Haskell 的 Maybe
  • generics 的设计不知道有没有参考 C++ 的 template 概念。

目前只看到文档的第 11 章,Rust 语言的很多概念都能够在其他语言找到对应,只有 ownership 机制是我第一次见到,觉得新颖有趣。

pip 离线安装包

1. 使用场景

在没有网络的设备上使用 pip 安装包。下面以 sklearn 包为例展示如何在没有网络的环境下安装包。

2. 下载包到本地缓存

首先进入一个目录,在这个例子里是 /Users/bef0rewind/Downloads/pip-tmp 目录。

1
pip download sklearn

我这里下载到了一个缓存目录 /Users/bef0rewind/Downloads/pip-tmp,随便选一个就好。pip download 只会下载对应的包,不会进行安装。

此时使用,pip freeze 可以看到已经安装的包,如果之前没有安装过 sklearn,显示的列表里是没有这个包的。

3. 断网安装

为了展示没有网络的情况下如何安装,我断开网络进行了验证。

1
pip install --no-index --find-links=/Users/bef0rewind/Downloads/pip-tmp sklearn

其中 --find-linkspip 从指定的目录里寻找安装包。

4. 其他

如果要用 Python3,而系统默认的版本是 Python 2,则可以将 pip 命令换成 pip3

还在下雨

早上闹钟还没响,就收到学弟从美东发来的微信消息,高管在加拿大被扣留,感觉刚释放的贸易战缓和信号瞬间消失无影。接着看到同学发送的新闻,张首晟教授去世,又给学弟把这则消息转了过去。

到了公司也没获得更多信息。一天工作下来,解决了几个问题,心情是近来最平静的一天。雨一直下个不停,沿着玻璃幕墙下来,汇成一条条水线流下。

各类信息媒介上也是消息攒动,同样没获得更多有效信息。前段时间看到一个问题:为什么感觉今年逝去的名人特别多?我想,也许是他们的时代逝去了,也许是我们的时代逝去了。

小学、初中、高中的时候喜欢数学和诗歌。现在对一些人向往的“诗意的栖居”这种描述不怎么向往了。在这个世界,不可能所有人都能到达那种美好的彼岸。现实还是要战斗,在枪炮中让玫瑰绽放。

有观点说外面的世界再乱,只要自己的小环境能保持美好就够了。我也不知道要怎么选择,一是我不知道什么叫好的小环境,而是我可能也没能力隔绝外界的干扰,也就无所谓选择。

晚上从公司回来,雨还是在下,风吹在手上冻得不行。

寒冷 / 困顿 / 饥饿
树影 / 摇曳 / 零落
混乱 / 猜测 / 迷惑
启程 / 归来 / 此刻

Escape from escape analysis

1. 逃逸分析背景

Go 语言采用了并发的(Concurrent)、非移动的(Non-Movable)、非分代的(Non-Generational)、基于三色(Tri-color)标记的垃圾回收(Garbage Collection)算法,只在 特定阶段开启写屏障(write barrier)。
特点是全局停顿时间比较少,在一些场景下是十微秒级别的。

垃圾回收算法针对的是堆(heap)中的内存。
为了减少垃圾回收的时间消耗,Go 语言在编译阶段通过静态分析算法对程序的结构进行分析,尽可能讲对象分配在栈上(如果这个对象的生命周期在它定义的函数返回时就结束的话)。
这一算法也利用了 Go 语言在函数传递参数时总是传递参数的值这一个语言特性。

而静态分析不总是完备的,会有一些本来可以分配在栈上的对象被 Go 的编译器分配在了堆上。
如这篇文章《Golang escape analysis》所描述的一些例子一样,有些对象本来可以避免逃逸(Escape,指的是对象被分配在堆上)。

对于某些场景,我们确定一个对象肯定可以(也应当)被分配在栈上,但是它却逃逸了。
这样在某些关键路径上的逃逸的对象会造成大量的分配和垃圾回收。

2. Go 版本

使用的 Go 版本为今晚刚从 master 分支上 pull 下的源码直接构建。

1
2
ThinkPad-X1-Carbon:bin bef0rewind$ ./go version
go version devel +42e8b9c3a4 Fri Nov 30 15:17:34 2018 +0000 darwin/amd64

3. 示例

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
// file: escape.go
package main

import "fmt"

type BigTempObject struct {
/// ...
field1 int
}

func causeEscape(i interface{}) {
switch i.(type) {
case *BigTempObject:
println(i)
default:
fmt.Println(i)
}
}

func main() {
obj := BigTempObject{}
addrObj := &obj

causeEscape(addrObj)
}

使用 go run -gcflags="-m -m" escape.go 可以在运行时输出逃逸分析的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
./escape.go:10: cannot inline causeEscape: unhandled op TYPESW
./escape.go:19: cannot inline main: non-leaf function
./escape.go:10: leaking param: i
./escape.go:10: from ... argument (arg to ...) at ./escape.go:15
./escape.go:10: from *(... argument) (indirection) at ./escape.go:15
./escape.go:10: from ... argument (passed to call[argument content escapes]) at ./escape.go:15
./escape.go:15: causeEscape ... argument does not escape
./escape.go:23: addrObj escapes to heap
./escape.go:23: from addrObj (passed to call[argument escapes]) at ./escape.go:23
./escape.go:21: &obj escapes to heap
./escape.go:21: from addrObj (assigned) at ./escape.go:21
./escape.go:21: from addrObj (interface-converted) at ./escape.go:23
./escape.go:21: from addrObj (passed to call[argument escapes]) at ./escape.go:23
./escape.go:20: moved to heap: obj
(0x10904e0,0xc420080050)

obj 可以分配在栈上,因为在 main 函数返回时(栈退出),这个变量占用的空间就可以安全被用在其他地方了。
但是 “./escape.go:20: moved to heap: obj” 说明 obj 被分配在了堆上。

4. 小技巧

如何改变这个分析结果,需要一点小技巧。

关键词是 uintptr 类型。
Go 语言中对 uintptr 是这样描述的:

uintptr is an integer type that is large enough to hold the bit pattern of any pointer.

比如在 64-bit Linux 系统上 uintptr 被定义成为了 uint64
Go 中合法的类型转换为:normal pointerunsafe.Pointeruintptr
因此我们可以把上面的程序中的 addrObj 转换为 uintptr
这样 Go 编译器不再认为 addrObj 同后面函数 causeEscape 使用的参数 i 存在引用关系,从而绕过 Escape Analysis Algorithm 。
为了防止垃圾回收过程中 obj 被回收,可以使用 obj.field1 = 0 来保持 obj 活跃。

修改后的代码如下:

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
package main

import (
"fmt"
"unsafe"
)

type BigTempObject struct {
/// ...
field1 int
}

func causeEscape(i interface{}) {
switch i.(type) {
case *BigTempObject:
println(i)
default:
fmt.Println(i)
}
}

func main() {
obj := BigTempObject{}
addrObj := &obj
intAddr := uintptr(unsafe.Pointer(addrObj))
causeEscape((*BigTempObject)(unsafe.Pointer(intAddr)))
obj.field1 = 0
}

使用 go run -gcflags="-m -m" escape.go 运行结果:

1
2
3
4
5
6
7
8
9
10
11
./escape.go:13: cannot inline causeEscape: unhandled op TYPESW
./escape.go:22: cannot inline main: non-leaf function
./escape.go:13: leaking param: i
./escape.go:13: from ... argument (arg to ...) at ./escape.go:18
./escape.go:13: from *(... argument) (indirection) at ./escape.go:18
./escape.go:13: from ... argument (passed to call[argument content escapes]) at ./escape.go:18
./escape.go:18: causeEscape ... argument does not escape
./escape.go:26: (*BigTempObject)(unsafe.Pointer(intAddr)) escapes to heap
./escape.go:26: from (*BigTempObject)(unsafe.Pointer(intAddr)) (passed to call[argument escapes]) at ./escape.go:26
./escape.go:24: main &obj does not escape
(0x10904e0,0xc42003bf70)

可以看到 obj 不再逃逸,主要是 intAddr 中断了逃逸分析算法构建的指针依赖关系(表示为一个有向图)。

5. 一点感想

我们可以做到不代表一定去做,有风险也不代表禁区,采取什么样的行动是个人权衡后的选择。
什么原因导致了人们做了不同的选择,而人们不同的选择又导致了什么结果?
多样性是这个世界的现状,黑暗面与光明面同在。
May the force be with you.

Golang Receiver Type 探索

1. 参考

在 Go 的官方 spec 中有以下涉及到类型和方法的章节,如果需要了解具体的细节,可以参考阅读。

核心的概念是 method sets:

A type may have a method set associated with it. The method set of an interface type is its interface. The method set of any other type T consists of all methods declared with receiver type T. The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T). Further rules apply to structs containing embedded fields, as described in the section on struct types. Any other type has an empty method set. In a method set, each method must have a unique non-blank method name.

The method set of a type determines the interfaces that the type implements and the methods that can be called using a receiver of that type.

下面的一些细节基本上都和这段描述相关。

2. Duck typing 与方法调用

在很多面向对象的语言中,一个对象都可以“拥有”一些方法,使用例如 obj.f(a, b, c) 的形式进行调用。结合语言的类型系统,通过“扩展”、“继承”、“实现”等术语,我们可以将不同的类组织起来。在 Go 语言中采用的是 “duck typing”,没有显式的类型关系定义关键字。当一个类型实现了一个接口的全部方法时,那这个类型就被视为实现了这个接口。

例如:

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
package main

import "fmt"

type Duck interface {
Bark()
}

type A struct {}

type B struct {}

func (*A) Bark() {}

func main() {
var iA interface{} = &A{}
if _, ok := (iA).(Duck); ok {
fmt.Println("&A{} is Duck")
} else {
fmt.Println("&A{} is not Duck")
}

var iB interface{} = &B{}
if _, ok := (iB).(Duck); ok {
fmt.Println("&B{} is Duck")
} else {
fmt.Println("&B{} is not Duck")
}
}
1
2
&A{} is Duck
&B{} is not Duck

我们可以用原始的类型去调用一个方法,也可以使用一个接口去调用方法。这里就涉及到方法调用者的问题:什么样的对象是一个合法的方法调用者?

至少 A{} 不是,因为我们实现 Duck 接口的时候,使用的是 func (*A) Bark() 进行的定义,而非 func (A) Bark()。这样就导致了只有 A 类型对象的指针类型才能作为方法调用者去调用 Bark 方法。

3. 成员函数的参数

在实现中,调用某个类型的成员方法,第一个参数其实是这个方法的实现对象自身,即如果是一个指针的方法,就是这个指针的值,如果是一个对象,就是这个对象的值。

下面使用 Go 1.8.3 展示,因为当前最新的 Go 编译器在打印 stack trace 的时候不再打印函数的参数(这个例子中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

type R1 struct {
n int
}

func (r *R1) f(n int) {
println("received", n)
panic("just a panic")
}

func main() {
r := &R1{}
a := 1
println(r)
r.f(a)
}
1
2
3
4
5
6
7
8
9
0xc420039f70
received 1
panic: just a panic

goroutine 1 [running]:
main.(*R1).f(0xc420039f70, 0x1)
/Users/bef0rewind/Projects/net example/src/main/receiver_type.go:9 +0xa3
main.main()
/Users/bef0rewind/Projects/net example/src/main/receiver_type.go:16 +0x5a

Stack trace 中函数 f 第一个值是指针 r 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

type R1 struct {
n int
m int
}

func (r *R1) f(n int) {
println("received", n)
panic("just a panic")
}

func (r R1) g(n int) {
println("received", n)
panic("just a panic")
}

func main() {
r := R1{7, 9}
a := 1
println(r.n)
(r).g(a)
}
1
2
3
4
5
6
7
8
9
7
received 1
panic: just a panic

goroutine 1 [running]:
main.R1.g(0x7, 0x9, 0x1)
/Users/bef0rewind/Projects/net example/src/receiver_type/main/args.go:15 +0xa3
main.main()
/Users/bef0rewind/Projects/net example/src/receiver_type/main/args.go:22 +0x58

Stack trace 中函数 g 第一个值是 r 的值 79

从这个实现方式中我们可以推断以下几点:

  • Go 语言采用参数传值的方式进行函数调用,因此如果对象很大,使用的对象本身调用函数会带来大量的复制
  • 不可能在函数调用中改变函数外的调用者,因为传到函数内部的只是调用者的副本

4. 使用接口调用函数

基于这样的成员函数实现方式,我们可以尝试另外一种调用方式:使用接口类型调用一个函数。
这里不是将一个对象转换成特定的接口然后去调用函数,而是使用接口类型本身去进行函数调用。
这种方式在 Go 1.9 中开始支持,在 Go 1.10 开始写入 Go 的 specs。这个例子使用的是 Go master 分支的版本,可能是 Go 1.11。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

type M struct {

}

func (*M) f(n int) {
println("I;m M, with", n)
}

type IM interface {
f(n int)
}

func main() {
m := &M{}
IM.f(m, 7)
}
1
I;m M, with 7

此外还能使用匿名接口类型去调用函数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

type M struct {

}

func (*M) f(n int) {
println("I;m M, with", n)
}

func main() {
m := &M{}
interface{f(n int)}.f(m, 7)
}

运行结果与上面的一段采用 IM 接口定义的例子是一样的。

5. 注入依赖

有时候一个对象在实例化的时候,它的一些成员方法的行为可能还没有确定,需要依赖外界注入。此时我们可以在对象类型定义中内嵌一个接口,然后在后期传入一个接口的实例来确定其行为。

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
package main

type BinaryOp interface {
Compute(a, b int) int
}

type ComputeNode struct {
x, y int
BinaryOp
}

func (node *ComputeNode) Result() int {
return node.BinaryOp.Compute(node.x, node.y)
}

type Add struct {}

func (*Add) Compute(a, b int) int {
return a + b
}

type Multi struct {}

func (*Multi) Compute(a, b int) int {
return a * b
}

func main() {
node := &ComputeNode{x:2, y:3}

node.BinaryOp = &Add{}
println(node.Result())

node.BinaryOp = &Multi{}
println(node.Result())
}
1
2
5
6

注意一定要记得传入接口的实例,在这个例子中如果不给 node 传入一个 BinaryOp 接口实例,那 node.BinaryOpnil,在调用 Compute 方法的时候就会发生异常。例如将上面的 main 函数稍作修改:

1
2
3
4
5
6
func main() {
node := &ComputeNode{x:2, y:3}

//node.BinaryOp = &Add{}
println(node.Result())
}
1
2
3
4
5
6
7
8
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x104d8d7]

goroutine 1 [running]:
main.(*ComputeNode).Result(...)
/Users/bef0rewind/Projects/net example/src/receiver_type/main/injection.go:13
main.main()
/Users/bef0rewind/Projects/net example/src/receiver_type/main/injection.go:32 +0x47

6. 内部机制

内部机制有一些细节。大体就是一个接口 i 包含两部分内容(指针),一个是接口代表的方法的集合,一个实现这个接口的具体对象;而一个对象 obj,它包含了自己的内存中的值,也能通过其类型获取到 obj 实现的方法集合。

将这两个概念记住,在实现一些模式的时候就会少很多心智负担。

7. 总结

Go 语言的这套基于 “duck typing” 的机制好不好,争论有很多。不过我一向对这些争论没有特别的倾向,至少理解其机制之后按照其设计思路来用还可以正常使用,而且里面没有复杂的概念和例外情形。

也许我的理解有偏差,但现在还没有发现什么矛盾的地方。

defer, panic and recover in Golang

1. 什么是异常处理

程序在执行过程中有可能出现异常状态,比如获取一个不再有效指针指向的内容、除零等。
一般语言都提供了异常处理机制来应对这些情形,例如 Java 的 try/catch/finally 机制(https://docs.oracle.com/javase/tutorial/essential/exceptions/catch.html)、
Python 的 try/raise/except/finally 机制(https://docs.python.org/3/tutorial/errors.html)等。

2. Go 语言中的异常处理机制

Go 语言中使用的是 defer/panic/recover 机制来处理异常。Go 语言官方博客的《Defer, Panic, and Recover》讲述了这个机制的具体应用方式。

还有一些其他教程对这个机制的使用方法、适用场景进行了进一步阐述:

如果搜索 “golang 异常处理”,类似的教程有很多。里面的核心思想大体就是:用 defer + recover 处理一个 panicdefer 结构要在 panic 触发之前被定义而且 recover 要直接在在 defer 结构定义的函数中被调用(而不是被直接调用或者在函数内部的其他函数中被调用)。

3. defer 语法糖的部分原理

在讲述 defer 机制的文章中,都会提到一个函数中多个 defer 结构执行的顺序和定义顺序是相反的,即后定义的 defer 结构总是先被执行。为什么会出现这样的情况?例如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
func g(n int) {
println(n)
}

func h(str string) {
println(str)
}

func f() {
defer g(0)
defer h("h")
}

调用 f 输出为:

1
2
h
0

常见的函数调用流程为:

  • 将函数使用的参数压入栈
  • 执行函数指令
  • 函数执行结束返回到调用点

如果 defer 相关的代码也是这么执行的话,那么为什么不是: 0 入栈 - 执行 g - g 返回 - "h" 入栈 - 执行 h - h 返回 这个顺序呢?
按照这个顺序执行,调用 f 输出应该是 0h 前面符合预期。是不是 Go 语言中执行 defer 时采用了特殊的处理流程?

是,也不是。

太阳底下无新鲜事,defer 不过是一个语法糖,用来对一个函数 deferproc 进行包装。

1
2
3
4
// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval)

deferproc 创建一个延迟调用的函数,其参数为 siz (延迟调用的函数的参数占用的字节数量)和 fn(被延迟调用的函数本身)。
当 Go 程序的编译器遇到 defer f(),会将这条语句翻译为一条 deferproc 和一条 deferreturn
其中 deferproc 把被调用的函数及其参数挂载在 goroutine (Go 中的并发单元,协程)结构的一个链表上;
deferreturn 从链表上取下一个挂载的被延迟执行的函数,执行它。

如何使用技巧绕过 defer 关键字,模拟类似效果?
可以使用 linkname 方法来把 Go 语言运行时的一些关键函数导出,从而进行某些不常见的操作。

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
package main

import (
_ "runtime"
"unsafe"
)

type Eface struct {
_type uintptr
Data unsafe.Pointer
}

func EfaceOf(ep *interface{}) *Eface {
return (*Eface)(unsafe.Pointer(ep))
}

type Funcval struct {
fn uintptr
// variable-size, fn-specific data here
}

//go:linkname Deferproc runtime.deferproc
func Deferproc(siz int32, fn *Funcval)

//go:linkname Deferreturn runtime.deferreturn
func Deferreturn(arg0 uintptr)

func main() {
var f = func() {
println("hacked defer")
}
var fI interface{} = f

// Attach a defer struct to the current goroutine struct
Deferproc(0, (*Funcval)(EfaceOf(&fI).Data))

defer func() {
println("original defer")
}()

// Run a deferred function if there is one
Deferreturn(0)
}

这段代码会输出:

1
2
original defer
hacked defer

当然,如果是使用 defer 关键字,Go 语言的编译器会选择合适的位置插入 deferreturn 语句,而不是像上述代码中一样手动放在结束位置处。

4. recover 生效位置的设计原因推测

言归正传,panic 发生后,会根据函数调用顺序逐层上报,直到最后一层被抛出到系统导致崩溃或者被 recover 机制处理。
那么如果被 recover 处理,这个过程是怎么生效的?

很多教程中都提到 recover 一定要在 defer 声明的函数里面(既不是这个函数本身也不能是函数里面的其他函数里面)才能正确处理当前的 panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// case 1, not work
defer recover()

// case 2, not work
defer func() {
func() {
recover()
}
}()

// case 3, work
defer func() {
recover()
}()

为什么呢?

先不考虑实现,先从理念上分析一下。

  1. defer 直接作用于 recover():无法根据 recover() 的返回值来进行不同类型的 panic 处理
  2. 在被 defer 作用的函数内部的函数 g 中使用 recover():如果 g 是一个第三方库的函数,无法保证其中没有未知的 recover 意外处理了系统中的 panic

因此事实上也只能通过这样的约束来使这个异常处理机制看上去直观易处理一些。当然通过对 Go 编译器进行修改,还是有办法使得上面三种情况下 recover 都可以中断 panic 向上层传递过程的。

此外,由于被 defer 处理的函数被挂载在 goroutine 结构的一个链表上,因此当 panic 发生时,可以直接从这个链表上取下被延迟执行的函数一个个执行。
这也是 recover 要放在 deferred function 中的原因,因为这些函数是肯定可以执行到的。

5. 总结

不能说 Go 中这个异常处理机制有多高明,基本上属于现代语言标配。了解更多背后的原理,在使用时可以更坚定一些。

此外,最近看到一本书《最好的告别》(https://book.douban.com/subject/26576861/)。

Being Mortal

豆瓣上的介绍:

当独立、自助的生活不能再维持时,我们该怎么办?在生命临近终点的时刻,我们该和医生谈些什么?应该如何优雅地跨越生命的终点?对于这些问题,大多数人缺少清晰的观念,而只是把命运交由医学、技术和陌生人来掌控。影响世界的医生阿图•葛文德结合其多年的外科医生经验与流畅的文笔,讲述了一个个伤感而发人深省的故事,对在21世纪变老意味着什么进行了清醒、深入的探索。

defer / finally 这些关键字让我们可以控制函数退出时的行为,但是我们自身呢?也许考虑这些问题可以让我们自身活得有意义一些。

推荐大家看一下。

Useful Commands

Convert images to a video

1
ffmpeg -r 30 -start_number 3455 -i _IMG%d.jpg -s 960X600 -pix_fmt yuv420p 30fps-960.mov
  • -r 30: 30 frames per second
  • -s 960X600: resolution
  • -pix_fmt yuv420p: for OsX

youtube-dl video and extract audio file

youtube-dl --proxy socks5://127.0.0.1:1080 -x --audio-format mp3 youtube-url

virtualenvwrapper

  • WORKON_HOME: which directory your environments are created in
  • /usr/local/bin/virtualenvwrapper.sh: default location for its configuration file
  • mkvirtualenv test --python=python3: make a virtual environment ‘test’ with python3
  • rmvirtualenv test: remove a virtual environment ‘test’
  • workon test3 or lsvirtualenv -b test3: activate a virtual environment ‘test’
  • deactivate: exit current environment
  • more details: search engine
    • how to avoid globa packages
    • how to copy an environment

node && npm

npm complains: Error: Cannot find module 'process-nextick-args'

Uninstall node, brew uninstall node, then by this stackoverflow post:

1
2
3
sudo rm -rf /usr/local/bin/npm /usr/local/share/man/man1/node* /usr/local/lib/dtrace/node.d ~/.npm ~/.node-gyp 
sudo rm -rf /opt/local/bin/node /opt/local/include/node /opt/local/lib/node_modules
sudo rm -rf /usr/local/bin/npm /usr/local/share/man/man1/node.1 /usr/local/lib/dtrace/node.d

Just delete something, then brew install npm.

delve (dlv) tips

  • funcs [regexp] : get function list
  • call : call a function (in a newer a go version, dlv should be installed in the newer go version too)

shadowsocks

ssserver -c /etc/shadowsocks/config.json

sslocal and ssserver are all from apt-get install shadowsocks.

WSL 2 && VMware switch

After enabling WSL 2 on Windows 10 insider preview, VMware virtual machine is disabled.
This is because collision between Hyper-V and VMware.

Turn on VMware

1
bcdedit /set hypervisorlaunchtype off

Turn on Hyper-V && WSL 2

1
bcdedit /set hypervisorlaunchtype auto

reference: https://blog.minirplus.com/10268/