原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-02-hello-revolution.html
在創(chuàng)世紀(jì)章節(jié)中我們簡(jiǎn)單介紹了 Go 語(yǔ)言的演化基因族譜,對(duì)其中來(lái)自于貝爾實(shí)驗(yàn)室的特有并發(fā)編程基因做了重點(diǎn)介紹,最后引出了 Go 語(yǔ)言版的“Hello, World”程序。其實(shí)“Hello, World”程序是展示各種語(yǔ)言特性的最好的例子,是通向該語(yǔ)言的一個(gè)窗口。這一節(jié)我們將沿著各個(gè)編程語(yǔ)言演化的時(shí)間軸,簡(jiǎn)單回顧下“Hello, World”程序是如何逐步演化到目前的 Go 語(yǔ)言形式、最終完成它的革命使命的。
圖 1-4 Go 語(yǔ)言并發(fā)演化歷史
首先是 B 語(yǔ)言,B 語(yǔ)言是 Go 語(yǔ)言之父貝爾實(shí)驗(yàn)室的 Ken Thompson 早年間開(kāi)發(fā)的一種通用的程序設(shè)計(jì)語(yǔ)言,設(shè)計(jì)目的是為了用于輔助 UNIX 系統(tǒng)的開(kāi)發(fā)。但是因?yàn)锽語(yǔ)言缺乏靈活的類型系統(tǒng)導(dǎo)致使用比較困難。后來(lái),Ken Thompson 的同事 Dennis Ritchie 以 B 語(yǔ)言為基礎(chǔ)開(kāi)發(fā)出了 C 語(yǔ)言,C 語(yǔ)言提供了豐富的類型,極大地增加了語(yǔ)言的表達(dá)能力。到目前為止它依然是世界上最常用的程序語(yǔ)言之一。而B(niǎo)語(yǔ)言自從被它取代之后,則就只存在于各種文獻(xiàn)之中,成為了歷史。
目前見(jiàn)到的 B 語(yǔ)言版本的“Hello World”,一般認(rèn)為是來(lái)自于 Brian W. Kernighan 編寫的 B 語(yǔ)言入門教程(Go 核心代碼庫(kù)中的第一個(gè)提交者名字正是 Brian W. Kernighan),程序如下:
main() {
extrn a, b, c;
putchar(a); putchar(b); putchar(c);
putchar('!*n');
}
a 'hell';
b 'o, w';
c 'orld';
由于 B 語(yǔ)言缺乏靈活的數(shù)據(jù)類型,只能分別以 a
、b
、c
全局變量來(lái)定義要輸出的內(nèi)容,并且每個(gè)變量的長(zhǎng)度必須對(duì)齊到了 4 個(gè)字節(jié)(有一種寫匯編語(yǔ)言的感覺(jué))。然后通過(guò)多次調(diào)用 putchar
函數(shù)輸出字符,最后的 '!*n'
表示輸出一個(gè)換行的意思。
總體來(lái)說(shuō),B 語(yǔ)言簡(jiǎn)單,功能也比較簡(jiǎn)陋。
C 語(yǔ)言是由 Dennis Ritchie 在 B 語(yǔ)言的基礎(chǔ)上改進(jìn)而來(lái),它增加了豐富的數(shù)據(jù)類型,并最終實(shí)現(xiàn)了用它重寫 UNIX 的偉大目標(biāo)。C 語(yǔ)言可以說(shuō)是現(xiàn)代 IT 行業(yè)最重要的軟件基石,目前主流的操作系統(tǒng)幾乎全部是由 C 語(yǔ)言開(kāi)發(fā)的,許多基礎(chǔ)系統(tǒng)軟件也是 C 語(yǔ)言開(kāi)發(fā)的。C 系家族的編程語(yǔ)言占據(jù)統(tǒng)治地位達(dá)幾十年之久,半個(gè)多世紀(jì)以來(lái)依然充滿活力。
在 Brian W. Kernighan 于 1974 年左右編寫的 C 語(yǔ)言入門教程中,出現(xiàn)了第一個(gè) C 語(yǔ)言版本的“Hello World”程序。這給后來(lái)大部分編程語(yǔ)言教程都以“Hello World”為第一個(gè)程序提供了慣例。第一個(gè) C 語(yǔ)言版本的“Hello World”程序如下:
main()
{
printf("hello, world");
}
關(guān)于這個(gè)程序,有幾點(diǎn)需要說(shuō)明的:首先是 main
函數(shù)因?yàn)闆](méi)有明確返回值類型,默認(rèn)返回 int
類型;其次 printf
函數(shù)默認(rèn)不需要導(dǎo)入函數(shù)聲明即可以使用;最后 main
沒(méi)有明確返回語(yǔ)句,但默認(rèn)返回 0 值。在這個(gè)程序出現(xiàn)時(shí),C 語(yǔ)言還遠(yuǎn)未標(biāo)準(zhǔn)化,我們看到的是上古時(shí)代的 C 語(yǔ)言語(yǔ)法:函數(shù)不用寫返回值,函數(shù)參數(shù)也可以忽略,使用 printf
時(shí)不需要包含頭文件等。
這個(gè)例子同樣出現(xiàn)在了 1978 年出版的《C 程序設(shè)計(jì)語(yǔ)言》第一版中,作者正是 Brian W. Kernighan 和 Dennis M. Ritchie(簡(jiǎn)稱 K&R)。書(shū)中的“Hello World”末尾增加了一個(gè)換行輸出:
main()
{
printf("hello, world\n");
}
這個(gè)例子在字符串末尾增加了一個(gè)換行,C 語(yǔ)言的 \n
換行比 B 語(yǔ)言的 '!*n'
換行看起來(lái)要簡(jiǎn)潔了一些。
在 K&R 的教程面世 10 年之后的 1988 年,《C 程序設(shè)計(jì)語(yǔ)言》第二版終于出版了。此時(shí) ANSI C 語(yǔ)言的標(biāo)準(zhǔn)化草案已經(jīng)初步完成,但正式版本的文檔尚未發(fā)布。不過(guò)書(shū)中的“Hello World”程序根據(jù)新的規(guī)范增加了 #include <stdio.h>
頭文件包含語(yǔ)句,用于包含printf
函數(shù)的聲明(新的 C89 標(biāo)準(zhǔn)中,僅僅是針對(duì)printf
函數(shù)而言,依然可以不用聲明函數(shù)而直接使用)。
#include <stdio.h>
main()
{
printf("hello, world\n");
}
然后到了 1989 年,ANSI C 語(yǔ)言第一個(gè)國(guó)際標(biāo)準(zhǔn)發(fā)布,一般被稱為 C89。C89 是流行最廣泛的一個(gè) C 語(yǔ)言標(biāo)準(zhǔn),目前依然被大量使用?!禖 程序設(shè)計(jì)語(yǔ)言》第二版的也再次印刷新版本,并針對(duì)新發(fā)布的 C89 規(guī)范建議,給 main
函數(shù)的參數(shù)增加了 void
輸入?yún)?shù)說(shuō)明,表示沒(méi)有輸入?yún)?shù)的意思。
#include <stdio.h>
main(void)
{
printf("hello, world\n");
}
至此,C 語(yǔ)言本身的進(jìn)化基本完成。后面的 C92、C99、C11 都只是針對(duì)一些語(yǔ)言細(xì)節(jié)做了完善。因?yàn)楦鞣N歷史因素, C89 依然是使用最廣泛的標(biāo)準(zhǔn)。
Newsqueak 是 Rob Pike 發(fā)明的老鼠語(yǔ)言的第二代,是他用于實(shí)踐 CSP 并發(fā)編程模型的戰(zhàn)場(chǎng)。Newsqueak 是新的 squeak 語(yǔ)言的意思,其中 squeak 是老鼠吱吱吱的叫聲,也可以看作是類似鼠標(biāo)點(diǎn)擊的聲音。Squeak 是一個(gè)提供鼠標(biāo)和鍵盤事件處理的編程語(yǔ)言,Squeak語(yǔ)言的管道是靜態(tài)創(chuàng)建的。改進(jìn)版的 Newsqueak 語(yǔ)言則提供了類似 C 語(yǔ)言語(yǔ)句和表達(dá)式的語(yǔ)法和類似 Pascal 語(yǔ)言的推導(dǎo)語(yǔ)法。Newsqueak 是一個(gè)帶自動(dòng)垃圾回收的純函數(shù)式語(yǔ)言,它再次針對(duì)鍵盤、鼠標(biāo)和窗口事件管理。但是在 Newsqueak 語(yǔ)言中管道是動(dòng)態(tài)創(chuàng)建的,屬于第一類值,因此可以保存到變量中。
Newsqueak 類似腳本語(yǔ)言,內(nèi)置了一個(gè) ?print
?函數(shù),它的“Hello World”程序看不出什么特色:
print("Hello,", "World", "\n");
從上面的程序中,除了猜測(cè) print
函數(shù)可以支持多個(gè)參數(shù)外,我們很難看到 Newsqueak 語(yǔ)言相關(guān)的特性。由于 Newsqueak 語(yǔ)言和 Go 語(yǔ)言相關(guān)的特性主要是并發(fā)和管道。因此,我們這里通過(guò)一個(gè)并發(fā)版本的“素?cái)?shù)篩”算法來(lái)略窺 Newsqueak 語(yǔ)言的特性?!八?cái)?shù)篩”的原理如圖:
圖 1-5 素?cái)?shù)篩
Newsqueak 語(yǔ)言并發(fā)版本的“素?cái)?shù)篩”程序如下:
// 向管道輸出從 2 開(kāi)始的自然數(shù)序列
counter := prog(c:chan of int) {
i := 2;
for(;;) {
c <-= i++;
}
};
// 針對(duì) listen 管道獲取的數(shù)列,過(guò)濾掉是 prime 倍數(shù)的數(shù)
// 新的序列輸出到 send 管道
filter := prog(prime:int, listen, send:chan of int) {
i:int;
for(;;) {
if((i = <-listen)%prime) {
send <-= i;
}
}
};
// 主函數(shù)
// 每個(gè)管道第一個(gè)流出的數(shù)必然是素?cái)?shù)
// 然后基于這個(gè)新的素?cái)?shù)構(gòu)建新的素?cái)?shù)過(guò)濾器
sieve := prog() of chan of int {
c := mk(chan of int);
begin counter(c);
prime := mk(chan of int);
begin prog(){
p:int;
newc:chan of int;
for(;;){
prime <-= p =<- c;
newc = mk();
begin filter(p, c, newc);
c = newc;
}
}();
become prime;
};
// 啟動(dòng)素?cái)?shù)篩
prime := sieve();
其中 counter
函數(shù)用于向管道輸出原始的自然數(shù)序列,每個(gè) filter
函數(shù)對(duì)象則對(duì)應(yīng)每一個(gè)新的素?cái)?shù)過(guò)濾管道,這些素?cái)?shù)過(guò)濾管道根據(jù)當(dāng)前的素?cái)?shù)篩子將輸入管道流入的數(shù)列篩選后重新輸出到輸出管道。mk(chan of int)
用于創(chuàng)建管道,類似 Go 語(yǔ)言的 make(chan int)
語(yǔ)句;begin filter(p, c, newc)
關(guān)鍵字啟動(dòng)素?cái)?shù)篩的并發(fā)體,類似
Go 語(yǔ)言的 go filter(p, c, newc)
語(yǔ)句;become
用于返回函數(shù)結(jié)果,類似 return
語(yǔ)句。
Newsqueak 語(yǔ)言中并發(fā)體和管道的語(yǔ)法和 Go 語(yǔ)言已經(jīng)比較接近了,后置的類型聲明和 Go 語(yǔ)言的語(yǔ)法也很相似。
在 Go 語(yǔ)言出現(xiàn)之前,Alef 語(yǔ)言是作者心中比較完美的并發(fā)語(yǔ)言,Alef 語(yǔ)法和運(yùn)行時(shí)基本是無(wú)縫兼容 C 語(yǔ)言。Alef 語(yǔ)言中的對(duì)線程和進(jìn)程的并發(fā)體都提供了支持,其中 proc receive(c)
用于啟動(dòng)一個(gè)進(jìn)程,task receive(c)
用于啟動(dòng)一個(gè)線程,它們之間通過(guò)管道 c
進(jìn)行通訊。不過(guò)由于 Alef 缺乏內(nèi)存自動(dòng)回收機(jī)制,導(dǎo)致并發(fā)體的內(nèi)存資源管理異常復(fù)雜。而且
Alef 語(yǔ)言只在 Plan9 系統(tǒng)中提供過(guò)短暫的支持,其它操作系統(tǒng)并沒(méi)有實(shí)際可以運(yùn)行的 Alef 開(kāi)發(fā)環(huán)境。而且 Alef 語(yǔ)言只有《Alef 語(yǔ)言規(guī)范》和《Alef 編程向?qū)А穬蓚€(gè)公開(kāi)的文檔,因此在貝爾實(shí)驗(yàn)室之外關(guān)于 Alef 語(yǔ)言的討論并不多。
由于 Alef 語(yǔ)言同時(shí)支持進(jìn)程和線程并發(fā)體,而且在并發(fā)體中可以再次啟動(dòng)更多的并發(fā)體,導(dǎo)致了 Alef 的并發(fā)狀態(tài)會(huì)異常復(fù)雜。同時(shí) Alef 沒(méi)有自動(dòng)垃圾回收機(jī)制(Alef 因?yàn)楸A舻?C 語(yǔ)言靈活的指針特性,也導(dǎo)致了自動(dòng)垃圾回收機(jī)制實(shí)現(xiàn)比較困難),各種資源充斥于不同的線程和進(jìn)程之間,導(dǎo)致并發(fā)體的內(nèi)存資源管理異常復(fù)雜。Alef 語(yǔ)言全部繼承了 C 語(yǔ)言的語(yǔ)法,可以認(rèn)為是增強(qiáng)了并發(fā)語(yǔ)法的 C 語(yǔ)言。下圖是 Alef 語(yǔ)言文檔中展示的一個(gè)可能的并發(fā)體狀態(tài):
圖 1-6 Alef 并發(fā)模型
Alef 語(yǔ)言并發(fā)版本的“Hello World”程序如下:
#include <alef.h>
void receive(chan(byte*) c) {
byte *s;
s = <- c;
print("%s\n", s);
terminate(nil);
}
void main(void) {
chan(byte*) c;
alloc c;
proc receive(c);
task receive(c);
c <- = "hello proc or task";
c <- = "hello proc or task";
print("done\n");
terminate(nil);
}
程序開(kāi)頭的 #include <alef.h>
語(yǔ)句用于包含 Alef 語(yǔ)言的運(yùn)行時(shí)庫(kù)。receive
是一個(gè)普通函數(shù),程序中用作每個(gè)并發(fā)體的入口函數(shù);main
函數(shù)中的 alloc c
語(yǔ)句先創(chuàng)建一個(gè) chan(byte*)
類型的管道,類似 Go 語(yǔ)言的 make(chan []byte)
語(yǔ)句;然后分別以進(jìn)程和線程的方式啟動(dòng) receive
函數(shù);啟動(dòng)并發(fā)體之后,main
函數(shù)向 c
管道發(fā)送了兩個(gè)字符串?dāng)?shù)據(jù);
而進(jìn)程和線程狀態(tài)運(yùn)行的 receive
函數(shù)會(huì)以不確定的順序先后從管道收到數(shù)據(jù)后,然后分別打印字符串;最后每個(gè)并發(fā)體都通過(guò)調(diào)用 terminate(nil)
來(lái)結(jié)束自己。
Alef 的語(yǔ)法和 C 語(yǔ)言基本保持一致,可以認(rèn)為它是在 C 語(yǔ)言的語(yǔ)法基礎(chǔ)上增加了并發(fā)編程相關(guān)的特性,可以看作是另一個(gè)維度的 C++ 語(yǔ)言。
Limbo(地獄)是用于開(kāi)發(fā)運(yùn)行在小型計(jì)算機(jī)上的分布式應(yīng)用的編程語(yǔ)言,它支持模塊化編程,編譯期和運(yùn)行時(shí)的強(qiáng)類型檢查,進(jìn)程內(nèi)基于具有類型的通信管道,原子性垃圾收集和簡(jiǎn)單的抽象數(shù)據(jù)類型。Limbo 被設(shè)計(jì)為:即便是在沒(méi)有硬件內(nèi)存保護(hù)的小型設(shè)備上,也能安全運(yùn)行。Limbo 語(yǔ)言主要運(yùn)行在 Inferno 系統(tǒng)之上。
Limbo 語(yǔ)言版本的“Hello World”程序如下:
implement Hello;
include "sys.m"; sys: Sys;
include "draw.m";
Hello: module
{
init: fn(ctxt: ref Draw->Context, args: list of string);
};
init(ctxt: ref Draw->Context, args: list of string)
{
sys = load Sys Sys->PATH;
sys->print("hello, world\n");
}
從這個(gè)版本的“Hello World”程序中,我們已經(jīng)可以發(fā)現(xiàn)很多 Go 語(yǔ)言特性的雛形。第一句 implement Hello;
基本對(duì)應(yīng) Go 語(yǔ)言的 package Hello
包聲明語(yǔ)句。然后是 include "sys.m"; sys: Sys;
和 include "draw.m";
語(yǔ)句用于導(dǎo)入其它的模塊,類似
Go 語(yǔ)言的 import "sys"
和 import "draw"
語(yǔ)句。然后 Hello 包模塊還提供了模塊初始化函數(shù) init
,并且函數(shù)的參數(shù)的類型也是后置的,不過(guò) Go 語(yǔ)言的初始化函數(shù)是沒(méi)有參數(shù)的。
貝爾實(shí)驗(yàn)室后來(lái)經(jīng)歷了多次動(dòng)蕩,包括 Ken Thompson 在內(nèi)的 Plan9 項(xiàng)目原班人馬最終加入了 Google 公司。在發(fā)明 Limbo 等前輩語(yǔ)言誕生十多年之后,在 2007 年底,Go 語(yǔ)言三個(gè)最初的作者因?yàn)榕既坏囊蛩鼐奂揭黄鹋?C++(傳說(shuō)是 C++ 語(yǔ)言的布道師在 Google 公司到處鼓吹的 C++11 各種牛逼特性徹底惹惱了他們),他們終于抽出了 20% 的自由時(shí)間創(chuàng)造了 Go 語(yǔ)言。最初的 Go 語(yǔ)言規(guī)范從 2008 年 3 月開(kāi)始編寫,最初的
Go 程序也是直接編譯到 C 語(yǔ)言然后再二次編譯為機(jī)器碼。到了 2008 年 5 月,Google 公司的領(lǐng)導(dǎo)們終于發(fā)現(xiàn)了 Go 語(yǔ)言的巨大潛力,從而開(kāi)始全力支持這個(gè)項(xiàng)目(Google 的創(chuàng)始人甚至還貢獻(xiàn)了func
關(guān)鍵字),讓他們可以將全部工作時(shí)間投入到 Go 語(yǔ)言的設(shè)計(jì)和開(kāi)發(fā)中。在 Go 語(yǔ)言規(guī)范初版完成之后,Go 語(yǔ)言的編譯器終于可以直接生成機(jī)器碼了。
package main
func main() int {
print "hello, world\n";
return 0;
}
這是初期 Go 語(yǔ)言程序正式開(kāi)始測(cè)試的版本。其中內(nèi)置的用于調(diào)試的 print
語(yǔ)句已經(jīng)存在,不過(guò)是以命令的方式使用。入口 main
函數(shù)還和 C 語(yǔ)言中的 main
函數(shù)一樣返回 int
類型的值,而且需要 return
顯式地返回值。每個(gè)語(yǔ)句末尾的分號(hào)也還存在。
package main
func main() {
print "hello, world\n";
}
入口函數(shù) main
已經(jīng)去掉了返回值,程序默認(rèn)通過(guò)隱式調(diào)用 exit(0)
來(lái)返回。Go 語(yǔ)言朝著簡(jiǎn)單的方向逐步進(jìn)化。
package main
func main() {
print("hello, world\n");
}
用于調(diào)試的內(nèi)置的 print
由開(kāi)始的命令改為普通的內(nèi)置函數(shù),使得語(yǔ)法更加簡(jiǎn)單一致。
package main
import "fmt"
func main() {
fmt.printf("hello, world\n");
}
作為 C 語(yǔ)言中招牌的 printf
格式化函數(shù)已經(jīng)移植了到了 Go 語(yǔ)言中,函數(shù)放在 fmt
包中(fmt
是格式化單詞 format
的縮寫)。不過(guò) printf
函數(shù)名的開(kāi)頭字母依然是小寫字母,采用大寫字母表示導(dǎo)出的特性還沒(méi)有出現(xiàn)。
package main
import "fmt"
func main() {
fmt.Printf("hello, world\n");
}
Go 語(yǔ)言開(kāi)始采用是否大小寫首字母來(lái)區(qū)分符號(hào)是否可以被導(dǎo)出。大寫字母開(kāi)頭表示導(dǎo)出的公共符號(hào),小寫字母開(kāi)頭表示包內(nèi)部的私有符號(hào)。國(guó)內(nèi)用戶需要注意的是,漢字中沒(méi)有大小寫字母的概念,因此以漢字開(kāi)頭的符號(hào)目前是無(wú)法導(dǎo)出的(針對(duì)問(wèn)題中國(guó)用戶已經(jīng)給出相關(guān)建議,等 Go2 之后或許會(huì)調(diào)整對(duì)漢字的導(dǎo)出規(guī)則)。
package main
import "fmt"
func main() {
fmt.Printf("hello, world\n")
}
Go 語(yǔ)言終于移除了語(yǔ)句末尾的分號(hào)。這是 Go 語(yǔ)言在 2009 年 11 月 10 號(hào)正式開(kāi)源之后第一個(gè)比較重要的語(yǔ)法改進(jìn)。從 1978 年 C 語(yǔ)言教程第一版引入的分號(hào)分割的規(guī)則到現(xiàn)在,Go 語(yǔ)言的作者們花了整整 32 年終于移除了語(yǔ)句末尾的分號(hào)。在這 32 年的演化的過(guò)程中必然充滿了各種八卦故事,我想這一定是 Go 語(yǔ)言設(shè)計(jì)者深思熟慮的結(jié)果(現(xiàn)在 Swift 等新的語(yǔ)言也是默認(rèn)忽略分號(hào)的,可見(jiàn)分號(hào)確實(shí)并不是那么的重要)。
在經(jīng)過(guò)半個(gè)世紀(jì)的涅槃重生之后,Go 語(yǔ)言不僅僅打印出了 Unicode 版本的“Hello, World”,而且可以方便地向全球用戶提供打印服務(wù)。下面版本通過(guò) http
服務(wù)向每個(gè)訪問(wèn)的客戶端打印中文的“你好, 世界!”和當(dāng)前的時(shí)間信息。
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
fmt.Println("Please visit http://127.0.0.1:12345/")
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
s := fmt.Sprintf("你好, 世界! -- Time: %s", time.Now().String())
fmt.Fprintf(w, "%v\n", s)
log.Printf("%v\n", s)
})
if err := http.ListenAndServe(":12345", nil); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
我們通過(guò)Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)自帶的 net/http
包構(gòu)造了一個(gè)獨(dú)立運(yùn)行的 http 服務(wù)。其中 http.HandleFunc("/", ...)
針對(duì) /
根路徑請(qǐng)求注冊(cè)了響應(yīng)處理函數(shù)。在響應(yīng)處理函數(shù)中,我們依然使用 fmt.Fprintf
格式化輸出函數(shù)實(shí)現(xiàn)了通過(guò) http 協(xié)議向請(qǐng)求的客戶端打印格式化的字符串,同時(shí)通過(guò)標(biāo)準(zhǔn)庫(kù)的日志包在服務(wù)器端也打印相關(guān)字符串。最后通過(guò) http.ListenAndServe
函數(shù)調(diào)用來(lái)啟動(dòng)
http 服務(wù)。
至此,Go 語(yǔ)言終于完成了從單機(jī)單核時(shí)代的 C 語(yǔ)言到 21 世紀(jì)互聯(lián)網(wǎng)時(shí)代多核環(huán)境的通用編程語(yǔ)言的蛻變。
![]() | ![]() |
更多建議: