Go 部落格

Go 的宣告語法

Rob Pike
2010 年 7 月 7 日

簡介

C 家族建立的傳統讓 Go 的新使用者對其宣告語法感到不解。在本文中我們會比較這兩種方法並解釋為何 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 選取一個整數和一個字串切片,然後傳回一個整數。

省略參數名稱,它還是很清楚,因為參數名稱總是排在第一個,所以不會混淆。

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 的宣告呈螺旋狀!查看大衛·安德森的「時針/螺旋規則」

下一篇文章:透過通訊分享記憶體
前一篇文章:Google I/O 的 Go 編程課程影片
部落格索引