Go 语言的声明语法比较“别致”,博主学习的时候看到官方的“安利”博文感觉写的很好也很有意思,能帮助理解其规律,以避免在遇到 Go 语言的复杂声明的时候犯错,顺道翻译了一下。原文地址: Go's Declaration Syntax

介绍

Go 语言初学者肯定会对其不同于传统 C 语言家族的声明语法感到奇怪。本文将会对比这两种方案并解释为什么 Go 语言要这么设计。

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(argc, argv) 返回 int 类型,所以确认 main 是个函数,在现代语法中会这样写 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 没有直接对等关系(下一章节会回头在讨论指针)。以一个单独语法的代价,你获得了明晰的语法规则。

现在考虑函数,把 C 语言中的 main 函数转化到 Go 语言中(虽然 Go 也有 main 函数但是实际上是没有参数的):

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

不像 char 数组到 strings 的变化,这个声明表面上看好像和 C 语言没有太大不同,但是从左往右阅读起来很爽:

  • 函数 main 接受一个 int 参数和一个 slice 参数,且元素为 string,并且 main 函数返回 int 。

哪怕丢掉参数名,也很清晰,因为名字总是在最前面,所以没有歧义:

func main(int, []string) int

这种从左往右风格的另外一个好处是,类型变得复杂起来其依旧能很好的工作。这里声明了一个函数变量(与 C 语言里面的函数指针类似):

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

或者,返回一个函数:

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 u语言采用了 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