爲何 Go 語言把類型放在後面?

本文整理自知乎,原文做者 @林建入。golang

不是爲了不同凡響。而是爲了更加清晰易懂。數組

Rob Pike 曾經在 Go 官方博客解釋過這個問題(原文地址:http://blog.golang.org/gos-declaration-syntax),簡略翻譯以下(水平有限翻譯的不對的地方見諒):閉包

引言

Go語言新人經常會很疑惑爲何這門語言的聲明語法(declaration syntax)會和傳統的C家族語言不一樣。在這篇博文裏,咱們會進行一個比較,並作出解答。函數

C 的語法

首先,先看看 C 的語法。C 採用了一種聰明而不一樣尋常的聲明語法。聲明變量時,只需寫出一個帶有目標變量名的表達式,而後在表達式裏指明該表達式自己的類型便可。好比:翻譯

int x;

上面的代碼聲明瞭 x 變量,而且其類型爲 int——即,表達式 xint 類型。通常而言,爲了指明新變量的類型,咱們得寫出一個表達式,其中含有咱們要聲明的變量,這個表達式運算的結果值屬於某種基本類型,咱們把這種基本類型寫到表達式的左邊。因此,下述聲明:指針

int *p;
int a[3];

指明瞭 p 是一個int類型的指針,由於 *p 的類型爲 int。而 a 是一個 int 數組,由於 a[3] 的類型爲 int(別管這裏出現的索引值,它只是用於指明數組的長度)。code

咱們接下來看看函數聲明的狀況。C 的函數聲明中關於參數的類型是寫在括號外的,像下面這樣:blog

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

如前所述,咱們能夠看到 main 之因此是函數,是由於表達式 main(argc, argv) 返回 int。在現代記法中咱們是這麼寫的:索引

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

儘管看起來有些不一樣,可是基本的結構是同樣的。get

總的來看,當類型比較簡單時,C的語法顯得很聰明。可是遺憾的是一旦類型開始複雜,C的這套語法很快就能讓人迷糊了。著名的例子如函數指針,咱們得按下面這樣來寫:

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 語言所採用的方案就是以此爲基礎的,但爲了追求簡潔性,Go 語言丟掉了冒號並去掉了部分關鍵詞,成了下面這樣:

x int
p *int
a [3]int

[3]int 和表達式中 a 的用法沒有直接的對應關係(咱們在下一節會回過頭來探討指針的問題)。至此,你得到了代碼清晰性方面的提高,但付出的代價是語法上須要區別對待。

下面咱們來考慮函數的問題。雖然在 Go 語言裏,main 函數實際上沒有參數,可是咱們先謄抄一下以前的 main 函數的聲明:

func main(argc int, argv *[]byte) int

粗略一看和 C 沒什麼不一樣,不過自左向右讀的話還不錯。

main 函數接受一個 int 和一個指針並返回一個 int

若是此時把參數名去掉,它仍是很清楚——由於參數名總在類型的前面,因此不會引發混淆。

func main(int, *[]byte) int

這種自左向右風格的聲明的一個價值在於,當類型變得更復雜時,它依然相對簡單。下面是一個函數變量的聲明(至關於 C 語言裏的函數指針)

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

或者當它返回一個函數時:

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

上面的聲明讀起來仍是很清晰,自左向右,並且究竟哪個變量名是當前被聲明的也容易看懂——由於變量名永遠在首位。

類型語法和表達式語法帶來的差異使得在 Go 語言裏調用閉包也變得更簡單:

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

指針

指針有些例外。注意在數組 (array )和切片 (slice) 中,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^

咱們也許還真的應該把 * 像上面這樣改爲 ^ (固然這麼一改 xor 運算的符號也得改),由於在類型和表達式中的 * 前綴確實把好些事兒都搞得有點複雜,舉個例子來講,雖然咱們能夠像下面這樣寫

[]int("hi")

但在轉換時,若是類型是以 * 開頭的,就得加上括號:

(*int)(nil)

若是有一天咱們願意放棄用 * 做爲指針語法的話,那麼上面的括號就能夠省略了。

可見,Go 的指針語法是和 C 類似的。但這種類似也意味着咱們沒法完全避免在文法中有時爲了不類型和表達式的歧義須要補充括號的狀況。

總而言之,儘管存在不足,但咱們相信 Go 的類型語法要比 C 的容易懂。特別是當類型比較複雜時。

相關文章
相關標籤/搜索