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

电池人生

电池,一般狭义上的定义是将本身储存的化学能转成电能的装置,广义的定义为将预先储存起的能量转化为可供外用电能的装置。因此,像太阳能电池只有转化而无储存功能的装置不算是电池。其他名称有电瓶、电芯,而中文池及瓶也有储存作用之意。

《维基百科》

5 月份上海校友会来杭州参观,我带队去云栖景区玩,后来大家一起去了阿里西溪园区。园区很漂亮,中间有一块地被高墙围起,据说马云在那里开董事会。后面有个讨论环节,那时我已经昏昏欲睡了,只记得那个阿里 P11 说今后公司要拓展的方向是娱乐和健康。

娱乐和健康都是人生活的必需品。

最近认识一些朋友去今日头条工作了。这个公司的应用通过算法构建消息流推送到人们的移动终端。在信息泛滥的今天,可以永远向下滑动手指,看到无尽的“新”闻。可算法知道,这些新闻的类型是你喜欢的,同样的主题是你的老朋友了。

当然这样的应用通常不需要你付费,因为你就是它们的商品。一次次的滑动中除了那些通过视网膜输入的新闻还有各式各样的广告,都是基于你的“兴趣”进行筛选后的结果。它们攫取你的时间卖给了肯花钱展示商品的人,赚到了差价。

以前看黑客帝国的时候,总是不明白 Matrix 那么发达,却还是需要将人关在营养仓里当做电池使用。人是消耗能量的啊!最近才逐渐有所领悟,人体不一定可以拿来产生能量。人作为进化的产物,智能依赖生物结构,Matrix 可以将一个个的人作为基础的计算组件,用来支撑虚拟世界中的各种计算。现在我们使用人工智能也是如此,用来识别人脸、用来预测天气 ……

《海伯利安》中有一个设定,机器智能帮助人类构建了可以在宇宙各处穿梭的跃迁节点,而这种技术远远超出了人类的理解范围。人们以为这是机器智能回馈给它们的“造物主”的礼物,但实际上这是一个陷阱。人穿梭过跃迁节点从另一侧出来并不是瞬间完成的。在节点中度过的无法感知到的时间中,人的大脑的计算能力被机器智能利用,完成了自身的不断进化,来追求它们的终极目标。

在阿西莫夫的经典作品《我,机器人》改编的电影中,威尔•史密斯扮演的黑人警探戴尔•斯普纳不相信人类能和机器人和平共处,故事的各种冲突也从此展开。这部作品的底线是机器人还遵守“机器人三定律”,虽然是被以某种人类意想不到的方式施行的。现实中,我们能对机器人有所期待吗?我们能对创造机器人的人有所期待吗?

总是有人对新的技术保持怀疑 —— 有些人因为未知而恐惧,有些人因为远见而忧虑,只有中间的人享受着生活。

有时候想人的一生好短暂,也就一百年。昨天看到淡豹的一篇文章《近日新闻有感》,中间有一大段对日常种种琐事的描述,读的过程中仿佛头被浸在水中。究竟是什么(强大的)力量在驱使我们生活?

在我看来这种力量里又加入了一方势力,有些人在尽最大努力使用着智能算法来侵占人的生命。以前他们也这样做,通过编辑人们喜欢的花边新闻和轶事来吸引人们注意,不过那种方式毕竟低效。现在算法可以让人们通过滑动手指,自己戴上镣铐,出卖生命。

小的时候,有段时间我很喜欢四驱车,当然是在看了《四驱小子》之后。终于有了一辆(无影剑)后,我发现了一个现实的问题:这车跑起来很费电池,于是又攒钱买充电电池。最终电池的能量变成了车在赛道上前进的动力以及我的回忆。

我对四驱车的欲望肯定是来自动画片,后来我弟弟那时候买的就是悠悠球了。人的大脑是可以被外界信息影响的,现代科技也会以某种方式对人的大脑进行编程。现在人脑运行的机制还不清楚,如果可行的话,那些广告商肯定想通过某种方式对你的大脑进行编程,让你看一遍广告就下单购物。

我前段时间在想:如果总是接受这样被算法(你的手指)选择过的信息,会变傻吗?至少我体会过那种无效信息过载的时刻,就是大脑没什么感觉了。

在这个过程中我们到底付出了什么呢?我们的时间,我们的未来。

随着时间的推移,人的未来就逐渐坍缩,直到生命终点,也就没有未来了。每一秒过去,我们的未来就少了几分可能性。

如果明天不值得期待,就继续燃烧自己吧。

Upgrade DigitalOcean's Ubuntu 17.04 to LTS version in 2018-06

What happened?

I found my proxy for accessing some websites stopped working today, so I had to change my VPS’s IP address.
After some trials, everything seemed OK and I started watching a skiing video made by NorthFace on Youtube.

