写点什么

Go 的声明语法为什么是这样

用户头像
Rayjun
关注
发布于: 2021 年 01 月 09 日
Go的声明语法为什么是这样

作为 Go 语言的初学者,一定觉得 Go 语言的声明语法很奇怪,我当然也不例外。对于这个问题,Go 语言官方写了一篇博客来回答这个问题,我觉的挺有意思的,就把博客翻译了一下。


总的来说, GO 的声明语法遵循一个原则,就是让声明可以从左往右读,由于指针沿用了 C 语言的风格,所以指针是一个例外,下面是原文的翻译。


原文地址:https://blog.golang.org/declaration-syntax


简介

很多 Go 语言的初学者都会奇怪,为什么声明语法与 C 语言族(Rayjun 注:C++,Java 等语言)的差异这么大。在这篇文章中,我们会对比着两种方式,并且解释为什么 Go 语言要这样来实现。


C 语法

首先,让我们来讨论一下 C 语言的语法。C 语言选择了一种不寻常而且巧妙的方式来声明语法。不用特殊的语法来描述类型,而是写一个包含被声明项的表达式,并声明该表达式将具有什么类型,就像下面这样:

int x
复制代码

把 x 声明为一个 int 意味着表达式 x 的类型是 int。通常,要写一个某种类型的变量,只需要将类型放在左边,将表达式放在右边。因此,声明的形式如下:


int *pint a[3];
复制代码

因为 *p 的类型是 int,所以 p 是一个指向 int 的指针,因为 a[3] 的类型是 int,所以 a 是一个 int 数组(忽略中括号中的这个数字,这个表示数组的长度)。


那么函数呢?本来,C 语言是把参数的类型写在括号之外,如下所示(看起来很奇怪):

int main(argc, argv)    int argc;    char *argv[];{ /* ... */ }
复制代码


我们再看一个方法,现在 C 语言的方法声明就是用的这种方式,因为这个表达式 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[]
复制代码


你可以删除中间的命名来构造它的类型,尽管你把这个命名放在中间并不明显。


看一下如果把所有参数的名称都删除会变成什么样子:

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: intp: pointer to inta: array[3] of int
复制代码


这些声明是清晰的,只需从左到右读取即可,但有些冗长。Go 从这里得到提示,但为了简洁起见,它去掉了冒号并删除了一些关键字:

x intp *inta [3]int
复制代码


这里看不出来 [3]int 的外观以及如何在表达式中使用 a(在下一节中我们会回到指针上)。为了获得清晰将付出一些语法的代价。


现在来看看方法。我们把 main 方法用 Go 语言来转写,尽管实际在 Go 语言中 main 方法是没有参数的:

func main(argc int, argv []string) int
复制代码


表面上看起来和 C 语言没有太多的不同,无非是从 char 数组到 string 数组的转变,但是它可以从左读到右:

方法 main 有 int 和 string slice 参数,并且返回一个 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)
复制代码


指针

指针是一个意外情况。看下面的例子,注意在组数和 slice 中,Go 的声明语法把中括号放在类型的左边,而表达式语法把中括号放在了右边:

var a []intx = a[1]
复制代码


Go 的指针使用了 C 语言中的 * 符号,但是我们不能对指针类型进行类似的转换。所以指针是这样声明的:

var p *intx = *p
复制代码


但不能这样用:

var p *intx = p*
复制代码


因为后缀 * 会和乘法的语法冲突。如果不用 * ,我们可以用 Pascal 语言中 ^ 符号,就像下面这样:

var p ^intx = p^
复制代码


也许我们应该为指针换一个操作符( 并为异或操作选择另一个操作符),因为类型和表达式上的前缀星号在许多方面把事情变得复杂。例如,有人可能会这样写:

[]int("hi")
复制代码


相反,如果类型以 * 开头,则必须将其括起来:

(*int)(nil)
复制代码


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


因此,Go 的指针语法与 C 语言的指针形式绑定在一起,这种绑定意味着我们还需要使用圆括号来消除类型和表达式的歧义。


尽管如此,我们还是相信 Go 的类型语法比 C 语言更容易理解,特别是当声明变得复杂的时候。

总结

总而言之,Go 语言的声明语法是从左读到右。有人认为 C 语言的声明语法有些混乱,详情请看 David Anderson 写的这篇 http://c-faq.com/decl/spiral.anderson.html



另外,腾讯云区块链方向在大量招人,包括前端、后端、架构师、产品等诸多岗位,如果感兴趣,请把简历投过来 rayjun0412@gmail.com。


文 / Rayjun

本文首发于微信公众号


发布于: 2021 年 01 月 09 日阅读数: 19
用户头像

Rayjun

关注

程序员,王小波死忠粉 2017.10.17 加入

非著名程序员,还在学习如何写代码,公众号同名

评论

发布
暂无评论
Go的声明语法为什么是这样