Go 博客

Go 的声明语法

Rob Pike
2010 年 7 月 7 日

简介

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

表面上这与 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 编程会议视频
博客索引