Go 变量声明语法的由来

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

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] (忽略特定的索引值,该值被理解为数组的大小) 具有类型诠释。

那么函数呢? 最初, 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 没有直接对应关系。 (我们将在下一节中返回指针。) 通过一个不同寻常的写法可以让你的代码更清晰。

现在看看函数。 但我是用 Go 来转换下 C 中 main 函数的声明, 虽然在 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 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^

或许我们可以这样 (为 xor 选择其他运算符), 因为类型和表达式的前缀星号在许多方面让事情变得更复杂。 例如, 一个这样写的例子

[]int("hi")

以 * 开头的类型在转换时必须加上括号:

(*int)(nil)

如果我们可以放弃 * 作为指针语法, 那么可以不需要这些括号。

为此 Go 的指针语法与 C 家族的有所牵扯, 但是这些联系意味着我们无法完全摆脱使用括号消除语法中类型和表达式歧义的麻烦。

总而言之, 我们认为 Go 的类型语法比 C 的语法更易于理解, 尤其是当情况变得复杂时。

注意

Go 的声明从左往右读。 有人提到 C 是螺旋式读取! 请参阅 David Anderson 的 “顺时针/螺旋规则”.

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/go-blog/gos-dec...

译文地址:https://learnku.com/docs/go-blog/gos-dec...

上一篇 下一篇
Summer
贡献者:3
讨论数量: 0
发起讨论 只看当前版本


暂无话题~