Go 博客

Go 的声明语法

Rob Pike
2010 年 7 月 7 日

引言

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

表面上看,这与 C 没什么不同,除了从 `char` 数组改为字符串,但它从左到右阅读得很顺畅:

函数 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 编程会议视频
博客索引