Emmmm, I noticed 12 packages needed to be updated.
Well, I typed sudo apt-get update and got messages like this (I didn’t save the error messages then):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  404  Not Found [IP: 91.189.91.23 80]
Ign:10 http://us.archive.ubuntu.com/ubuntu zesty-backports InRelease
Err:11 http://us.archive.ubuntu.com/ubuntu zesty Release
404 Not Found [IP: 91.189.91.26 80]
Err:12 http://us.archive.ubuntu.com/ubuntu zesty-updates Release
404 Not Found [IP: 91.189.91.26 80]
Err:13 http://us.archive.ubuntu.com/ubuntu zesty-backports Release
404 Not Found [IP: 91.189.91.26 80]
Reading package lists... Done
E: The repository 'http://security.ubuntu.com/ubuntu zesty-security Release' does no longer have a Release file.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
E: The repository 'http://us.archive.ubuntu.com/ubuntu zesty Release' does no longer have a Release file.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
E: The repository 'http://us.archive.ubuntu.com/ubuntu zesty-updates Release' does no longer have a Release file.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
E: The repository 'http://us.archive.ubuntu.com/ubuntu zesty-backports Release' does no longer have a Release file.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.

Wow, It was the first time I encountered such things.
After some digging, I realized it was because Ubuntu 17.04 (zesty) was not supported any longer.

According to https://wiki.ubuntu.com/ZestyZapus/ReleaseNotes

Ubuntu 17.04 will be supported for 9 months until January 2018. If you need Long Term Support, it is recommended you use Ubuntu 16.04 LTS instead.

It means my current release version (Ubuntu 17.04) won’t receive any updates in the future.

release-end-of-life-date

How to upgrade

I was struggled to reveal some steps to upgrade my Ubuntu 17.04 (End-of-Life) to 18.04 (LTS version).

The official guide to upgrade from an Ubuntu release which reaches its “end of life” is: https://help.ubuntu.com/community/EOLUpgrades

1. Update sources.list

The path of sources.list is /etc/apt/sources.list.

1
2
3
4
5
6
7
8
## EOL upgrade sources.list
# Required
deb http://old-releases.ubuntu.com/ubuntu/ CODENAME main restricted universe multiverse
deb http://old-releases.ubuntu.com/ubuntu/ CODENAME-updates main restricted universe multiverse
deb http://old-releases.ubuntu.com/ubuntu/ CODENAME-security main restricted universe multiverse

# Optional
#deb http://old-releases.ubuntu.com/ubuntu/ CODENAME-backports main restricted universe multiverse

In my case, CODENAME should be replaced by zesty.

2. Upgrade

Try apt-get upgrade, and I got:

1
2
3
dpkg: dependency problems prevent configuration of ubuntu-standard:
ubuntu-standard depends on libpam-systemd; however:
Package libpam-systemd:amd64 is not configured yet.

By some method suggested by stackoverflowers, I finally got over it by these commands:

1
2
sudo dpkg --force-all -P libpam-systemd
sudo apt-get -f install libpam-systemd

3. Dist Upgrade

These are some normal steps for an upgrade.

1
2
sudo apt-get dist-upgrade
sudo do-release-upgrade

If you’re unlucky like me, you will not upgrade to a newer release completely.
You may see:

1
Package grub-efi-amd64 is not configured yet.

According to a question asked by someone, you can try this:

1
2
sudo dpkg --force-all -P grub-efi-amd64
sudo dpkg --force-all -P grub-efi-amd64-signed

Execute sudo apt-get update and sudo apt-get upgrade.

4. Thoughts

I first upgraded to Ubuntu 17.10 then Ubuntu 18.04.

To see if I have done the right things, I check the release version by sudo lsb_release -a:

1
2
3
4
Distributor ID:    Ubuntu
Description: Ubuntu 18.04 LTS
Release: 18.04
Codename: bionic

Is it the DigitalOcean’s customized release makes the upgrade so complex for a non-expert programmer or the Ubuntu release itself?

But apparently, I’m not the only one who is confused by the error messages and asks questions on the web. Luckily I’ve found helping answers I need.

The commands with words like force or -f make me feel anxious.

These violent delights have violent ends.
— by Shakespeare

Dolores

Anyway, it works.

2016 十一塞罕坝行动

这是很久之前应该出现的一篇日志,当时忙于处理照片、毕业论文之类的事情,加上强烈的懈怠情绪,一直没写。


2016 年暑假过后开学不久,政委喊大家出去骑车,这注定会很有趣。目的地是承德的塞罕坝,不是张家口的坝上草原。
主题是骑行、赏秋景。大家确认好时间后,大家定了火车票,提前去西直门北京北站托运了自行车。剩下的就是等待出行了。

