fatcat 枝头不见绿,蓄势待春风

golang: 新时代的编程语言

2018-03-03
fatcat22

春节假期忙里愉闲,学了一下久仰大名的Go语言。然后就被Go的精妙设计所折服,不禁在内心感叹:这真的是大师手笔。

Go语言最为人称道的特性就是原生支持程序的并行运行,且使用起来非常简单(想想用C++和Python写一个多线程程序有多麻烦)。除此之外,随着我对Go语言的不断了解,心里经常会冒出“原来还可以这样”或者“早就应该这样”的感叹。下面就介绍一些Go语言里我认为非常赞的地方。


只有for循环,没有while、do/while、until
在Go语言里,普通的循环语句只有for循环,但表达能力并没有减弱。使用for关键字可以实现多数常用的循环表达:
传统的计数循环

for i := 0; i < n; i++ {
  //do something
}

while(condition)循环

for ; condition; {
  //do something
}

foreach循环

for k, v := range a_map {
  //do something
}

while(true)循环

for {
  //do something
}

有人觉得一个关键字这么多表达方式,非常混乱。这是个见仁见智的事情,我个人非常欣赏这种方式。(我尤其不能理解为什么有些语言中要有until循环)
这是一个非常小的特性,但要想舍弃其它语言都有的东西,需要一些勇气。见微知著,Go的设计团队对“简洁”的要求可见一斑。

内置常用集合类型:array, slice, map。有且只有这三种
Go语言只内置了三种集合:array(数组), slice(可以理解成可动态增长的数组), map(映射)。其实在日常编程中,这三种类型足以满足80%以上的需求。而其它集合类型比如set等,可以通过map实现;对于list,则以标准库的形式提供。这种分层次的集合提供方式,折射出Go语言的设计者对于语言功能集的谨慎思考。而其它语言(如C++、Python),要么全部以标准库的形式、要么全部以内置的形式,提供了各种各样的集合类型,比如array, map, set、list,甚至tuple等(我就不说c++的map和unordered_map了),新人使用起来非常混乱。

defer关键字
defer关键字在函数体中使用,用来声明一个函数调用语句。当函数退出时,程序自动执行defer声明的语句。举个例子:

func bar() {
  fmt.Println("in bar")
}

func foo() {
  defer bar()
  fmt.Println("in foo")
}

//调用foo后输出:
//in foo
//in bar

defer是一个非常实用的功能,不知道为何其它语言一直没有支持类似的特性。在C++、Python、JAVA中,只能使用析构来保证资源的自动释放,但其麻烦程度使多数人宁愿写不安全的代码。
比如打开一个文件后,一般都要主动关闭这个文件,否则就会造成资源泄露。通过写一个类来包装这个文件对象,在类的析构函数中释放文件,可以保证在任何情况下都会安全释放文件。但计算机中存在着非常多需要安全释放的对象,用这种办法你要给每个对象写一个包装类。这是一种实践上不可实现的做法。所以多数人都写出了这样的C++代码:

int foo(const char* path)
{
  FILE* fp = NULL;

  do {
    fp = fopen(path);
    if (NULL == fp)
      break;

    if (handle_file(fp) == false)
      break;

    //do something else
  } while(false);

  if (fp) {
    fclose(fp);
    fp = NULL;
  }
}

这就是经典的do/while式的关闭资源的方式。但这种写法有两个潜在问题,导致文件不会被关闭:

  1. 在释放文件之前任何一个地方发生异常,函数直接退出执行。
  2. 后来维护的人没有遵守这种“约定”,直接在do/while中return。

有了defer,这些问题都不是问题:

func foo(path string) int {
  f, err := os.Open(path)
  defer f.Close()

  //do something and return
  return 0
}

foo函数不管是异常返回还是return 0返回,文件都会被正确关闭。
我突然想到以前google的C++编程规范里不准使用异常,如此也就不奇怪Go语言里出现defer关键字了。现在Go里有了defer,他们——仍然不使用异常,因为在Go语言里异常机制基本被废除了。

顺便说一句,C++11里提供了lambda表达式,也可以实现类似defer的功能。通过学习刘未鹏大神的文章,写了一个自己的SCOPE_EXIT宏实现了类似defer的功能。

基本丢弃异常机制
在讲解其它语言的书里,都会有单独一章专门讲解异常处理,这些语言基本都有类似try/catch/throw的异常处理机制。但在Go语言的书中没有专门讲异常的章节。Go将这些特性全部抛弃,取而代之的是panic函数和recover函数。panic可以让程序崩溃,而在defer中调用recover可以阻止程序崩溃。这就是Go的异常处理,是不是又一次简单到没朋友?

