Go的魅力

本文通过一个例子来展示Go语言的魅力。

打印输出前n个数中的质数

在开始学习编程时,很多人都会遇到这个经典的题目,打印前n个数中的质数。但如何并行解决这个问题呢?

一个直接的思路是,如果我们打印前100个数中的质数,我们直接找100个工人,每个工人计算其中一个数是不是质数,如果是质数则输出该数。虽然这是一个可行的解决方案,但不够优雅。特别是较小数容易判断是否是质数,较大数不容易判断,造成了工人之间负载不平衡。

下面有一段很好的代码展示了如何并行解决这个问题。代码用Go语言实现,这个例子也展示了Go语言的魅力。

这个解决方案的核心思想在于,将判断是否是质数转换为一个流水线任务。假如我们有100个工人,编号从1到100.当需要判断数字29是不是质数时,只需要向1号工人问一下,29能不能被2整除,向2号工人问一下,29能不能被3整除…这样如果前28个工人都回答不能整除,则29就是质数。

上面的计算仍有冗余,可以优化。判断一个数是否是质数,只需要拿比它小的质数除一下,并不需要比它小的所有数。这样我们需要计算2,3,5…的工人,而不需要计算4的工人。

最后一点,工人是个动态增多的过程,当我们判断出一个质数时,才需要新增这个工人。例如,当我们需要判断2,3,4,5,6…是否是质数,我们知道2是质数,则产生一个工人负责计算2,当3来到时,发现3不能被2整除,则产生一个工人负责计算3,以此类推。

下面我们看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func primetask(c chan int) {
p := <- c
if p > goal {
os.Exit(0)
}
fmt.Println(p)
nc := make(chan int)
go primetask(nc)
for {
i := <- c
if i % p != 0 {
nc <- i
}
}
}

在主函数进行调用时,用以下方式

1
2
3
4
5
c := make(chan int)
go primetask(c)
for i := 2;; i++ {
c <- i
}

每调用一次primetask时,产生一个工人。第一次在主函数调用go primetask(c)时,这个c就是流水线的输入。先输入2,这时函数内p的值变为2,然后打印输出2,表明2是一个质数。从此以后这个函数负责计算新来的值是不是被2整除。另外这个函数还需要构造和下一个工人的通道,即nc,当这个工人发现新来的数字不能被2整除时,需要通过nc交给下一个工人。每个工人第一次接受到的值(p)是不能被前面所有工人整除的值,即是一个质数,打印输出。之后,每个工人需要检查新来的值是否能被p整除,如果能,则无需发给下一个工人,如果不能,则发给下一个工人。

通过这个小例子可以看到Go语言精妙的地方。一门优秀的语言往往能解放思想,提高生产力。