塞罕坝国家森林公园位于承德北部,毗邻内蒙古(上面),是清代皇家猎苑一部分,集满蒙汉三民族文化。
景区内有七星湖、太阳湖、泰丰湖、白桦林、月亮湖等较大的景点和多个小景点,自然风光和人文遗迹于一体。
(注:这些湖我们一个也没去,人文遗迹也没看到)
另外,还珠格格就是在这个地方被他皇阿玛一箭射中的。

整体路线规划为:

  • 北京北 - 四合永镇 (火车)
  • 四合永镇 - 围场县 (骑车、住宿)
  • 围场县 - 机械林场 (骑车、住宿)
  • 机械林场 - 半截塔镇 (骑车、住宿)
  • 半截塔镇 - 隆化县 (骑车)
  • 隆化县 - 北京北 (火车)

Day 0

火车上的旅行总是欢快的,小帅哥 YL 拿了很多桌游,大家玩啊玩。
在玩阿瓦隆的时候,某个女博士一脸愁容,明显就是脑细胞不够用的表现。
路上路过了很多听过但是没去过的地方,比如古北口。

到了四合永镇,取车合影。

Gubeikou

到围场县很近,我们很快就骑到了,安顿好了住宿。肚子饿的呱呱叫,出去觅食,集体决定吃火锅(作为吃饭不挑剔的我当然也给不出什么建设性意见)。
一行人吃得热火朝天,风生水起,生龙活虎。最后算账,人均三十。

Hot Pot

然后去附近的超市准备接下来几天的食粮。水果、面包、零食、饮料,各种东西都来点。
逛超市比较有意思的是看到各种东西分门别类堆在一起,那么多,又都很光鲜。虽然不能都买了,但总有种莫名的愉悦感。

Supermarket

晚上还是玩玩玩,时刻压制住情绪,以免吵到其他人。

Day 1

第二天按时起床,旁边小店吃了早饭,准备出发。
说到这个出发,还是有些字面意义上的周折。小城在修路,大家凭着感觉和导航终于骑了出去。

骑车嘛,还是为了拍照。一路上大家各种互拍、自拍,对着天空、河流、群山拍。

骑车嘛,总会爆胎,尤其是漂亮的那种车,比如鲜红的挑战者 300。一路上补过胎的,没经验的,也基本都学会了。

scene1

scene2

scene4

这样的景色大家拍来拍去,怎么也拍不厌。骑在这样的路上,也感觉不累(假的,毕竟在爬大坡)。

接近傍晚,天空的颜色变得多彩,而眼前原野的景色淡下去了。

scene5

scene3

剩下的就是赶路了,还要走一段公路到机械林场。虽然路上的景色也很美,但是看不到,只能通过黑黢黢的树影摇曳想象了。路上发生了一起意外,美女 GY 摔车了。
简单处理了一下,也只能继续骑车。到了机械林场后买了创可贴和碘酒暂时顶一下。

together4

晚上十点多大家吃了羊腿饭,景区物价高,不过仍不及五道口。住宿的话,还停暖和的,三个大男生挤一挤了,另外的妹子们怎么睡都随意了,虽然大家都是在一间屋子里吧。

Day 2

together5

第二天早上在店家吃了早餐,馒头、稀饭、饼、鸡蛋、炒小菜,典型的住店组合套餐(甚至在西藏也是这种组合……)。
出去后,开始下小雪,然后是小雨。大家都披上了雨衣。不过这注定是狼狈的、凄惨的、励志的一天 ……
我不说别人了,就说我自己吧。首先我的鞋子是登山鞋,防水的那种,可以避免水从外面进来,也可以防止水从里面出去。于是我就相当于穿着一个便携泡脚盆在骑车。
总之就是一种说不清道不明的酸楚与傻乐呵在心中交织。

最大的收获是,雨中的山林颜色更加润了。眼前的雨虽然冷,但是远处的雾气很没啊,我们是不畏风雨的骑车人。

scene6

scene7

scene8

遇到一个便利店,大家进去买了各种包裹隔离物品(塑料袋),试图把自己的脚和腿保护起来。
中午在一个小饭馆吃了面,面对有点凶狠的狗去上了厕所。
吃完饭接着骑车,然后大家的车纷纷爆了胎,前面爆完后面爆胎。
忘记提的一点是,我带的补胎片失效了。
而备胎也用光了,于是各种打气筒接力打气艰难前行。
直到政委看到了一个给汽车补胎的地方,我们借用先进装备磨胎,强力胶水补胎,颇有种鸟枪换炮的感觉。

我最为先锋官,到了半截塔镇,发现了一个问题:有人办婚礼,没旅馆可以住了。
大家到了后,决定一拨人先买点东西吃饭,政委向来时方向去找住的地方,我向前面骑,去看看镇子里有没有住的地方。
我本来看到一家说可以的,但是后来店主说很抱歉,他现在在外地,只有自己孩子在家不放心。
幸好政委找到了一家汽车旅馆,住的地方有着落了,安心吃饭喽。

