为何 Go 的声明语法有点怪?(语法比较)

原创
2018/08/28 16:20
阅读数 848

摘要

Go 语法对第一次接触 Go 的新手来有点怪,因为大家习惯了类 C 语法将类型放在前面的方式,对 Go 将类型放在参数后面有点不习惯,刚开始感觉很别扭,那 Go 设计者是基于什么考量才设计成这样呢?这里我们比较一下 C,Go,Haskell 三者的语法,可以看到其实语言的语法其实都是服务于自己的设计目标的。

C 语法

我们先来看一下 C 语法,从大学出来的一般刚开始就是接触的 C,培训出身的刚开始接触的应该是 Java,不过这两者在声明语法上基本一致(当然 Java 简化了很多,像指针就没了),我们就以 C 来看,毕竟 Go 号称新世纪的 C 语言。

简单声明:

int x;

这里我们将类型放在左边,在右边是一个表达式,因此我们声明指针和数组这样写:

int *p;
int x[3];

这里*p 的类型是int,x 是一个int类型的数组,x[3] 的类型是int。

函数也遵循这个基本的结构

int foo(int x)
int foo2(char *arg[])

这是一个很聪明的结构,对于简单类型来说,但是当类型变得复杂后,这个语法就会变得让人迷惑,得费点工夫才能看明白。

声明一个函数指针:

int (*fp) (int a, int b);

这里 *fp 必须用括号括起来,以表明这是一个函数指针,如果我们有一个函数指针的参数呢?

int (*fp)(int (*fp1) (int a), int b);

这已经变得非常难看懂了,至少在第一眼的时候你看不懂,不管你怎么加空格,如果你觉得还好的话,那我们的返回值是一个函数指针呢?

int (*(*fp)(int (*)(int, int), int))(int, int)

嗯,反正我已经看不懂了。

Java 里没有函数指针,只有使用接口,这大大简化了类型声明的复杂度,而且 Java 的数组声明也和 C 不一样,为了保持清晰度,Java 将中括号挪到了类型后面 int[] a, 而不是跟 C 一样 int a[] 将参数放在中间。

Go 语法

Go 将类型放到了后面,我们与 C 比对一下就能发现在复杂情况下 Go 还是能保证基本的类型清晰度。

基本声明

x int
p *int
a [3]int

p 就是一个int类型的指针,不存在第二种写法,数组也很明确的是类型的一部分。

看下函数:

func foo(a int, b *int) string

这和 C 感觉也没有多大的差别,而且从左向右读起来也很顺畅。

参数是函数和返回值是参数的情况呢?

func foo(func(int, int), int) func(float, []int) string

还是非常清晰,从左到右需要的参数和返回值都是一目了然的。

想要说明的一点是数组和指针的使用是和 C 一样的,我们获取数组某个位置的值和指针指向的值:

x := a[1]
int t = *p

声明和使用中括号和星号的位置反过来了,数组的使用是从 C 继承过来的, 指针的星号放在前面也是为了不和乘号的星号混淆,不过这样我们有时候在使用的时候也避免不了括号。

在我看来,这种情况下不如直接换一个符号来获取指针所指向地址的值,因为星号已经有了两种语义,编译器需要根据上下文来判断星号代表的具体含义。我扫视键盘,觉得@ 符号甚好,语义和含义都符合取值的要求,只是不知道语言作者在设计的时候为什么没有考虑好,可能是这个符号没人用过,他们也就顺理成章的沿袭了 C 的语法吧。

Haskell 语法

Haskell 作为一门纯函数式编程语言,大部分人可能听过,但是接触过、学习过的人应该不会太大,毕竟平常工作用不到,我也只是简单的了解过,里面的一些函数式理念对于写出更复用的函数有很强的启发作用,建议大家去了解一下。

Haskell 的语法是与自身为纯函数式的编程语言分不开的,Haskell 不使用括号这种具有边界性质的符号来界定参数,而是使用 -> 开放形式来声明,返回值与入参一样,都是用-> 串起来的,使得声明看起来非常的一致。

Haskell 是强类型语言,但是带了一个很强大的类型推导系统,我们在声明变量时不需要指定变量的类型,编译器会根据初始化数据或函数返回值等来判断参数类型,另一方面,Haskell是函数式编程语言,我们声明的类型都是 immutable 的,我们看不到 int a 的情况。

OK, 我们现在来声明一个函数:

inc :: Int -> Int
inc x = x + 1

注:在 Haskell 里,函数是一等公民,这里我将函数的声明类型也写出来只是为了清晰起见,其实我们可以简单只写inc x = x + 1, Haskell 自动推断出相关类型。

我们的入参是一个整数,返回值也是一个整数,从左到右很清晰,如果我们的入参、返回值是函数如何呢?写一个函数式编程里常用的filter

filter :: (a -> Bool) ->[a] -> [a]
filter _ [] = []
filter f (x:xs) 
     | f x = x : filter f xs
     | otherwise = filter f xs

我们使用括号来界定一个函数,表明这是一个整体,返回值也一样,只需要在后面加上括号就可以了,可以看到也是非常清楚明白的。

Haskell 为什么要这样设计? 这和 Haskell 语言的函数式本质是分不开的。函数式里面有一个术语叫柯里化,柯里化后的函数可以一次只接收一个参数,每次返回一个新的函数,直到所有的参数都满足了,才会触发计算返回最终值,而 Haskell 里的函数默认是全部柯里化的,譬如我们想过滤出列表里所有偶数,我们可以这样写:

list1 = filter even a
list2 = filter even b

这里a/b都是列表,你有没有发现filter even 我们写了两边,秉持DRY原则,我们可以将它抽出来变成一个函数:

filterEven = filter even
list1 = filterEven a
list2 = filterEven b

我们只对filter 提供一个参数,返回值是一个接收一个list参数的函数,我们就可以复用我们新的函数了。 回过头来我们再看一下 Haskell 的函数声明语法a -> b -> c,其实这里面没有什么入参、返回值的区别,函数从左到右接收参数,返回值就是最后参数后面的部分,也就是说我们提供了一个参数a,返回就是b -> c, 是不是很熟悉,这就是一个函数,我们可以按正常的函数来使用,因为它于正常函数的声明是一模一样的。

一点思维发散

昨天(2018.09.26)在路上走着突然又想起来这个,C 语言的声明语法可类比中国人的姓名,而 Go语言的声明语法可类比美国人的名姓。中国人的先姓后名导致一般孩子随父亲姓的话,不太可能将妈妈的姓也加进来,比如魏随风,加入另一个姓变成魏张随风,魏马随风很奇怪,美国人的名字后面可以加任意多的姓,Anderson Ma Li Zhang,而且也相对清晰。

总结

各个语言在设计时总要小心的考虑自己的声明语法,要使它符合自己的设计目标,同时语法又要尽可能的简单、清晰、易用,Go 在 C 语法上的基础上做了一点改进,就让一些复杂情况变得清晰了,可见也是下了很大功夫的。同时我们也不要仅仅局限在类 C 语言的语法上,一些其他的语言像函数式编程语言,声明式编程语言的编程思想对我们也会有很大的启发,多涉猎一下,对我们思考问题的思路会有很大的启发作用。

展开阅读全文
打赏
0
0 收藏
分享
加载中
52iSilence7博主

引用来自“OSC_KulLHJ”的评论

类型放在后面,是编译器正常逻辑就是后解释返回类型的,可能是考虑到最优化,Go在构建时就直接后置类型了,这个要去了解下编译相关原理
这个应该不会影响编译速度,而主要是为了清晰度考虑
2018/08/29 11:06
回复
举报
类型放在后面,是编译器正常逻辑就是后解释返回类型的,可能是考虑到最优化,Go在构建时就直接后置类型了,这个要去了解下编译相关原理
2018/08/28 19:18
回复
举报
更多评论
打赏
2 评论
0 收藏
0
分享
返回顶部
顶部