【譯】Go語言宣告語法

ligand發表於2019-03-29

引言

Go語言的初學者可能會好奇為什麼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 *[]的變數,你得把變數名放在char *[]中間,顯然這樣不夠直觀。
如果不宣告引數的名字,來看看fp的宣告會是什麼樣?

int (*fp)(int (*)(int, int), int)
複製程式碼

這樣不僅不知道變數名該放在哪

int (*)(int, int)
複製程式碼

而且很難看出來這是在宣告一個函式指標。進一步,如果該函式的返回型別也是一個函式指標呢?

int (*(*fp)(int (*)(int, int), int))(int, int)
複製程式碼

這裡甚至很難看出這其中變數名為fp。
這樣精心構建的例子還有很多,這就說明C的變數宣告語法能有多複雜。
還有一點學要提下,就是型別轉換和型別宣告語法是一樣的,這使得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改成了strings。但Go版本更加適合從左向右閱讀:
main函式接受一個int和一個strings的切片,返回一個int。
去掉引數名也一樣簡潔,因為變數名永遠在宣告的最前面,所以這不會造成迷惑。

func main(int, []string) int
複製程式碼

這種從左到右的宣告語法有一個非常實用的地方:當型別非常複雜的時候,它也能很清晰。下面是一個函式變數的宣告(類比C中的函式指標):

f func (func(int, int) int, int) int
複製程式碼

或者返回值也是一個函式

f func(func(int, int) int, int) func(int, int) int
複製程式碼

從左到右讀起來依然非常清晰,變數的名字也非常明顯,因為它總是第一次出現。
型別轉換和表示式之間的語法區別使得寫和呼叫閉包都非常容易:

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*
複製程式碼

因為字尾*與乘法衝突。我們也曾使用過^這個符合,例如:

var p ^int
x = p^
複製程式碼

或許我們應該這麼寫(使用其他的符號表示異或),因為在型別宣告和表示式中同時使用字首*在一些情況下會很複雜。例如

[]int("hi")
複製程式碼

為了型別轉換,如果型別以*開頭就必須加括號:

(*int)(nil)
複製程式碼

如果不使用*作為指標語法,那就不用打括號了。
所以Go的指標嘗試接近C的形式,這就意味著不能完全去掉區分型別宣告和表示式的括號。 總的來說,我們相信Go的型別宣告語法比C更容易理解,特別是在複雜的情況下。

總結

Go的型別宣告語法是從左到右的,而C的型別宣告語法則是螺旋形的!具體參考David Anderson的這篇文章 The "Clockwise/Spiral Rule"

個人總結

Go語言型別宣告語法中,永遠將變數名放在最前面,這比C系語言更加清晰,更適合從左向右閱讀。
但是型別轉換似乎沒有多大區別,都需要括號,只是括號的物件不同而已:

C:  (type_name) expression;
Go: type_name(expression)
複製程式碼

原文

Go's Declaration Syntax

相關文章