这天路上还遇到了交通事故,别人的。车堵了好几公里,我们骑车经过拥堵路段,骑在没车的路上,好爽啊。

这个汽车旅馆洗漱都要在屋子外面的,水冰凉。
睡之前大家把坏掉的车胎都换了。大家不停吐槽,车胎上面粘的泥里一股马粪味儿。
我和政委一屋,拿出了宝具 - 吹风机,开始吹白天被灌湿的登山鞋。
效果吗?还是超群的。累了,就睡得好了。

Day 3

因为今天要赶车,大家还是早起,吃了一点行动食品就出发了。
到了一个村子里,发现了卖包子的地方,于是纷纷买包子吃。
发现骑车的时候,怎么吃都吃不饱啊。
今天的天气,好极了,阳光灿烂。

scene9

scene10

大家嫌拍照片不过瘾,开始各种拍视频、拍延时,不亦乐乎。

我在前面骑,突然感觉内心迸发出无比力量,蹭蹭蹭。一路冲到了隆化县城火车站。
后来听闻某美女骑车走神,冲到了别人的电动三轮车车斗里面 ……

selfie1

[我一个人吃果粒爽,真是太好吃了 ……]

县城汇合后吃了羊蝎子,味道赞赞赞,食材很给力。让我的冲锋衣回去后携带的羊油气息经久不散 ……

吃完大家就去坐车了。火车上的时候我就开始傲娇了,伙伴们换了座位玩桌游,我却不想去,自己一个人坐在一个车厢里。
现在想想,很多时候情绪就不受自己控制了,当时有什么好的处理方法吗?

Day 4

回学校了。


一起骑车的小伙伴,有的还要继续在学校几年,有的已经要毕业了。想起你们,还是有些伤感的,在杭州有些孤单。

祝你们顺利。

理解 C++ Traits

C++ 是我学习的第一种编程语言,当时是 2007 年。现在 C++ 标准委员会已经在讨论 2020 年要发布的特性了,我连 2011 年发布的 Traits 都还不知道是啥。今天下定决心了解总结一下。

C++ 之父 Bjarne Stroustrup 说:

Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine “policy” or “implementation details”.

嗯,这段描述也比较抽象。我的理解是,trait 把不同类型的特定信息进行打包,用在算法或者其他对象中。这样算法或者其他对象的设计可以做到更加通用,通过 trait 打包的信息进行策略选择或者实现细节。

trait1

这段代码判断两个类型的关系。第 4 行的模板以两个类型作为参数,通过结构体 type_relation 的成员 relation 获取比较结果。当前代码输出:

1
2
type_relation<bool, int>: different
type_relation<int, int>: different

这段代码并未实现预期功能,因为比较 boolint,以及 intint 时,都输出 "different" 。下面继续完善这个 trait 实现。

trait2

第 11 行的类型参数种使用 <T, T> (即两个相同的类型)进行模式匹配。这段代码输出:

1
2
type_relation<bool, int>: different
type_relation<int, int>: equal

输出结果正确完成了类型比较的功能。


实现

trait 的实现基于 C++ 的模板引擎(template engine),能够在编译时期(compile time)将模板(基于最匹配的模式)展开。如果要在 Golang 或者 Java 种实现类似功能,可以使用反射或者 interface 之类的方法,而这些方法都是在运行时(runtime)进行判定,不可避免带来性能开销。


进阶

除了可以比较两个类型是否相等,还可以在类型参数上添加其他约束,例如:*[] 等。下面是一个稍微复杂一些的版本:

trait3

1
2
3
4
5
type_relation<bool, int>: different
type_relation<int, int>: equal
type_relation<int*, int>: is pointer of
type_relation<int[], int>: is array of
type_relation<int[4], int>: is array (length 4) of

当然这个“进阶”仅仅是比上面的例子复杂了一些,trait 还有更多能力。
C++ 的模板引擎为 C++ 提供了强大的元编程(meta programming)能力,也使得现代的 C++ 代码看上去比较奇怪。
事实上模板引擎是图灵完备的,如果使用好的话,能够将 C++ 的易用性和优雅性提升到一个高度(从那些看不到模板实现细节的用户的视角)。


感想

每次 C++ 标准更新的新闻我都看到了,从 C++0X、C++11、C++14 到 C++17,新出的特性一直在关注,可却从来没有真正编写一段程序去试试这些特性。这让我很伤感。

看到新的语言就想去尝试,OCaml、Clojure、Haskell …… 这些语言都很有特色,OCaml 让我体会到模式匹配的爽快,Clojure 让我体会到 S-表达式的自由和 Lisp 宏的强大,Haskell 让我体会到抽象的威力以及自己脑容量的有限。可是工作中用的最多的还是 Python、Java 和 C,当然还有 PHP (这个不能忘)。

