💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
>[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著