Go 語言 Hello, World 的革命

2023-03-22 14:56 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-02-hello-revolution.html


1.2 Hello, World 的革命

在創(chuàng)世紀章節(jié)中我們簡單介紹了 Go 語言的演化基因族譜,對其中來自于貝爾實驗室的特有并發(fā)編程基因做了重點介紹,最后引出了 Go 語言版的“Hello, World”程序。其實“Hello, World”程序是展示各種語言特性的最好的例子,是通向該語言的一個窗口。這一節(jié)我們將沿著各個編程語言演化的時間軸,簡單回顧下“Hello, World”程序是如何逐步演化到目前的 Go 語言形式、最終完成它的革命使命的。


圖 1-4 Go 語言并發(fā)演化歷史

1.2.1 B 語言 - Ken Thompson, 1972

首先是 B 語言,B 語言是 Go 語言之父貝爾實驗室的 Ken Thompson 早年間開發(fā)的一種通用的程序設計語言,設計目的是為了用于輔助 UNIX 系統(tǒng)的開發(fā)。但是因為B語言缺乏靈活的類型系統(tǒng)導致使用比較困難。后來,Ken Thompson 的同事 Dennis Ritchie 以 B 語言為基礎開發(fā)出了 C 語言,C 語言提供了豐富的類型,極大地增加了語言的表達能力。到目前為止它依然是世界上最常用的程序語言之一。而B語言自從被它取代之后,則就只存在于各種文獻之中,成為了歷史。

目前見到的 B 語言版本的“Hello World”,一般認為是來自于 Brian W. Kernighan 編寫的 B 語言入門教程(Go 核心代碼庫中的第一個提交者名字正是 Brian W. Kernighan),程序如下:

main() {
    extrn a, b, c;
    putchar(a); putchar(b); putchar(c);
    putchar('!*n');
}
a 'hell';
b 'o, w';
c 'orld';

由于 B 語言缺乏靈活的數(shù)據(jù)類型,只能分別以 a、bc 全局變量來定義要輸出的內容,并且每個變量的長度必須對齊到了 4 個字節(jié)(有一種寫匯編語言的感覺)。然后通過多次調用 putchar 函數(shù)輸出字符,最后的 '!*n' 表示輸出一個換行的意思。

總體來說,B 語言簡單,功能也比較簡陋。

1.2.2 C 語言 - Dennis Ritchie, 1974 ~ 1989

C 語言是由 Dennis Ritchie 在 B 語言的基礎上改進而來,它增加了豐富的數(shù)據(jù)類型,并最終實現(xiàn)了用它重寫 UNIX 的偉大目標。C 語言可以說是現(xiàn)代 IT 行業(yè)最重要的軟件基石,目前主流的操作系統(tǒng)幾乎全部是由 C 語言開發(fā)的,許多基礎系統(tǒng)軟件也是 C 語言開發(fā)的。C 系家族的編程語言占據(jù)統(tǒng)治地位達幾十年之久,半個多世紀以來依然充滿活力。

在 Brian W. Kernighan 于 1974 年左右編寫的 C 語言入門教程中,出現(xiàn)了第一個 C 語言版本的“Hello World”程序。這給后來大部分編程語言教程都以“Hello World”為第一個程序提供了慣例。第一個 C 語言版本的“Hello World”程序如下:

main()
{
    printf("hello, world");
}

關于這個程序,有幾點需要說明的:首先是 main 函數(shù)因為沒有明確返回值類型,默認返回 int 類型;其次 printf 函數(shù)默認不需要導入函數(shù)聲明即可以使用;最后 main 沒有明確返回語句,但默認返回 0 值。在這個程序出現(xiàn)時,C 語言還遠未標準化,我們看到的是上古時代的 C 語言語法:函數(shù)不用寫返回值,函數(shù)參數(shù)也可以忽略,使用 printf 時不需要包含頭文件等。

這個例子同樣出現(xiàn)在了 1978 年出版的《C 程序設計語言》第一版中,作者正是 Brian W. Kernighan 和 Dennis M. Ritchie(簡稱 K&R)。書中的“Hello World”末尾增加了一個換行輸出:

main()
{
    printf("hello, world\n");
}

這個例子在字符串末尾增加了一個換行,C 語言的 \n 換行比 B 語言的 '!*n' 換行看起來要簡潔了一些。

