Go 博客
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 *[] 类型的东西并不明显。
如果不命名参数,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
表面上看,除了从 char
数组变为字符串之外,这与 C 没有太大区别,但它从左到右读起来很顺畅
函数 main 接收一个 int 和一个字符串切片,并返回一个 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)(nil)
如果我们愿意放弃将 * 用作指针语法,那些括号就不必要了。
所以 Go 的指针语法与熟悉的 C 形式紧密相连,但这种联系意味着我们无法完全摆脱使用括号来消除语法中类型和表达式的歧义。
不过,总的来说,我们认为 Go 的类型语法比 C 的更容易理解,尤其是在事情变得复杂时。
备注
Go 的声明是从左到右阅读的。有人指出,C 的声明是按螺旋状阅读的!请参阅 David Anderson 的 “顺时针/螺旋规则”。
下一篇文章:通过通信共享内存
上一篇文章:Google I/O 的 Go 编程会议视频
博客索引