想我和 C++ 的关系,最初的时候还是看的 Bjarne Stroustrup 的《The C++ Programming Language》。后来怎么就越走越远了呢?虽然现在工作中暂时依旧用不到,还是希望以此为契机,了解更多模板引擎可以做的事情。

追逐过的其他语言,各有各的契机,或许是缘分,或许是喜欢,或许是虚荣。现在都不敢说自己会 C++ 了。
如果当时能够更专注一些,自己目前的职业轨迹会不会有所区别?

想起最初自己理解 Haskell 里 Monad 概念的时候,一头雾水。后来懂了,就是懂了。为什么偏爱某一种语言呢?我也搞不清楚自己。

也许爱情也是如此,回忆当初做的不同结果是否能够更好?也许。记住过往,抗拒住回忆的诱惑,很难,但不得不做。喜欢的,就喜欢着吧。有些事情就是不行,正如有些事情莫名其妙地发生。

Just live with it.

‘How Does Monero Work?' 笔记

Monero 门罗币的一个介绍视频

我刚买到 1070Ti 显卡时曾经想要挖过这个币,后来放弃了,觉得没有意义。不过了解这个技术的一些原理还是有价值的。


Siraj Raval 的免责声明:


Monero 是一种不可追踪的加密货币,网络上很多非法买卖东西的人会使用它。他介绍这个技术的原因是:你应当对你的数据有控制权,而很多数据是交易数据(transactional data)。目前很多公司可以从中免费挖掘信息,用来预测你的购买行为,以便向你展示广告。理想情况下他们应当为你的数据付费,而他们付费的唯一可能是你控制着数据,而唯一控制数据的方式为数据是匿名的。为了查阅你的交易历史,

两个玻璃球(网上流传的面试题)

据说这是 Google 的一个面试题:

有一栋 100 层高的大楼,有两个完全相同的玻璃球。假设从某一层开始丢下玻璃球会摔碎,利用手中的两个玻璃球确定是第几层。最少扔几次玻璃球可以确定这个临界楼层(玻璃球在这一层以及更高的楼层扔下会摔碎)?

就如孔子说过的那句名言 “I Never Said All That Shit” 一样,这个题目出自哪里我也没有去考证。下面把我的思路整理一下。

如果只有一个玻璃球,能做的就是从 1 层开始尝试扔下玻璃球,然后尝试 2 层、3 层 ……,直到某一层扔下玻璃球后摔碎。而现在有两个玻璃球,我们可以用一个玻璃球从 1 层、11 层、21 层 …… 扔下去,确定一个较小的破碎范围,然后使用第二个玻璃球确定具体的楼层。

假如总共 N 层,我们从第 X 层扔下第一个玻璃球,有两种可能性:玻璃球摔碎或者没碎。如果玻璃球摔碎,说明临界楼层在 1~X 中;如果没摔碎,则临界楼层在 X+1~N 中。

如果临界楼层在 1~N 中是均匀分布的,那么 $N > 2$ 时确定临界楼层需要扔玻璃球的最少次数可以由以下公式表示:

$$
f(N)= \min_{X \in [1, N]} {1 + f(N-X)\frac{N-X}{N} + g(X)\frac{X}{N}}
$$

$f(N)$ 为使用两个玻璃球确定区间长度为 N 时的临界楼层所用次数,$g(X)$ 为使用一个玻璃球确定区间长度为 N 时的临界楼层所用次数。这个公式的三部分分别对应:

  • 一次扔玻璃球的尝试
  • 玻璃球未摔碎概率$\times$此时继续尝试需要的次数 $f(N-X)$(两个玻璃球)
  • 玻璃球摔碎概率$\times$此时继续尝试需要的次数 $g(X)$(一个玻璃球)。

显然有 $f(1)=0$,$g(N)=N-1$。可以用动态规划求解 $N=100$ 时需要的最少次数。

Golang 系统调用/Syscall

概述

很多和系统相关的函数都需要调用系统 API,例如读写文件的函数。Golang 对一些系统调用接口进行了封装,提供了 Golang 函数让用户调用,例如:

1
2
func Read(fd int, p []byte) (n int, err error)
func Write(fd int, p []byte) (n int, err error)

同时,Golang 也提供了对 Syscall 的直接调用支持:

1
2
3
4
5
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

RawSyscallRawSyscall6 是对操作系统 Syscall 的直接调用;SyscallSyscall6 会在调用操作系统 Syscall 前调用 runtime·entersyscall ,在操作系统 Syscall 返回后调用 runtime·exitsyscall