在 K&R 的教程面世 10 年之后的 1988 年,《C 程序設計語言》第二版終于出版了。此時 ANSI C 語言的標準化草案已經初步完成,但正式版本的文檔尚未發(fā)布。不過書中的“Hello World”程序根據(jù)新的規(guī)范增加了 #include <stdio.h> 頭文件包含語句,用于包含printf函數(shù)的聲明(新的 C89 標準中,僅僅是針對printf函數(shù)而言,依然可以不用聲明函數(shù)而直接使用)。

#include <stdio.h>

main()
{
    printf("hello, world\n");
}

然后到了 1989 年,ANSI C 語言第一個國際標準發(fā)布,一般被稱為 C89。C89 是流行最廣泛的一個 C 語言標準,目前依然被大量使用?!禖 程序設計語言》第二版的也再次印刷新版本,并針對新發(fā)布的 C89 規(guī)范建議,給 main 函數(shù)的參數(shù)增加了 void 輸入參數(shù)說明,表示沒有輸入參數(shù)的意思。

#include <stdio.h>

main(void)
{
    printf("hello, world\n");
}

至此,C 語言本身的進化基本完成。后面的 C92、C99、C11 都只是針對一些語言細節(jié)做了完善。因為各種歷史因素, C89 依然是使用最廣泛的標準。

1.2.3 Newsqueak - Rob Pike, 1989

Newsqueak 是 Rob Pike 發(fā)明的老鼠語言的第二代,是他用于實踐 CSP 并發(fā)編程模型的戰(zhàn)場。Newsqueak 是新的 squeak 語言的意思,其中 squeak 是老鼠吱吱吱的叫聲,也可以看作是類似鼠標點擊的聲音。Squeak 是一個提供鼠標和鍵盤事件處理的編程語言,Squeak語言的管道是靜態(tài)創(chuàng)建的。改進版的 Newsqueak 語言則提供了類似 C 語言語句和表達式的語法和類似 Pascal 語言的推導語法。Newsqueak 是一個帶自動垃圾回收的純函數(shù)式語言,它再次針對鍵盤、鼠標和窗口事件管理。但是在 Newsqueak 語言中管道是動態(tài)創(chuàng)建的,屬于第一類值,因此可以保存到變量中。

Newsqueak 類似腳本語言,內置了一個 ?print ?函數(shù),它的“Hello World”程序看不出什么特色:

print("Hello,", "World", "\n");

從上面的程序中,除了猜測 print 函數(shù)可以支持多個參數(shù)外,我們很難看到 Newsqueak 語言相關的特性。由于 Newsqueak 語言和 Go 語言相關的特性主要是并發(fā)和管道。因此,我們這里通過一個并發(fā)版本的“素數(shù)篩”算法來略窺 Newsqueak 語言的特性?!八財?shù)篩”的原理如圖:


圖 1-5 素數(shù)篩

Newsqueak 語言并發(fā)版本的“素數(shù)篩”程序如下:

// 向管道輸出從 2 開始的自然數(shù)序列
counter := prog(c:chan of int) {
    i := 2;
    for(;;) {
        c <-= i++;
    }
};

// 針對 listen 管道獲取的數(shù)列,過濾掉是 prime 倍數(shù)的數(shù)
// 新的序列輸出到 send 管道
filter := prog(prime:int, listen, send:chan of int) {
    i:int;
    for(;;) {
        if((i = <-listen)%prime) {
            send <-= i;
        }
    }
};

// 主函數(shù)
// 每個管道第一個流出的數(shù)必然是素數(shù)
// 然后基于這個新的素數(shù)構建新的素數(shù)過濾器
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;
};

// 啟動素數(shù)篩
prime := sieve();

其中 counter 函數(shù)用于向管道輸出原始的自然數(shù)序列,每個 filter 函數(shù)對象則對應每一個新的素數(shù)過濾管道,這些素數(shù)過濾管道根據(jù)當前的素數(shù)篩子將輸入管道流入的數(shù)列篩選后重新輸出到輸出管道。mk(chan of int) 用于創(chuàng)建管道,類似 Go 語言的 make(chan int) 語句;begin filter(p, c, newc) 關鍵字啟動素數(shù)篩的并發(fā)體,類似 Go 語言的 go filter(p, c, newc) 語句;become 用于返回函數(shù)結果,類似 return 語句。

Newsqueak 語言中并發(fā)體和管道的語法和 Go 語言已經比較接近了,后置的類型聲明和 Go 語言的語法也很相似。

