>[info] 原文地址: https://blog.golang.org/gos-declaration-syntax
**介绍**
Go的入门者会疑惑为什么声明语法和传统C家族的语法不一样。本文我们会比较这2种方式,并且解释为什么Go会这样声明。
**C语法**
首先,我们说说C语法。C在声明语法上用了一个不同寻常很聪明的做法。并没有使用特殊语法来描述类型,而是编写一个包含已被声明项的表达式,并说明这个表达式将具有什么类型。因此
~~~
int x;
~~~
声明x为int类型:表达式'x'将会具有int类型。一般来说,为了弄明白如何书写一个新变量的类型,就要编写一个包含该变量的表达式,假如其求值为基本类型,那就把基本类型放在左边,表达式放在右边。
如下,声明如:
~~~
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 *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数组变成了string类型,但是它更适合从左往右读:
main方法接收一个int参数,和一个strings片段参数,然后返回一个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数组
~~~
但是如果类型前面以\*开始,就必须将其括起来:
~~~
(*int)(nil)
~~~
假如我们愿意放弃\*作为指针语法,那么这些括号就不是必须的了。
所以Go的指针语法,依赖于熟悉的C形式,但是这种联系,也意味着我们不能完全打破陈规:使用加括号,来消除类型和表达式在语法上的歧义。
总之,无论怎样,我们相信Go的类型语法比C的更容易懂,特别是在复杂的情况下。
**备注**
Go的声明阅读顺序是从左到右。而已经有人指出,C的声明读法则是螺旋式的。可以参见David Anderson的[The "Clockwise/Spiral Rule"](http://c-faq.com/decl/spiral.anderson.html)
本文由Rob Pike著