Go 的声明语法

Go’s Declaration Syntax - Go 的声明语法

原文:https://go.dev/blog/declaration-syntax

Rob Pike 7 July 2010

2010年7月7日

介绍

​ 对于 Go 的新手来说,他们会想知道为什么声明语法与 C 系列的传统不同。在这篇文章中,我们将比较两种方法,并解释为什么 Go 的声明看起来像这样。

C 语法

​ 首先,让我们来谈谈 C 语法。C 对声明语法采取了一种不同寻常而聪明的方法。与其使用特殊语法来描述类型,不如编写一个表达式,涉及到被声明的项,并指明该表达式的类型。因此,

1
int x;

声明 x 是 int 类型:表达式"x"将具有 int 类型。一般来说,要想知道如何写一个新变量的类型,请编写一个涉及到该变量的表达式,该表达式的结果为基本类型,然后将基本类型放在左边,将表达式放在右边。

因此,声明

1
2
int *p;
int a[3];

说明 p 是 int 类型的指针,因为"*p“具有 int 类型,并且 a 是 int 数组,因为 a[3](忽略特定索引值,该值被解释为数组的大小)具有 int 类型。

​ 最初,C 的函数声明将参数类型写在括号外,如下所示:

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

​ 同样,我们可以看到 main 是一个函数,因为表达式 main(argc, argv) 返回一个 int。在现代的记法中,我们会写成那么函数呢?

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

但基本结构是相同的。

​ 这是一个聪明的语法想法,适用于简单类型,但很容易变得混乱。其中一个著名的例子是声明函数指针。按照规则,您会得到以下结果:

1
int (*fp)(int a, int b);

在这里,fp是一个指向函数的指针,因为如果您写表达式(*fp)(a, b),您将调用一个返回int的函数。如果fp的一个参数本身是一个函数会怎样呢?

1
int (*fp)(int (*ff)(int x, int y), int b)

这变得越来越难读了。

​ 当然,当我们声明一个函数时,可以省略参数的名称,所以main可以这样声明:

1
int main(int, char *[])

​ 回想一下,argv的声明是这样的:

1
char *argv[]

​ 所以您可以从它的声明中间删除名称来构建它的类型。然而,很难想象通过将名称放在中间来声明一个char *[]类型。

​ 如果不给参数命名,fp的声明会怎样呢:

1
int (*fp)(int (*)(int, int), int)

不仅不明显在

1
int (*)(int, int)

哪里放置名称,它甚至不清楚它是一个函数指针声明。如果返回类型是一个函数指针呢?

1
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 函数不带参数:

1
func main(argc int, argv []string) int

​ 从表面上看,这与 C 不太不同,除了从字符数组到字符串的更改外,但从左到右读起来很好:

​ 函数 main 接受一个 int 和一个字符串切片,并返回一个 int。

​ 去掉参数名称,它仍然很清晰 - 它们总是在第一位,因此没有混淆。

1
func main(int, []string) int

​ 这种从左到右的风格的一个优点是,在类型变得更加复杂时,它的工作效果很好。这里是一个函数变量的声明(类似于 C 中的函数指针):

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

或者如果 f 返回一个函数:

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

​ 它仍然可以清晰地从左到右阅读,并且始终明确正在声明哪个名称 - 名称在第一位。

​ 类型和表达式语法之间的区别使得在 Go 中编写和调用闭包变得容易:

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

指针

​ 指针是证明这个规则的例外。请注意,在数组和切片中,Go 的类型语法将括号放在类型的左侧,但表达式语法将括号放在表达式的右侧:

1
2
var a []int
x = a[1]

​ 为了熟悉起见,Go 的指针使用了来自 C 的 * 符号,但我们无法将指针类型的括号做出类似的颠倒。因此指针的工作方式如下:

1
2
var p *int
x = *p

我们无法写成

1
2
var p *int
x = p*

​ 因为后缀的 * 会与乘法混淆。我们本可以使用 Pascal 中的 ^,例如:

1
2
var p ^int
x = p^

也许我们应该这样做(并选择另一个操作符作为xor异或),因为类型和表达式上的前缀星号在许多方面都会使事情变得复杂。例如,虽然可以编写

1
[]int("hi")

作为转换,但如果类型以 * 开头,则必须将其括在括号中:

1
(*int)(nil)

如果我们愿意放弃 * 作为指针语法,那么这些括号就是不必要的。

​ 因此,Go 的指针语法与熟悉的 C 形式相结合,但这些关系意味着我们不能完全摆脱使用括号来消除语法中类型和表达式的歧义。

​ 总体而言,我们认为 Go 的类型语法比 C 更容易理解,特别是当事情变得复杂时。

Notes

​ Go 的声明从左到右读取。David Anderson 在其 “[Clockwise/Spiral Rule](Clockwise/Spiral Rule)” 中指出了 C 的声明以螺旋形读取!

最后修改 October 10, 2024: 更新 (a4b8f85)