1.2.4 Alef - Phil Winterbottom, 1993

在 Go 語言出現(xiàn)之前,Alef 語言是作者心中比較完美的并發(fā)語言,Alef 語法和運行時基本是無縫兼容 C 語言。Alef 語言中的對線程和進程的并發(fā)體都提供了支持,其中 proc receive(c) 用于啟動一個進程,task receive(c) 用于啟動一個線程,它們之間通過管道 c 進行通訊。不過由于 Alef 缺乏內存自動回收機制,導致并發(fā)體的內存資源管理異常復雜。而且 Alef 語言只在 Plan9 系統(tǒng)中提供過短暫的支持,其它操作系統(tǒng)并沒有實際可以運行的 Alef 開發(fā)環(huán)境。而且 Alef 語言只有《Alef 語言規(guī)范》和《Alef 編程向導》兩個公開的文檔,因此在貝爾實驗室之外關于 Alef 語言的討論并不多。

由于 Alef 語言同時支持進程和線程并發(fā)體,而且在并發(fā)體中可以再次啟動更多的并發(fā)體,導致了 Alef 的并發(fā)狀態(tài)會異常復雜。同時 Alef 沒有自動垃圾回收機制(Alef 因為保留的 C 語言靈活的指針特性,也導致了自動垃圾回收機制實現(xiàn)比較困難),各種資源充斥于不同的線程和進程之間,導致并發(fā)體的內存資源管理異常復雜。Alef 語言全部繼承了 C 語言的語法,可以認為是增強了并發(fā)語法的 C 語言。下圖是 Alef 語言文檔中展示的一個可能的并發(fā)體狀態(tài):


圖 1-6 Alef 并發(fā)模型

Alef 語言并發(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);
}

程序開頭的 #include <alef.h> 語句用于包含 Alef 語言的運行時庫。receive 是一個普通函數(shù),程序中用作每個并發(fā)體的入口函數(shù);main 函數(shù)中的 alloc c 語句先創(chuàng)建一個 chan(byte*) 類型的管道,類似 Go 語言的 make(chan []byte) 語句;然后分別以進程和線程的方式啟動 receive 函數(shù);啟動并發(fā)體之后,main 函數(shù)向 c 管道發(fā)送了兩個字符串數(shù)據(jù); 而進程和線程狀態(tài)運行的 receive 函數(shù)會以不確定的順序先后從管道收到數(shù)據(jù)后,然后分別打印字符串;最后每個并發(fā)體都通過調用 terminate(nil) 來結束自己。

Alef 的語法和 C 語言基本保持一致,可以認為它是在 C 語言的語法基礎上增加了并發(fā)編程相關的特性,可以看作是另一個維度的 C++ 語言。

1.2.5 Limbo - Sean Dorward, Phil Winterbottom, Rob Pike, 1995

Limbo(地獄)是用于開發(fā)運行在小型計算機上的分布式應用的編程語言,它支持模塊化編程,編譯期和運行時的強類型檢查,進程內基于具有類型的通信管道,原子性垃圾收集和簡單的抽象數(shù)據(jù)類型。Limbo 被設計為:即便是在沒有硬件內存保護的小型設備上,也能安全運行。Limbo 語言主要運行在 Inferno 系統(tǒng)之上。

Limbo 語言版本的“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");
}

從這個版本的“Hello World”程序中,我們已經可以發(fā)現(xiàn)很多 Go 語言特性的雛形。第一句 implement Hello; 基本對應 Go 語言的 package Hello 包聲明語句。然后是 include "sys.m"; sys: Sys; 和 include "draw.m"; 語句用于導入其它的模塊,類似 Go 語言的 import "sys" 和 import "draw" 語句。然后 Hello 包模塊還提供了模塊初始化函數(shù) init,并且函數(shù)的參數(shù)的類型也是后置的,不過 Go 語言的初始化函數(shù)是沒有參數(shù)的。

1.2.6 Go 語言 - 2007~2009

