摘要
Go 语法对第一次接触 Go 的新手来有点怪,因为大家习惯了类 C 语法将类型放在前面的方式,对 Go 将类型放在参数后面有点不习惯,刚开始感觉很别扭,那 Go 设计者是基于什么考量才设计成这样呢?这里我们比较一下 C,Go,Haskell 三者的语法,可以看到其实语言的语法其实都是服务于自己的设计目标的。
C 语法
我们先来看一下 C 语法,从大学出来的一般刚开始就是接触的 C,培训出身的刚开始接触的应该是 Java,不过这两者在声明语法上基本一致(当然 Java 简化了很多,像指针就没了),我们就以 C 来看,毕竟 Go 号称新世纪的 C 语言。
简单声明:
代码语言:javascript复制int x;
这里我们将类型放在左边,在右边是一个表达式,因此我们声明指针和数组这样写:
代码语言:javascript复制int *p;
int x[3];
这里*p
的类型是int,x
是一个int类型的数组,x[3]
的类型是int。
函数也遵循这个基本的结构
代码语言:javascript复制int foo(int x)
int foo2(char *arg[])
这是一个很聪明的结构,对于简单类型来说,但是当类型变得复杂后,这个语法就会变得让人迷惑,得费点工夫才能看明白。
声明一个函数指针:
代码语言:javascript复制int (*fp) (int a, int b);
这里 *fp
必须用括号括起来,以表明这是一个函数指针,如果我们有一个函数指针的参数呢?
int (*fp)(int (*fp1) (int a), int b);
这已经变得非常难看懂了,至少在第一眼的时候你看不懂,不管你怎么加空格,如果你觉得还好的话,那我们的返回值是一个函数指针呢?
代码语言:javascript复制int (*(*fp)(int (*)(int, int), int))(int, int)
嗯,反正我已经看不懂了。
Java 里没有函数指针,只有使用接口,这大大简化了类型声明的复杂度,而且 Java 的数组声明也和 C 不一样,为了保持清晰度,Java 将中括号挪到了类型后面 int[] a
, 而不是跟 C 一样 int a[]
将参数放在中间。
Go 语法
Go 将类型放到了后面,我们与 C 比对一下就能发现在复杂情况下 Go 还是能保证基本的类型清晰度。
基本声明
代码语言:javascript复制x int
p *int
a [3]int
p
就是一个int类型的指针,不存在第二种写法,数组也很明确的是类型的一部分。
看下函数:
代码语言:javascript复制func foo(a int, b *int) string
这和 C 感觉也没有多大的差别,而且从左向右读起来也很顺畅。
参数是函数和返回值是参数的情况呢?
代码语言:javascript复制func foo(func(int, int), int) func(float, []int) string
还是非常清晰,从左到右需要的参数和返回值都是一目了然的。
想要说明的一点是数组和指针的使用是和 C 一样的,我们获取数组某个位置的值和指针指向的值:
代码语言:javascript复制x := a[1]
int t = *p
声明和使用中括号和星号的位置反过来了,数组的使用是从 C 继承过来的, 指针的星号放在前面也是为了不和乘号的星号混淆,不过这样我们有时候在使用的时候也避免不了括号。
在我看来,这种情况下不如直接换一个符号来获取指针所指向地址的值,因为星号已经有了两种语义,编译器需要根据上下文来判断星号代表的具体含义。我扫视键盘,觉得@
符号甚好,语义和含义都符合取值的要求,只是不知道语言作者在设计的时候为什么没有考虑好,可能是这个符号没人用过,他们也就顺理成章的沿袭了 C 的语法吧。
Haskell 语法
Haskell 作为一门纯函数式编程语言,大部分人可能听过,但是接触过、学习过的人应该不会太大,毕竟平常工作用不到,我也只是简单的了解过,里面的一些函数式理念对于写出更复用的函数有很强的启发作用,建议大家去了解一下。
Haskell 的语法是与自身为纯函数式的编程语言分不开的,Haskell 不使用括号这种具有边界性质的符号来界定参数,而是使用 ->
开放形式来声明,返回值与入参一样,都是用->
串起来的,使得声明看起来非常的一致。
Haskell 是强类型语言,但是带了一个很强大的类型推导系统,我们在声明变量时不需要指定变量的类型,编译器会根据初始化数据或函数返回值等来判断参数类型,另一方面,Haskell是函数式编程语言,我们声明的类型都是 immutable 的,我们看不到 int a
的情况。
OK, 我们现在来声明一个函数:
代码语言:javascript复制inc :: Int -> Int
inc x = x 1
注:在 Haskell 里,函数是一等公民,这里我将函数的声明类型也写出来只是为了清晰起见,其实我们可以简单只写inc x = x 1
, Haskell 自动推断出相关类型。
我们的入参是一个整数,返回值也是一个整数,从左到右很清晰,如果我们的入参、返回值是函数如何呢?写一个函数式编程里常用的filter
代码语言:javascript复制filter :: (a -> Bool) ->[a] -> [a]
filter _ [] = []
filter f (x:xs)
| f x = x : filter f xs
| otherwise = filter f xs
我们使用括号来界定一个函数,表明这是一个整体,返回值也一样,只需要在后面加上括号就可以了,可以看到也是非常清楚明白的。
Haskell 为什么要这样设计? 这和 Haskell 语言的函数式本质是分不开的。函数式里面有一个术语叫柯里化,柯里化后的函数可以一次只接收一个参数,每次返回一个新的函数,直到所有的参数都满足了,才会触发计算返回最终值,而 Haskell 里的函数默认是全部柯里化的,譬如我们想过滤出列表里所有偶数,我们可以这样写:
代码语言:javascript复制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 语言的语法上,一些其他的语言像函数式编程语言,声明式编程语言的编程思想对我们也会有很大的启发,多涉猎一下,对我们思考问题的思路会有很大的启发作用。