这四个函数都是使用汇编语言实现,代码和具体的硬件体系结构和操作系统有关。RawSyscallRawSyscall6 的行为和 C 语言中系统调用很类似,在这里不展开描述。而 SyscallSyscall6 的行为(在进行真正的系统调用前后插入额外操作)与 Golang 的调度器关系紧密,在下面会进行要点描述。

Syscall 的关键在于 runtime·entersyscallruntime·exitsyscall ,而 runtime·entersyscall 还有一个行为有部分差异的版本 runtime·entersyscallblock

runtime·entersyscall

1
2
3
func entersyscall(dummy int32) {
reentersyscall(getcallerpc(), getcallersp(unsafe.Pointer(&dummy)))
}
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
func reentersyscall(pc, sp uintptr) {
_g_ := getg()

// Disable preemption because during this function g is in Gsyscall status,
// but can have inconsistent g->sched, do not let GC observe it.
_g_.m.locks++

// Entersyscall must not call any function that might split/grow the stack.
// (See details in comment above.)
// Catch calls that might, by replacing the stack guard with something that
// will trip any stack check and leaving a flag to tell newstack to die.
_g_.stackguard0 = stackPreempt
_g_.throwsplit = true

// Leave SP around for GC and traceback.
save(pc, sp)
_g_.syscallsp = sp
_g_.syscallpc = pc
casgstatus(_g_, _Grunning, _Gsyscall)
if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
systemstack(func() {
print("entersyscall inconsistent ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
throw("entersyscall")
})
}

if trace.enabled {
systemstack(traceGoSysCall)
// systemstack itself clobbers g.sched.{pc,sp} and we might
// need them later when the G is genuinely blocked in a
// syscall
save(pc, sp)
}

if atomic.Load(&sched.sysmonwait) != 0 {
systemstack(entersyscall_sysmon)
save(pc, sp)
}

if _g_.m.p.ptr().runSafePointFn != 0 {
// runSafePointFn may stack split if run on this stack
systemstack(runSafePointFn)
save(pc, sp)
}

_g_.m.syscalltick = _g_.m.p.ptr().syscalltick
_g_.sysblocktraced = true
_g_.m.mcache = nil
_g_.m.p.ptr().m = 0
atomic.Store(&_g_.m.p.ptr().status, _Psyscall)
if sched.gcwaiting != 0 {
systemstack(entersyscall_gcwait)
save(pc, sp)
}

// Goroutines must not split stacks in Gsyscall status (it would corrupt g->sched).
// We set _StackGuard to StackPreempt so that first split stack check calls morestack.
// Morestack detects this case and throws.
_g_.stackguard0 = stackPreempt
_g_.m.locks--
}

runtime·entersyscall 主要完成以下几件事:

  1. 声明函数为 NOSPLIT ,不触发栈扩展检查
  2. 禁止抢占
  3. 通过 dummy 参数获得调用者的 SPPC 的值,并保存到 goroutine 的 syscallspsyscallpc 字段。同时记录 syscallstacksyscallguard ,使得垃圾回收器只对系统调用前的栈进行 mark-sweep (cgo 机制也利用了 entersyscall 来使得 cgo 中运行的代码不受垃圾回收机制管理)。
  4. 将 goroutine 的状态切换到 Gsyscall 状态。
  5. 唤醒后台线程 sysmon (这个线程会监控执行 syscall 的线程,如果超过某个时间阈值,就会将 M 与对应的 P 解除绑定)。
  6. 置空 M 的 mcache 、将 P 的 m 字段,切换 P 的状态到 Psyscall
  7. 检查是否需要垃圾回收
  8. 通过 g->stackguard0 = StackPreempt 使得出现 split stack 时可以通过 morestack 捕获并抛出错误
  9. 恢复抢占

可以看到 reentersyscall 多次调用 save 保存 pcspsave 更新 getg().sched 中的 sppc ,使得调用 gogo 的时候可以恢复 pcspreentersyscallsave 的目的都是为 goroutine 跳回这个 syscall 调用者执行 syscall 时刻的 pcsp做准备。

需要继续深入:

  1. StackPreempt
  2. syscallstacksyscallguard 的具体作用时机

runtime·entersyscallblock

runtime·entersyscall 区别在于这个函数认为当前执行的 syscall 会运行较长时间,因此在函数中主动进行了 M 和 P 的解除绑定,无需等待 sysmon 处理。解除 M 和 P 绑定的逻辑由 entersyscallblock_handoff 实现。

1
2
3
4
5
6
7
func entersyscallblock_handoff() {
if trace.enabled {
traceGoSysCall()
traceGoSysBlock(getg().m.p.ptr())
}
handoffp(releasep())
}

runtime·exitsyscall

主要实现了从 syscall 状态中恢复的动作:

  1. 尝试调用 exitsyscallfast ,如果 M 与 P 没有完全解除绑定,那么该操作会将 M 和 P 重新绑定;否则获取一个空闲的 P 与当前 M 绑定。如果绑定成功,返回 True,否则返回 False 进行后续步骤处理。
  2. 如果 exitsyscallfast 返回 True ,函数就直接返回;返回 False,则进入 slow path 将当前 goroutine 放到任务队列中等待调度,具体实现由 mcall(exitsyscall0) 实现。

exitsyscall0 这个函数比较清晰,只是对其中 dropg() 的目的还没想清楚。

runtime·exitsyscall 的函数说明中提到的 // Write barriers are not allowed because our P may have been stolen. 也没有搞清楚,知道和 GC 有一定关系。

引用

  1. https://studygolang.com/articles/7005

Golang 中学到的新东西

数据类型

string 类型

string 类型使用 2 个 word(64 bit 系统为 8 byte * 2)表示:一个 word 是指针,指向字符串存储区域;一个 word 表示长度数据。

slice $\leftrightarrow$ unsafe.Pointer

1
2
s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])
1
2
var ptr unsafe.Pointer
s := ((*[1<<10]byte)(ptr))[:200]

or

1
2
3
4
5
6
7
8
var ptr unsafe.Pointer
var s1 = struct {
addr uintptr
len int
cap int
}{ptr, length, length}

s := *(*[]byte)(unsafe.Pointer(&s1))

or

1
2
3
4
5
var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)

map 实现

整个页面的内容对我来说都是新的:https://tiancaiamao.gitbooks.io/go-internals/content/zh/02.3.html
不过这个页面描述的内容和最新的 Golang source 有一定差别。

HashMap 的实现,里面的一些核心关键词:bucketoverflow 让我理解起来有些困难。查询 HashMap 相关的一些资料后有了进一步了解。

  1. bucket 一般使用某种 array 管理,从 key 经过 hash-function 映射的 hash-value(可能截取一部分,也可以视作 sub-hash,我自己编的)作为 index 直接得到。一个 bucket 中可能包含多个不同的 hash-value ,它们截取那一部分得到的 index 相同。因此 bucket 会用一个数据结构管理这些冲突的值,可能是 linked-list 或者 tree-map 之类的。这些内部的数据结构中的 node 存储着真正对应 mapkey\value 对(pair)。
  2. 如果 bucket 太满了,比如元素的数量超过 bucket 数量一定倍数(load factor),则会进行扩容,所有元素都被 rehashed 到一个新的值。
  3. 采用这种方式实现 HashMapbucket 可以有两种选择:
    • Direct chaining 只存一个指向冲突元素集合的 header
    • Seperate Chainingbucket 存一部分(一个)元素集合(Golang HashMap 实现里放了 8 个),和一个指向剩下冲突元素集合的 header
  4. 上面 header 指向的元素集合叫 overflow list 或者 overflow some-other-data-structure

有了这些背景后,看代码应该会比较清晰了。

目前 Golang 中的 bucket 是为 insert 操作优化的,找到第一个空余位置就可以插入,但是删除的时候要把所有相同 key 的元素都删掉,要遍历 bucketoverflow 集合。

如果 key 或者 value 小于 128 字节,那么它们是直接在 bucket 存储值,否则存指向数据的指针。

nil 语义

按照 Golang 规范,任何类型在未初始化时都对应一个零值:

  • bool $\rightarrow$ true
  • integer $\rightarrow$ 0
  • string $\rightarrow$ ""
  • pointer/function/interface/slice/channel/map $\rightarrow$ nil

关于 interface{}

1
2
3
var v *T           // v == nil
var i interface{} // i == nil
i = v // i != nil

关于 channel

一些操作规则:

  • 读写一个 nilchannel 会立即阻塞
  • 读一个关闭的 channel 会立刻返回一个 channel 元素类型的零值,即 chan int 会返回 0
  • 写一个关闭的 channel 会导致 panic

函数调用

汇编

可以看一下这个 Golang 的官方介绍页面:https://golang.org/doc/asm

add.go

1
2
3
package add

func Add(a, b uint64) uint64

add_amd64.s 或使用其他平台后缀,和 add.go 在同一个目录

1
2
3
4
5
6
TEXT    ·Add+0(SB),$0-24
MOVQ a+0(FP),BX
MOVQ b+8(FP),BP
ADDQ BP,BX
MOVQ BX,res+16(FP)
RET ,

Golang 调用 C

add.c ,和 add.go 在同一个目录

1
2
3
4
void ·Add(uint64 a, uint64 b, uint64 ret) {
ret = a + b;
FLUSH(&ret);
}

编译这个包:go install add

C 文件中需要包含 runtime.h 头文件。因为 Golang 使用特殊寄存器存放像全局 struct Gstruct M ,包含这个文件可以让所有链接到 Go 的 C 文件感知这一点,避免编译器使用这些特定的寄存器做其他用途。

