来一口 golang 做的玻璃渣

转到后端差不多也快三个月了,拿 golang 糊代码的时间算上自己平时写的一些练手 demo 加起来差不多也有一个月。

这将近一个月的时间过来差不多能体会到 golang 的设计精髓,那就是:simple & stupid, being convenient as the first class support.

换句话说就是:短平快糙猛,满口玻璃渣,怎么方便怎么来。

所以接下来不免俗地是吐槽 golang 设计的内容。

吐槽不考虑 PLT 上的设计,纯粹从日常堆业务逻辑出发。毕竟理论的东西我一个鶸也不懂,且 golang 的设计目的就是方便应届毕业生快速堆业务代码。

以下吐槽点的顺序为自己在实际中遇到的顺序逆序。

Flaky Goroutines

golang 里起一个 goroutine 很方便,但是目前感觉 goroutine 太过于 flaky,有点飘。一旦没用 chan struct{} 或者 sync.WaitGroup ”固定好“,就总有一种这玩意儿是不是已经脱离自己手心的感觉。

另外不知道是不是很多用 golang 的人之前都是 php / python 的背景,相当一部分人其实对 goroutine-safe 没有什么概念。不过严格来说这个不是 golang 自身的问题。

Cannot assign to fields within short declaration notation

代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
func foobar() (bool, error) {
return true, nil
}

func main() {
var err error
ok, err := foobar()
if err != nil {
fmt.Println("error")
} else {
fmt.Println(ok)
}
}

是合法的,因为 ok 之前并没有被定义,所以这里 err 可以蹭着使用 :=

虚拟机 Linux Mint 磁盘扩容

之前在虚拟机里创建 mint 时磁盘就给了 20G 的 SSD,最近发现可用容量捉襟见肘,必须要扩容一波。

摸索了一下发现给虚拟机里的 linux 磁盘扩容并不是那么简单直白,至少有几个坑,所以稍微做了一些记录,以备日后不时之需。

Step 0:通过虚拟机给系统增加磁盘容量

这一步比较简单,VMWare 的话直接在虚拟机属性里可以进行 expand;VirtualBox 应该类似。

Step 1: 装配增加的磁盘容量

这一步比较麻烦的原因是,如果你的 linux 和我一样一开的时候使用默认安装,那么一般主分区后面会跟着一个 swap 分区,导致我们新增加的磁盘容量不和主分区连续。

所以我们要做的是把 swap 分区挪到最后。

首先安装 gparted,这玩意儿带 GUI,用起来也很直观。

Mint 下直接利用

1
sudo apt install gparted

装完后记的用管理员权限运行。

然后,首先将 linux-swap 分区停掉,操作是:右键,然后选择 swapoff

注:下面的操作在 gparted 中会是一个模拟操作,所有操作完之后再进行 apply。尽量别中途 apply!

接着删除这个 linux-swap 分区,然后选择 linux-swap 的父分区,进行 Move / Resize。

通过调整示意图中的容量条,将这整个分区挪到最后,保证我们新分配的容量分区和主分区连续。

再接着选择主分区,一样是利用 Move / Resize,进行容量扩增。

接着别忘了重新创建回我们的 linux-swap。

最后选择 apply changes 即可

Step 2:重新启用 swap

首先记录现在的 linux-swap 的 UUID。

然后用管理员权限打开 /etc/fstab,找到记录 swap 的项,更新 UUID。

最后重启系统。

重启后检查 swap 分区是否已经真的 active 了。这一步很重要,不然你会发现扩容后系统启动非常慢(因为 /etc/fstab 里的记录有问题)

检查可以通过系统自带的磁盘工具,看看 linux-swap 是否是 active 状态就行。

放弃给 ezio 加 SIGINT Handler

原本的计划是今天给 ezio 加上 SIGINT 的处理:自动退出运行的 EventLoop,让程序自主正常退出;但是在实现 Windows 版本的过程中发现了一些问题,最后思考再三,决定放弃整个特性。

在 Linux 上,这个实现很简单:添加一个 class SigIntHandler,在构造函数中利用 signal(SIGINT, handler) 安装自定义的处理函数,然后将这个类作为 IOServiceContext 的一个成员就行。

而 Windows 下可以用 SetConsoleCtrlEvent() 安装 handler,然后处理对应的 Ctrl+C 事件。

但是在 Windows 下实现是遇到了一点问题:测试过程中我发现退出一直没效果。

退出的代码如下:

1
2
3
4
5
6
7
8
void QuitEventLoop()
{
// Acquire from TLS
auto loop = EventLoop::current();
if (loop) {
loop->Quit();
}
}

挂上调试器后我发现两个事实:

  • loop 是 nullptr
  • 执行这个函数的线程是一个新线程

第二点可以解释第一点。

查了一下 MSDN 发现这是 by-design:SetConsoleCtrlEvent() 永远是运行在一个独立线程。

Linux 下因为 signal-handler 的调用都是在主线程,所以可以拿到主线程 TLS 里的 event-loop。

Windows 这么设计是因为他们觉得 Unix / Linux 的 signal 机制太危险,容易出问题(多线程不友好不说,还独立引入了 async signal-safety 的概念)。详情可以参考 Raymond Chen 写的这篇

那么,要在 Windows 上实现类似的效果,必须要能够拿到主线程的 EvenetLoop,我思前想后,除了引入一个 main event-loop 的概念之外,很难保持当前抽象框架上解决这个问题。

同时为可考虑到两个平台的基本行为的一致性,最终我放弃了这个特性。

毕竟,在服务器上跑着的服务,一般都是由类似 supervisor 驱动并且长期运行在后台的 XD。

Build Your Own HandlerThread Part Finale

至此,我们整个系列宣告完结。

现在回过头往前看,是不是觉得其实这些底层的东西也没这么难?有时候只是需要一些基础和解决问题的方法罢了。

Rant:做业务才真的难呢,框架和流程都给你定死了,一坨又一坨用了几年的不明觉厉的封装,如果内部文档哪个不详,又没有熟悉的人带着你,基本就净在里面绕圈子了。

好了我们转回正题。

整个系列的代码可见这里:Eureka/ActiveThread-Java

部分还带了单元测试。

系列索引

Build Your Own HandlerThread Part 4

有了前面的铺垫我们终于可以开始实现我们的主角 ActiveThread 了,虽然它登场的有点晚。

这个系列的开头我们提到 ActiveThread 有两个鲜明的特点:

  • 它是线程,可以运行,代表一个单独的执行上下文(execution unit/context)
  • 每个 ActiveThread 内部运行一个 message-loop,方便持续的执行我们提交的任务

这两点和 Android 原生提供的 HandlerThread 是一模一样的。

不过和 HandlerThread 不同,我们这里不打算采用继承 Thread 的方式,而是采用 composition。原因之一是我个人非常反感传统的 OO 继承手法;并且 Java 8 开始正式支持 lambda 之后,不用继承我们的工作也可以做得很好。

不适用继承同时有个好处,我们可以只暴露我们需要的接口,避免误用(比如经典的用 run() 而不是 start()

Build Your Own HandlerThread Part 3

前面我们实现了 MessageLoop,现在该轮到 TaskRunner 了。

在我们的设计中,TaskRunner 的定位是类似 Handler,用来驱动/使用 MessageLoop,将 MessageLoop 隐藏在日常使用中。

TaskRunner 的实现比较简单,因为几乎所有的实际工作都是由内部的 MessageLoop 完成。

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
// Our MessageLoop doesn't provide a way to complete pending tasks when it is asked to quit.
// Therefore our post-task methods don't care about if a task was submitted.

import java.time.Instant;

public class TaskRunner {
private final MessageLoop _loop;

// Uses current thread's owning MessageLoop.
// Throws an exception if the thread has no bound MessageLoop.
public TaskRunner() {
_loop = MessageLoop.current();
}

public TaskRunner(MessageLoop loop) {
_loop = loop;
}

public MessageLoop getMessageLoop() {
return _loop;
}

public void postTask(Runnable r) {
_loop.enqueueTask(new PendingTask(r, Instant.EPOCH));
}

public void postTaskAt(Runnable r, long uptimeMillis) {
_loop.enqueueTask(new PendingTask(r, Instant.ofEpochMilli(uptimeMillis)));
}

public void postDelayedTask(Runnable r, long delayMillis) {
_loop.enqueueTask(new PendingTask(r, Instant.now().plusMillis(delayMillis)));
}

public void runTask(Runnable r) {
if (_loop.belongsToCurrentThread()) {
r.run();
} else {
postTask(r);
}
}
}

Handler 一个最明显的不同之处仅是我们增加了 runTask() 函数。

postTask() 不同,如果当前线程就是 loop thread,r 会被立即执行。这在某些时候是一个优势。