Go 部落格

Go 的宣告語法

Rob Pike
2010 年 7 月 7 日

引言

Go 的新手可能會奇怪為什麼宣告語法與 C 家族中建立的傳統不同。在這篇文章中,我們將比較這兩種方法,並解釋 Go 的宣告為何看起來如此。

C 語法

首先,我們來談談 C 語法。C 在宣告語法上採取了一種不尋常但巧妙的方法。它不使用特殊的語法來描述型別,而是編寫一個涉及被宣告項的表示式,並說明該表示式的型別。因此,

int x;

宣告 x 為 int:表示式‘x’的型別為 int。一般來說,要弄清楚如何編寫新變數的型別,請寫一個涉及該變數並計算為基本型別的表示式,然後將基本型別放在左邊,表示式放在右邊。

因此,宣告

int *p;
int a[3];

表示 p 是 int 的指標,因為‘*p’的型別是 int;a 是 int 的陣列,因為 a[3](忽略具體索引值,該值被用來表示陣列大小)的型別是 int。

函式呢?最初,C 的函式宣告在括號外寫引數的型別,如下所示:

int main(argc, argv)
    int argc;
    char *argv[];
{ /* ... */ }

同樣,我們看到 main 是一個函式,因為表示式 main(argc, argv) 返回一個 int。用現代的表示法,我們會寫:

int main(int argc, char *argv[]) { /* ... */ }

但基本結構是相同的。

這是一個巧妙的語法思想,對於簡單型別效果很好,但很快就會變得令人困惑。著名的例子是宣告函式指標。遵循規則,你會得到:

int (*fp)(int a, int b);

這裡,fp 是一個指向函式的指標,因為如果你寫表示式 (*fp)(a, b),你就會呼叫一個返回 int 的函式。如果 fp 的一個引數本身也是一個函式呢?

int (*fp)(int (*ff)(int x, int y), int b)

這開始變得難以閱讀。

當然,我們在宣告函式時可以省略引數名,所以 main 可以宣告為:

int main(int, char *[])

回想一下 argv 的宣告是這樣的:

char *argv[]

所以你從其宣告的中間去掉名稱來構建其型別。但並不明顯,透過將名稱放在中間來宣告 char *[] 型別的東西。

看看如果你不給引數命名,fp 的宣告會變成什麼樣子:

int (*fp)(int (*)(int, int), int)

不僅不清楚名字應該放在哪裡:

int (*)(int, int)

甚至不完全清楚它是一個函式指標宣告。如果返回型別是函式指標呢?

int (*(*fp)(int (*)(int, int), int))(int, int)

甚至很難看出這個宣告是關於 fp 的。

你可以構造更復雜的例子,但這應該能說明 C 宣告語法可能帶來的一些困難。

還有一點需要指出。因為型別和宣告語法相同,所以在中間解析帶有型別的表示式可能很困難。這就是為什麼,例如,C 的型別轉換總是用括號括起型別,例如:

(int)M_PI

Go 語法

C 家族之外的語言通常在宣告中使用不同的型別語法。儘管這是一個獨立的問題,但名稱通常放在前面,後面通常跟著一個冒號。因此,我們上面的例子會變成(在一個虛構但具有啟發性的語言中):

x: int
p: pointer to int
a: array[3] of int

這些宣告很清晰,雖然有些囉嗦——你只需從左到右閱讀它們。Go 從這裡汲取靈感,但為了簡潔,它省略了冒號並移除了一些關鍵字。

x int
p *int
a [3]int

在 [3]int 的外觀和 a 在表示式中的用法之間沒有直接的對應關係。(我們將在下一節討論指標。)你以犧牲單獨的語法來換取清晰度。

現在考慮函式。讓我們轉錄 main 的宣告,就像它在 Go 中讀一樣,儘管 Go 中真正的 main 函式不接受任何引數:

func main(argc int, argv []string) int

表面上看,這與 C 沒什麼不同,除了從 `char` 陣列改為字串,但它從左到右閱讀得很順暢:

函式 main 接受一個 int 和一個字串切片,並返回一個 int。

去掉引數名,它同樣清晰——它們總是放在前面,所以沒有混淆。

func main(int, []string) int

這種從左到右的風格有一個優點,那就是隨著型別的複雜化,它的效果也很好。這是一個函式變數(類似於 C 中的函式指標)的宣告:

f func(func(int,int) int, int) int

或者如果 f 返回一個函式:

f func(func(int,int) int, int) func(int, int) int

它仍然清晰地從左到右閱讀,並且總是顯而易見哪個名稱被宣告——名稱放在最前面。

型別和表示式語法之間的區別使得在 Go 中編寫和呼叫閉包變得容易。

sum := func(a, b int) int { return a+b } (3, 4)

指標

指標是例外,證明了規則。請注意,在陣列和切片中,例如,Go 的型別語法將括號放在型別的左邊,而表示式語法將它們放在表示式的右邊:

var a []int
x = a[1]

為了熟悉,Go 的指標使用 C 的 * 符號,但我們無法讓自己為指標型別做類似的逆轉。因此,指標的工作方式如下:

var p *int
x = *p

我們不能說:

var p *int
x = p*

因為那個字尾 * 會與乘法混淆。我們可以使用 Pascal 的 ^,例如:

var p ^int
x = p^

也許我們應該這樣做(並選擇另一個運算子用於異或),因為型別和表示式上的字首星號在許多方面使事情變得複雜。例如,雖然你可以寫:

[]int("hi")

作為轉換,如果型別以 * 開頭,則必須用括號括起來:

(*int)(nil)

如果我們願意放棄 * 作為指標語法,那麼這些括號將是不必要的。

所以 Go 的指標語法與熟悉的 C 形式繫結,但這些繫結意味著我們無法完全擺脫在語法中使用括號來區分型別和表示式。

總的來說,我們認為 Go 的型別語法比 C 的更容易理解,尤其是在情況變得複雜時。

注意事項

Go 的宣告是從左到右閱讀的。有人指出 C 的宣告是從螺旋形閱讀的!請參閱 David Anderson 的“順時針/螺旋法則”

下一篇文章:透過通訊共享記憶體
上一篇文章:Google I/O 上的 Go 程式設計會議影片
部落格索引