貝爾實驗室后來經歷了多次動蕩,包括 Ken Thompson 在內的 Plan9 項目原班人馬最終加入了 Google 公司。在發(fā)明 Limbo 等前輩語言誕生十多年之后,在 2007 年底,Go 語言三個最初的作者因為偶然的因素聚集到一起批斗 C++(傳說是 C++ 語言的布道師在 Google 公司到處鼓吹的 C++11 各種牛逼特性徹底惹惱了他們),他們終于抽出了 20% 的自由時間創(chuàng)造了 Go 語言。最初的 Go 語言規(guī)范從 2008 年 3 月開始編寫,最初的 Go 程序也是直接編譯到 C 語言然后再二次編譯為機器碼。到了 2008 年 5 月,Google 公司的領導們終于發(fā)現(xiàn)了 Go 語言的巨大潛力,從而開始全力支持這個項目(Google 的創(chuàng)始人甚至還貢獻了func關鍵字),讓他們可以將全部工作時間投入到 Go 語言的設計和開發(fā)中。在 Go 語言規(guī)范初版完成之后,Go 語言的編譯器終于可以直接生成機器碼了。

1.2.6.1 hello.go - 2008 年 6 月

package main

func main() int {
    print "hello, world\n";
    return 0;
}

這是初期 Go 語言程序正式開始測試的版本。其中內置的用于調試的 print 語句已經存在,不過是以命令的方式使用。入口 main 函數(shù)還和 C 語言中的 main 函數(shù)一樣返回 int 類型的值,而且需要 return 顯式地返回值。每個語句末尾的分號也還存在。

1.2.6.2 hello.go - 2008 年 6 月 27 日

package main

func main() {
    print "hello, world\n";
}

入口函數(shù) main 已經去掉了返回值,程序默認通過隱式調用 exit(0) 來返回。Go 語言朝著簡單的方向逐步進化。

1.2.6.3 hello.go - 2008 年 8 月 11 日

package main

func main() {
    print("hello, world\n");
}

用于調試的內置的 print 由開始的命令改為普通的內置函數(shù),使得語法更加簡單一致。

1.2.6.4 hello.go - 2008 年 10 月 24 日

package main

import "fmt"

func main() {
    fmt.printf("hello, world\n");
}

作為 C 語言中招牌的 printf 格式化函數(shù)已經移植了到了 Go 語言中,函數(shù)放在 fmt 包中(fmt 是格式化單詞 format 的縮寫)。不過 printf 函數(shù)名的開頭字母依然是小寫字母,采用大寫字母表示導出的特性還沒有出現(xiàn)。

1.2.6.5 hello.go - 2009年1月15日

package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n");
}

Go 語言開始采用是否大小寫首字母來區(qū)分符號是否可以被導出。大寫字母開頭表示導出的公共符號,小寫字母開頭表示包內部的私有符號。國內用戶需要注意的是,漢字中沒有大小寫字母的概念,因此以漢字開頭的符號目前是無法導出的(針對問題中國用戶已經給出相關建議,等 Go2 之后或許會調整對漢字的導出規(guī)則)。

1.2.6.7 hello.go - 2009 年 12 月 11 日

package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n")
}

Go 語言終于移除了語句末尾的分號。這是 Go 語言在 2009 年 11 月 10 號正式開源之后第一個比較重要的語法改進。從 1978 年 C 語言教程第一版引入的分號分割的規(guī)則到現(xiàn)在,Go 語言的作者們花了整整 32 年終于移除了語句末尾的分號。在這 32 年的演化的過程中必然充滿了各種八卦故事,我想這一定是 Go 語言設計者深思熟慮的結果(現(xiàn)在 Swift 等新的語言也是默認忽略分號的,可見分號確實并不是那么的重要)。

1.2.7 你好, 世界! - V2.0

在經過半個世紀的涅槃重生之后,Go 語言不僅僅打印出了 Unicode 版本的“Hello, World”,而且可以方便地向全球用戶提供打印服務。下面版本通過 http 服務向每個訪問的客戶端打印中文的“你好, 世界!”和當前的時間信息。

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)
    }
}

我們通過Go語言標準庫自帶的 net/http 包構造了一個獨立運行的 http 服務。其中 http.HandleFunc("/", ...) 針對 / 根路徑請求注冊了響應處理函數(shù)。在響應處理函數(shù)中,我們依然使用 fmt.Fprintf 格式化輸出函數(shù)實現(xiàn)了通過 http 協(xié)議向請求的客戶端打印格式化的字符串,同時通過標準庫的日志包在服務器端也打印相關字符串。最后通過 http.ListenAndServe 函數(shù)調用來啟動 http 服務。

至此,Go 語言終于完成了從單機單核時代的 C 語言到 21 世紀互聯(lián)網時代多核環(huán)境的通用編程語言的蛻變。



以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號