Go的声明语法
引言
Go的声明语法与C家族的声明语法不同,在Go’s Declaration Syntax 中解释了为什么Go要选择这种与众不同的语法。本文为该文的翻译,个人水平有限,权做参考,如有错误,欢迎指出
C 语法
先来看看C语言的语法。C语言采用了一种不寻常但是巧妙地声明语法——不是用特殊语法描述类型,而是编写一个包含要声明的项的表达式,并声明该表达式的类型。如:
int x;
将x声明为int类型:表达式x
的类型是int。一般来说,要声明一个新变量的类型,需要写一个包含该变量的表达式,该表达式可以求值为一个基本类型,然后把基本类型放在左边,表达式放在右边。
因此,就有了如下的声明:
int *p;
int a[3];
上例中,因为*p
拥有类型int
,所以p
是一个指向int
的指针,a[3]
拥有类型int
,所以a
是一个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[]
因此,这是通过将名字从其声明中间删除来构造它的类型。 如果将参数名从函数声明中移除的话,上述函数声明就变成了:
int (*)(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没有直接的对应关系。你通过一个单独的语法为代价得到了清晰的语法
现在在来看看函数。尽管实际上Go中的main
函数没有参数,但还是可以看看在Go中如何声明之前提到的main
函数:
func main(argc int, argv []string) int
表面上看与C语言没有太大的差异,其他的不同是char
数组变成了string
数组,但是从左到右读阅读该声明式很方便的:
function main takes an int and a slice of strings and returns an int
main函数接收一个int参数和一个string切片,并返回一个int
将参数的名字删除也同样清晰——名字总是在最前面,因此不会搞错
func main(int, []string) int
这种从左到右的风格的一个优点是在类型变得复杂的时候也同样清晰。比如声明中一个函数变量(与C中的函数指针类似):
f func(func(int, int), int) int
或者返回一个函数:
f func(func(int, int), int) func(int, int) int
从左到右读起来依然是很清晰的,而且总是和在声明哪个名字——名字在最前面
类型和表达式语法之间的差别使得在Go中编写和调用闭包变得很容易:
sum := func(a, b int) int { return a + b } (3, 4)
指针
指针是证明规则的例外。请注意,Go的类型语法将括号放在类型的左侧,而表达式语法将括号放在表达式的右侧:
var a[] int
a = 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相关联,这些联系意味着我们不能完全放弃用括号来消除语法中的类型和表达式的歧义。
总而言之,我们相信比起C的语法,Go的语法更加容易理解,特别是当事情变得复杂的时候。
Notes
Go的声明式从左往右读的,而有人指出C的读法的螺旋的! See The “Clockwise/Spiral Rule” by David Anderson.