本样章为未经编辑的作者原稿。

作者自述

樊虹剑。15岁迷上Apple II。立志成为程序员。从Applesoft BASIC,到Microsoft C、C++、C#,又到Apple Objective-C,其间使用过大小十几种编程语言,始终不得要领。不明白发源于数学和电子学的计算机科学,为何不去追求诗歌般至简至纯的美学,而是缠绕于繁复的形式和纠结于空洞的哲学。几欲放弃。直到偶然发现Plan 9,才知道返璞归真的乐趣,并一路欢喜走过Inferno,快乐走入Go语言的世界。

图灵机

Go作为高级语言,当然是图灵完备的。我们用Go来写一个最简单的图灵机,使用脑操编程语言,下面的程序可以输出“hi”:

++++++++++[>++++++++++<-]>++++.+.

此编程语言仅有7条指令,理论上和任何图灵完备的语言等价。但程序员使用什么语言的表达能力和效率,是有云泥之分的。这也是为什么人们总是在探索新的语言,提高表达效率。

这7条指令是:
+ 使当前数据单元的值增一
- 使当前数据单元的值减一
> 下一个单元作为当前数据单元
< 上一个单元作为当前数据单元
[ 如果当前数据单元的值为0, 下一指令在对应的]后
] 如果当前数据单元的值不为0, 下一指令在对应的[后
. 把当前数据单元的值作为字符输出

这样,当前单元的值加10,作为[和]的循环变量,>到下一单元,也加10,<-使循环变量减一,循环10遍,再加4,得到104,是字符h的UTF8编码,输出,再加1,输出i。

package main

import "fmt"

var (
    a     [30000]byte
    prog  = "++++++++++[>++++++++++<-]>++++.+."
    p, pc int
)
func loop(inc int) {
    for i := inc; i != 0; pc += inc {
        switch prog[pc+inc] {
        case '[':
            i++
        case ']':
            i--
        }
    }
}
func main() {
    for {
        switch prog[pc] {
        case '>':
            p++
        case '<':
            p--
        case '+':
            a[p]++
        case '-':
            a[p]--
        case '.':
            fmt.Print(string(a[p]))
        case '[':
            if a[p] == 0 {
                loop(1)
            }
        case ']':
            if a[p] != 0 {
                loop(-1)
            }
        default:
            fmt.Println("Illegal instruction")
        }
        pc++
        if pc == len(prog) {
            return
        }
    }
}

程序一开始的变量a是我们图灵机的数据内存,prog是指令内存,p和pc分别是这两个内存的指针,代表当前数据单元和当前指令。

函数loop执行[和]指令,移动指令指针pc。这里用到了三段式的for语句,也就是:

for 初始;判断;增值 {

初始在循环开始前执行一次,通常是给循环控制变量一个初始值,然后每次判断如果真,就执行大括号的语句块一次,再执行增值的部分,再判断、执行、增值,直到判断为假,才跳过大括号的块,继续其后的语句。

switch是Go的单项选择语句。它根据后面的值,选择执行大括号里的某一个case分支。同样的for和switch语句,也出现在main函数里。但那里的for,没有三段式,所以会一直循环,直到return结束。而那里的switch,有一个default分支,当其它的case都不是switch的值时,选择执行default分支。

此程序还用到了++和--语句,给变量加一和减一。例如p++就是p = p + 1,也可以写为p += 1。这里,函数loop的for语句的第三段增值部分的pc += inc,就是pc = pc + inc的缩写。

注意++和--是单独的语句,不是表达式,不可以用在其它语句里。象*p++=*q++这种高明的C语句,在Go里是不能用的。目的是避免语义误导,给程序员少一点犯错的机会。

而这种加减一的操作,主要用来移动数组的下标。例如,p和pc分别是数组a和prog的下标,这样a[p]和prog[pc]就分别是对应下标的数组的单元的值。

至此,我们应该可以理解此程序了。作为练习,请读者拿出纸笔,画一条33个格子的带子,逐一填上prog的每个字符,这就是prog数组,也是图灵机的指令内存。再画一条至少两个格子的带子,作为数组a,也就是图灵机的数据内存,然后,执行每一条指令,看数据格子是怎样更新的。我们明白了图灵机器,也就明白了计算机器的理论基础。

在某些人看来,物理的宇宙也是如此运行的。一切皆是宿命、神旨、历史规律。但还有人认为,在这个无限的宇宙里,没有不可能,再小的几率也一定会发生,一切都不是确定的,所以机械的图灵机理论不适用。更何况,作为物理宇宙的采样观察者,人类太渺小,采样太少,是不可能正确解读物理宇宙的信息的。人,还是应该先观察明白自己身边的小事情,看怎样写的指令才能体现自己和人类整体的价值,而自己又是不是改变数据的那条指令?

好了,哲学人生观的讨论对Go的编程毫无帮助,我们还是检讨一下都学会了哪些Go的语法:包、函数、变量、常量、赋值、类型、字符串、数组、表达式、比较逻辑算数操作符、if、else、for、switch、case、++和--。够用了。下面我们不再用无用的例子,而是直接给出几个完整的工具程序。