首字母大小写控制可访问性
C++、Python、JAVA这三大语言都有public、private这些关键字,用来控制字段是否可以被外面访问。实际编程时,C++和JAVA中这几个关键字用得还比较频繁,Python中基本不用。而在Go语言中索性就没有这样的关键字。
在我看来,private关键字对代码控制得太细,private的本意是阻止其他人胡乱使用我们代码中的字段。但实践中直接使用我们代码的人往往是我们自己,那些“可能胡乱使用”的人只会接触到我们发布的库,以及我们暴露出来的接口,这些接口往往经过精心设计并且有完整文档。因此总得来看,private多数情况下限制了我们自己对自己字段的访问,而这完全没有必要(我们连自己的代码都用不对吗?)。在这一点上Go语言又一次体现了设计者对细节的深思熟虑:Go语言的发布单位是包,因此Go语言只控制包外是否可见(函数或字段首字段大写如DoSomething,包外是可见的;doSomething包外是不可见的),对于包内,Go选择相信程序员的能力,任何字段都可见。

精巧的interface机制
Go语言是支持面向对象的,但机制是那么的与众不同。在其它常用的语言中,继承是面向对象的重要组成部分:一个类要想实现某个接口,就得继承这个接口,然后实现接口中的所有方法。为了避免多继承带来的混乱,Python、Ruby、Java等语言各自通过不同的方式,规避了多继承。但Go语言中没有继承,你只要实现了某个接口的所有方法,你就实现了这个接口。是不是很与众不同?来看一个例子:

type Reader interface {
  Read(p []byte) (n int, err error)
}

type MyStruct struct {
  //...
}

func (ms MyStruct) Read(p []byte) (n int, err error) {
  //...
}

MyStruct通过实现Read方法,就实现了Reader这个接口。这真正实现了“鸭子类型”:只要行为表现得像只鸭子,那它就是只鸭子。
正是因为interface机制的这种方便和灵活,Go的标准库里有大量类似Reader的接口:它们通常都很小,只有几个甚至一个方法。你的代码可以轻松的实现某个接口的所有方法,从而将你的代码与标准库结合起来。这看起来没什么,但想想其它语言的有些库动不动十几个方法的接口,你会觉得Go的接口是那么简单。

函数返回值列表在参数列表的后面
Go语言的函数定义时,返回值声明放在最后面,例如:

func foo(x int) int

官方有一篇博文专门解释了这个设计。大体意思是主要有两个原因:

  • 可读性好。所有函数声明从左到右读就可以。比如上面的例子,从左到右解读为:函数foo拥有一个int类型的参数并且返回int。
  • 函数作为类型声明时更清晰。直接用文中的例子:
    //声明一个函数类型,第一个参数类型是函数指针,第二个参数类型是int,函数返回类型是函数指针
    //c
    int (*(*fp)(int (*)(int, int), int))(int, int)
    //go
    fp func(func(int, int) int, int) func(int, int) int
    

    Go语言直接从左向右读。哪个好理解,你们感受下。

丰富又好用的内置工具
Go语言的go命令集成了不少官方直接支持的工具,这些工具都非常实用。比如用来格式化代码的go fmt,可以将你写的代码格式化为官方标准样式,这样所有人写出来的代码风格都是一样的;再比如官方直接支持写测试用例,使用go test就可以运行测试集,并可以进行性能测试;再比如go doc,可以查看和生成文档。
你可能会觉得这些工具其它语言都有呀,有什么惊奇的。Go的不同在于这些都是官方直接支持的,且都非常简单易用。官方支持+简单易用的结果是人们会经常用;第三方工具+不易用的结果就是很少人有用,跟没有差不多。

开源思想
Go语言里使用import导入一个包时,可以直接使用一个网络路径。或者使用go get/install从网络下拉一下包到本地。这样你就可以非常方便的使用第三方包,这个包可能在github上,而你只要import它的网络路径就可以了。 另外,Go语言提供了一个网页,可以在线运行你写入的代码,并可将你写的代码生成一个唯一的网址分享给别人。这是一个看似简单、但又实在让人觉得非常棒的功能。

其它特性
作为一个编译型的静态类型语言,Go语言同样提供了闭包、匿名函数、同时返回多个值等高级语言的特性,这些特性对于编程效率是不言而喻的。
另外不得不提的一点是Go语言判断函数调用是否成功的惯用方法。先看代码:

f, err := os.Open(file)
if err != nil {
  fmt.Fprintf(os.Stderr, "%v\n", err)
  return nil, err
}

再例如:

v, exist := a_map["key"]
if !exist {
  fmt.Println("key does not exist")
}

可以看到Go语言一般将真正想要的返回数据和错误数据分开返回。这种方式非常清晰。


总结
相较其它语言,无论是从语法细节,还是语言生态,Go语言都作出了很大的改变,力求简单、高效。即使有些特性在其它语言里已习以为常,Go语言也进行了大胆的舍弃和改造。因此在喜欢这门语言的同时,我也非常佩服Go语言设计者的大师智慧。

另外,这篇文章并没有提Go语言如何简单地使一个函数并行运行、如何简单地开发高并行程序。正如文章开头所说,这是Go语言非常重要的一大特性(但其实又如此简单)。要想知道Go是怎么做到的,赶快去学这门神奇的语言吧。


Similar Posts

上一篇 hello, blog world!

Comments