上面示例中返回值为空,使用 ret 作为返回值,FLUSHpkg/runtime/runtime.h 中定义为 USED() ,防止编译器优化掉对某个变量的赋值操作(因为看不到这个变量在后面其他地方使用了)。

函数调用时的内存布局

Golang 中使用的 C 编译器是 plan9 的 C 编译器,与 gcc 有一定差异。
这个页面中有部分基础介绍:
https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.1.html

如果返回多个值,func f(a, b int) (d, e int) 内存布局如下所示:

1
2
3
4
5
slot for e
slot for d
b
a
<- SP

调用后为

1
2
3
4
5
6
slot for e
slot for d
b
a <- FP
PC <- SP
f's stack

plan9 的 C 汇编器对被调用函数的参数值的修改是会返回到调用函数中的。

1
2
3
MOVQ    BX,d+16(FP)
...
MOVQ BX,e+24(FP)

go 关键字

f(1, 2, 3) 的汇编:

1
2
3
4
MOVL    $1, 0(SP)
MOVL $2, 4(SP)
MOVL $3, 8(SP)
CALL f(SB)

go f(1, 2, 3) 的汇编:

1
2
3
4
5
6
7
8
MOVL    $1, 0(SP)
MOVL $2, 4(SP)
MOVL $3, 8(SP)
PUSHQ $f(SB)
PUSHQ $12
CALL runtime.newproc(SB)
POPQ AX
POPQ AX

12 是参数占用的大小,runtime.newproc 函数接受的参数为:参数大小、新的 goroutine 要运行的函数、函数的参数。runtime.newproc 会新建一个栈空间,将栈参数的 12 个字节复制到新的栈空间,并让栈指针指向参数。可以看做 runtime.newproc(size, f, args)

defer 关键字

return x 不是原子语句,函数执行顺序为:

  1. 给返回值赋值
  2. defer 调用
  3. return

defer 实现对应 runtime.deferproc,其出现的地方插入指令 call runtime.deferproc ,函数返回之前的地方,插入 call runtime.deferreturn 。 goroutine 的控制结构中有一张表记录 defer,表以栈行为运作。

Continuous Stack

我也基本理解了思路,具体细节可以看:https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.5.html

最后的 runtime.lessstack 有些没看懂。

闭包

闭包中引用的变量不能在栈上分配,否则闭包函数返回的时候,栈上变量的地址就失效了。

逃逸分析(escape analyze)

Golang 有个特性,可以自动识别哪些变量在栈上分配,哪些在堆上分配。

1
2
3
4
5
6
func f() *Cursor {
var c Cursor
c.X = 500
noinline()
return &c
}
1
2
3
4
5
6
7
8
9
MOVQ      $type."".Cursor+0(SB),(SP)    // 取变量c的类型,也就是Cursor
PCDATA $0,$16
PCDATA $1,$0
CALL ,runtime.new(SB) // 调用new函数,相当于new(Cursor)
PCDATA $0,$-1
MOVQ 8(SP),AX // 取c.X的地址放到AX寄存器
MOVQ $500,(AX) // 将AX存放的内存地址的值赋为500
MOVQ AX,"".~r0+24(FP)
ADDQ $16,SP

在编译的过程中可以通过指令输出哪些变量逃逸了:go build --gcflags=-m main.go

闭包结构体

闭包将函数和它引用的环境表示为一个结构体:

1
2
3
4
type Closure struct {
F func()()
i *int
}

整体思路是返回闭包的时候,返回一个结构体,包含闭包返回函数的地址和引用的环境中的变量地址。

1
2
3
4
5
6
func f(i int) func() int {
return func() int {
i++
return i
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MOVQ    $type.int+0(SB),(SP)
PCDATA $0,$16
PCDATA $1,$0
CALL ,runtime.new(SB) // 是不是很熟悉,这一段就是i = new(int)
...
MOVQ $type.struct { F uintptr; A0 *int }+0(SB),(SP) // 这个结构体就是闭包的类型
...
CALL ,runtime.new(SB) // 接下来相当于 new(Closure)
PCDATA $0,$-1
MOVQ 8(SP),AX
NOP ,
MOVQ $"".func·001+0(SB),BP
MOVQ BP,(AX) // 函数地址赋值给Closure的F部分
NOP ,
MOVQ "".&i+16(SP),BP // 将堆中new的变量i的地址赋值给Closure的值部分
MOVQ BP,8(AX)
MOVQ AX,"".~r1+40(FP)
ADDQ $24,SP
RET ,

引用

  1. https://tiancaiamao.gitbooks.io/go-internals
  2. http://gki.informatik.uni-freiburg.de/teaching/ss11/theoryI/07_Hashing_Chaining.pdf