這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
作者:Rob Pike,原文連結:Go's Declaration Syntax
以下是譯文:
前言
Go 的初學者可能會有這樣的疑問:為什麼 Go 的聲明文法與傳統的其他 C 家族程式設計語言不太一樣?在這篇文章中我們會比較這兩種不同的方式,並且也會解釋為什麼。
C 變數
首先,讓我們說說 C 中的文法。C 使用了一種不尋常的巧妙的方法來實現聲明文法。我們不是用什麼特殊的文法來描述類型,而是寫一個運算式,這個運算式包含兩個部分:被聲明的變數和變數的類型。
int x;
上面這行代碼聲明了一個類型為 int 的變數 x。一般來說,為了弄清楚如何編寫新變數的類型,可以先寫一個含基本類型變數的運算式,然後將基本類型放在左邊,將運算式放在右邊。
因此,下面的聲明:
int *p;int a[3];
描述的是 p 是一個指向 int 類型的指標,因為 ‘*p’ 的類型為 int。而 a 是一個 int 類型的數組,因為 ‘a[3]’ (這裡請忽略下標的值 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 *agrv[]
通過把變數名放在中間來聲明類似 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: intp: pointer to inta: array[3] of int
這些聲明是明確的,如果從左往右讀你會發現也是詳細的。Go 語言從中得到了啟發,但為了簡潔起見,刪除了冒號和一些關鍵字:
x intp *inta [3]int
這個例子中 [3]int 與如何在運算式中使用 a 這兩者似乎沒有直接的對應。(後面一小節中我們會講到指標的。)你可以通過單獨的文法來獲得清晰的結果。
現在讓我們考慮下函數。讓我們把這個聲明寫成 Go 的形式,儘管在 Go 中真正的 main 函數是沒有入參的:
func main(argc int, argv []string) int
表面上這和 C 語言並沒什麼不同,除了將字元數組改成了字串形式。但是從左往右讀起來卻很順暢:
函數 main 需要傳入一個整型和字串切片並且返回一個整型。(譯者註:直到譯者看到這篇文章,譯者才發現原來這麼寫讀起來竟這麼順暢。。。)
即便捨去變數名還是很明確——因為對於型別宣告上沒有位置的變化,所以也沒有什麼困惑。
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 []intx = a[1]
為了讓大家有一種熟悉的感覺,Go 的指標同樣延續 C 語言中的 * 符號,但是我們不能簡單的將指標類型也反轉一下。所以指標使用方式如下:
var p *intx = *p
我們不能簡單粗暴地改成這樣:
var p *intx = p*
因為尾碼 會與乘法的 相混淆。那或許我們可以使用 ^,舉個例子:
var p ^intx = p^
但同樣的這個符號也已經有其他含義了,類型和運算式在首碼尾碼的問題上總是在許多方面使事情複雜化。舉個例子,
[]int("hi")
這是一種寫法,但一旦以 * 打頭就必須用括弧將其包住:
(*int)(nil)
如果我們願意放棄 * 作為指標文法,那麼這些括弧就不是必要的了。(譯者註:但還能有更好的指標文法嗎。。。)
所以 Go 的指標文法與熟悉的 C 語言是類似的,但這個關聯也意味著我們不得不使用括弧來消除文法中的類型和運算式之間的差異。
總體而言,我們相信 Go 的類型文法比 C 的要更容易理解,尤其是當事情變得複雜的時候。