大道至简、大智若愚—GO语言最佳详解实践

 

 

 

 

 

导读:2007年,受够了C++煎熬的Google首席软件工程师Rob Pike纠集Robert Griesemer和Ken Thompson两位牛人,决定创造一种新语言来取代C++, 这就是Golang。出如今21世纪的GO语言,虽然不能如愿对C++取而代之,可是其近C的执行性能和近解析型语言的开发效率以及近乎于完美的编译速度,已经风靡全球。特别是在云项目中,大部分都使用了Golang来开发,不得不说,Golang早已深刻人心。而对于一个没有历史负担的新项目,Golang或许就是个不二的选择。

 

被称为GO语言之父的Rob Pike说,你是否赞成GO语言,取决于你是承认少就是多,仍是少就是少(Less is more or lessis less)。Rob Pike以一种很是朴素的方式,归纳了GO语言的整个设计哲学--将简单、实用体现得淋漓尽致。php

 

不少人将GO语言称为21世纪的C语言,由于GO不只拥有C的简洁和性能,并且还很好的提供了21世纪互联网环境下服务端开发的各类实用特性,让开发者在语言级别就能够方便的获得本身想要的东西。python

 

本文大纲:程序员

  • GO语言的发展与现状数据库

  • 发展历史编程

  • 开发团队数组

  • 业务案例安全

  • GO语言关键特性服务器

  • 并发与协程多线程

  • 基于消息传递的通讯方式并发

  • 丰富实用的内置数据类型

  • 函数多返回值

  • Defer延迟处理机制

  • 反射(reflect)

  • 高性能HTTP Server

  • 工程管理

  • 编程规范

  • API快速开发框架实践

  • 咱们为何选择GO语言

  • API框架的实现

  • 公共组件能力

  • 通用列表组件

  • 通用表单组件

  • 协程池

  • 数据校验

  • 小结

  • 性能评测

  • 开发过程当中须要注意的点

 

 GO语言的发展与现状


 

发展历史

2007年9月,Rob Pike在Google分布式编译平台上进行C++编译,在漫长的等待过程当中,他和Robert Griesemer探讨了程序设计语言的一些关键性问题,他们认为,简化编程语言相比于在臃肿的语言上不断增长新特性,会是更大的进步。随后他们在编译结束以前说服了身边的Ken Thompson,以为有必要为此作一些事情。几天后,他们发起了一个叫Golang的项目,将它做为自由时间的实验项目。

 

2008年5月 Google发现了GO语言的巨大潜力,获得了Google的全力支持,这些人开始全职投入GO语言的设计和开发。

 

2009年11月 GO语言第一个版本发布。2012年3月 第一个正式版本Go1.0发布。

 

2015年8月 go1.5发布,这个版本被认为是历史性的。彻底移除C语言部分,使用GO编译GO,少许代码使用汇编实现。另外,他们请来了内存管理方面的权威专家Rick Hudson,对GC进行了从新设计,支持并发GC,解决了一直以来广为诟病的GC时延(STW)问题。而且在此后的版本中,又对GC作了更进一步的优化。到go1.8时,相同业务场景下的GC时延已经能够从go1.1的数秒,控制在1ms之内。GC问题的解决,能够说GO语言在服务端开发方面,几乎抹平了全部的弱点。

 

在GO语言的版本迭代过程当中,语言特性基本上没有太大的变化,基本上维持在GO1.1的基准上,而且官方承诺,新版本对老版本下开发的代码彻底兼容。事实上,GO开发团队在新增语言特性上显得很是谨慎,而在稳定性、编译速度、执行效率以及GC性能等方面进行了持续不断的优化。

 

开发团队

 

GO语言的开发阵营能够说是空前强大,主要成员中不乏计算机软件界的历史性人物,对计算机软件的发展影响深远。Ken Thompson,来自贝尔实验室,设计了B语言,创立了Unix操做系统(最初使用B语言实现),随后在Unix开发过程当中,又和Dennis Ritchie一同设计了C语言,继而使用C语言重构了Unix操做系统。Dennis Ritchie和Ken Thompson被称为Unix和C语言之父,并在1983年共同被授以图灵奖,以表彰他们对计算机软件发展所做的杰出贡献。Rob Pike,一样来自贝尔实验室,Unix小组重要成员,发明了Limbo语言,而且和Ken Thompson共同设计了UTF-8编码,《Unix编程环境》、《编程实践》做者之一。

 

能够说,GO语言背靠Google这棵大树,又不乏牛人坐镇,是名副其实的“牛二代”。

 

 

大名鼎鼎的Docker,彻底用GO实现,业界最为火爆的容器编排管理系统kubernetes,彻底用GO实现,以后的Docker Swarm,彻底用GO实现。除此以外,还有各类有名的项目如etcd/consul/flannel等等,均使用GO实现。有人说,GO语言之因此出名,是遇上了云时代,但为何不能换种说法,也是GO语言促使了云的发展?

 

除了云项目外,还有像今日头条、UBER这样的公司,他们也使用GO语言对本身的业务进行了完全的重构。

 

 

GO语言关键特性

GO语言之因此厉害,是由于它在服务端的开发中,总能抓住程序员的痛点,以最直接、简单、高效、稳定的方式来解决问题。这里咱们并不会深刻讨论GO语言的具体语法,只会将语言中关键的、对简化编程具备重要意义的方面介绍给你们,跟随大师们的脚步,体验GO的设计哲学。

 

GO语言的关键特性主要包括如下几方面:

  • 并发与协程

  • 基于消息传递的通讯方式

  • 丰富实用的内置数据类型

  • 函数多返回值

  • defer机制

  • 反射(reflect)

  • 高性能HTTP Server

  • 工程管理

  • 编程规范

 

 

在当今这个多核时代,并发编程的意义不言而喻。固然,不少语言都支持多线程、多进程编程,但遗憾的是,实现和控制起来并非那么使人感受轻松和愉悦。Golang不一样的是,语言级别支持协程(goroutine)并发(协程又称微线程,比线程更轻量、开销更小,性能更高),操做起来很是简单,语言级别提供关键字(go)用于启动协程,而且在同一台机器上能够启动成千上万个协程。

 

对比JAVA的多线程和GO的协程实现,明显更直接、简单。这就是GO的魅力所在,以简单、高效的方式解决问题,关键字go,或许就是GO语言最重要的标志。

 

基于消息传递的通讯方式

 

在异步的并发编程过程当中,只能方便、快速的启动协程还不够。协程之间的消息通讯,也是很是重要的一环,不然,各个协程就会成为脱缰的野马而没法控制。在GO语言中,使用基于消息传递的通讯方式(而不是大多数语言所使用的基于共享内存的通讯方式)进行协程间通讯,而且将消息管道(channel)做为基本的数据类型,使用类型关键字(chan)进行定义,并发操做时线程安全。这点在语言的实现上,也具备革命性。可见,GO语言自己并不是简单得没有底线,偏偏他们会将最实用、最有利于解决问题的能力,以最简单、直接的形式提供给用户。

 

Channel并不只仅只是用于简单的消息通讯,还能够引伸出不少很是实用,而实现起来又很是方便的功能。好比,实现TCP链接池、限流等等,而这些在其它语言中实现起来并不轻松,但GO语言能够轻易作到。

 

 

GO语言做为编译型语言,在数据类型上也支持得很是全面,除了传统的整型、浮点型、字符型、数组、结构等类型外。从实用性上考虑,也对字符串类型、切片类型(可变长数组)、字典类型、复数类型、错误类型、管道类型、甚至任意类型(Interface{})进行了原生支持,而且用起来很是方便。好比字符串、切片类型,操做简便性几乎和python相似。

 

另外,将错误类型(error)做为基本的数据类型,而且在语言级别再也不支持try…catch的用法,这应该算是一个很是大胆的革命性创举,也难怪不少人吐槽GO语言不三不四。可是跳出传统的观念,GO的开发者认为在编程过程当中,要保证程序的健壮性和稳定性,对异常的精确化处理是很是重要的,只有在每个逻辑处理完成后,明确的告知上层调用,是否有异常,并由上层调用明确、及时的对异常进行处理,这样才能够高程度的保证程序的健壮性和稳定性。虽然这样作会在编程过程当中出现大量的对error结果的判断,可是这无疑也加强了开发者对异常处理的警戒度。而实践证实,只要严格按GO推荐的风格编码,想写出不健壮的代码,都很难。固然,前提是你不排斥它,承认它。

 

在语言中支持函数多返回值,并非什么新鲜事,Python就是其中之一。容许函数返回多个值,在某些场景下,能够有效的简化编程。GO语言推荐的编程风格,是函数返回的最后一个参数为error类型(只要逻辑体中可能出现异常),这样,在语言级别支持多返回值,就颇有必要了。

 

Defer延迟处理机制

在GO语言中,提供关键字defer,能够经过该关键字指定须要延迟执行的逻辑体,即在函数体return前或出现panic时执行。这种机制很是适合善后逻辑处理,好比能够尽早避免可能出现的资源泄漏问题。

 

能够说,defer是继goroutine和channel以后的另外一个很是重要、实用的语言特性,对defer的引入,在很大程度上能够简化编程,而且在语言描述上显得更为天然,极大的加强了代码的可读性。

 

 

Golang做为强类型的编译型语言,灵活性上天然不如解析型语言。好比像PHP,弱类型,而且能够直接对一个字符串变量的内容进行new操做,而在编译型语言中,这显然不太可能。可是,Golang提供了Any类型(interface{})和强大的类型反射(reflect)能力,两者相结合,开发的灵活性上已经很接近解析型语言。在逻辑的动态调用方面,实现起来仍然很是简单。既然如此,那么像PHP这种解析型语言相比于GO,优点在那里呢?就我我的而言,写了近10年的PHP,实现过开发框架、基础类库以及各类公共组件,虽然执行性能不足,可是开发效率有余;而当赶上Golang,这些优点彷佛不那么明显了。

 

 

做为出如今互联网时代的服务端语言,面向用户服务的能力必不可少。GO在语言级别自带HTTP/TCP/UDP高性能服务器,基于协程并发,为业务开发提供最直接有效的能力支持。要在GO语言中实现一个高性能的HTTP Server,只须要几行代码便可完成,很是简单。

 

 

在GO语言中,有一套标准的工程管理规范,只要按照这个规范进行项目开发,以后的事情(好比包管理、编译等等)都将变得很是的简单。

 

在GO项目下,存在两个关键目录,一个是src目录,用于存放全部的.go源码文件;一个是bin目录,用于存在编译后的二进制文件。在src目录下,除了main主包所在的目录外,其它全部的目录名称与直接目录下所对应的包名保持对应,不然编译没法经过。这样,GO编译器就能够从main包所在的目录开始,彻底使用目录结构和包名来推导工程结构以及构建顺序,避免像C++同样,引入一个额外的Makefile文件。

 

在GO的编译过程当中,咱们惟一要作的就是将GO项目路径赋值给一个叫GOPATH的环境变量,让编译器知道将要编译的GO项目所在的位置。而后进入bin目录下,执行go build {主包所在的目录名},便可秒级完成工程编译。编译后的二进制文件,能够推到同类OS上直接运行,没有任何环境依赖。

 

 

GO语言的编程规范强制集成在语言中,好比明确规定花括号摆放位置,强制要求一行一句,不容许导入没有使用的包,不容许定义没有使用的变量,提供gofmt工具强制格式化代码等等。奇怪的是,这些也引发了不少程序员的不满,有人发表GO语言的XX条罪状,里面就不乏对编程规范的指责。要知道,从工程管理的角度,任何一个开发团队都会对特定语言制定特定的编程规范,特别像Google这样的公司,更是如此。GO的设计者们认为,与其将规范写在文档里,还不如强制集成在语言里,这样更直接,更有利用团队协做和工程管理。

 

 

API快速开发框架实践

编程语言是一个工具,它会告诉咱们能作什么,而怎么作会更好,一样值得去探讨。这部分会介绍用GO语言实现的一个开发框架,以及几个公共组件。固然,框架和公共组件,其它语言也彻底能够实现,而这里所关注的是成本问题。除此以外,抛开GO语言自己不说,咱们也但愿可让你们从介绍的几个组件中,获得一些解决问题的思路,那就是经过某种方式,去解决一个面上的问题,而非一味的写代码,最终却只是解决点上的问题。若是你承认这种方式,相信下面的内容也许会影响你以后的项目开发方式,从根本上提升开发效率。

 

咱们为何选择GO语言

选择GO语言,主要是基于两方面的考虑

 

  1. 执行性能

     

    缩短API的响应时长,解决批量请求访问超时的问题。在Uwork的业务场景下,一次API批量请求,每每会涉及对另外接口服务的屡次调用,而在以前的PHP实现模式下,要作到并行调用是很是困难的,串行处理却不能从根本上提升处理性能。而GO语言不同,经过协程能够方便的实现API的并行处理,达处处理效率的最大化。

     

    依赖Golang的高性能HTTP Server,提高系统吞吐能力,由PHP的数百级别提高到数千里甚至过万级别。

     

  2. 开发效率

    GO语言使用起来简单、代码描述效率高、编码规范统1、上手快。

     

    经过少许的代码,便可实现框架的标准化,并以统一的规范快速构建API业务逻辑。

     

    能快速的构建各类通用组件和公共类库,进一步提高开发效率,实现特定场景下的功能量产。

 

 

不少人在学习一门新语言或开启一个新项目时,都会习惯性的是网上找一个认为合适的开源框架来开始本身的项目开发之旅。这样并无什么很差,可是我的以为,了解它内部的实现对咱们会更有帮助。或许你们已经注意到了,所说的MVC框架,其本质上就是对请求路径进行解析,而后根据请求路径段,路由到相应的控制器(C)上,再由控制器进一步调用数据逻辑(M),拿到数据后,渲染视图(V),返回用户。在整个过程当中,核心点在于逻辑的动态调用。

 

不过,对API框架的实现相对于WEB页面框架的实现,会更简单,由于它并不涉及视图的渲染,只须要将数据结果以协议的方式返回给用户便可。

 

使用GO语言实现一套完整的MVC开发框架,是很是容易的,集成HTTP Server的同时,整个框架的核心代码不会超过300行,从这里能够实际感觉到GO的语言描述效率之高(若是有兴趣,能够参考Uwork开源项目seine)。

也有人说,在GO语言中,就没有框架可言,言外之意是说,引入一个重型的开源框架,必要性并不大,相反还可能把简单的东西复杂化。

 

 

在实际项目开发过程当中,只有高效的开发语言还不够,要想进一步将开发效率扩大化,不断的沉淀公共基础库是必不可少的,以便将通用的基础逻辑进一步抽象和复用。

 

除此以外,通用组件能力是实现功能量产的根本,对开发效率会是质的提高。组件化的开发模式会帮忙咱们将问题的解决能力从一个点上提高到一个面上。如下会重点介绍几个通用组件的实现,有了它们的存在,才能真正的解放程序员的生产力。而这些强有力的公共组件在Golang中实现起来并不复杂。同时,结合Golang的并发处理能力,相比于PHP的版本实现,执行效率也会有质的提高。这是组件能力和语言效率的完美结合。

 

 

通用列表组件用于全部可能的二维数据源(如MySQL/MongoDB/ES等等)的数据查询场景,从一个面上解决了数据查询问题。在Uwork项目开发中,被大量使用,实现数据查询接口和页面查询列表的量产开发。它以一个JSON配置文件为中心,来实现对通用数据源的查询,并将查询结果以API或页面的形式自动返回给用户。整个过程当中几乎没有代码开发,而惟一要作的只是以一种统一的规范编写配置文件(而不是代码),真正实现了对数据查询需求的功能量产。

 

以上是通用列表组件的构建过程,要实现这样一个功能强大的通用组件,是否是会给人一种可望而不可及的感受?其实并不是如此,只要理清了它的整个过程,将构建思路融入Golang中,并非一件复杂的事情。在咱们的项目中,整个组件的实现,只用了不到700行Go代码,就解决了一系列的数据查询问题。另外,经过Golang的并发特性,实现字段处理器的并行执行,进一步的提升了组件的执行效率。能够说,通用列表和Golang的融合,是性能和效率的完美结合。

 

 

 

通用表单组件主要用于对数据库的增、删、改场景。该组件在Uwork的项目开发中,也有普遍的应用,与通用列表相似,以一个JSON配置文件为中心,来完成对数据表数据的增、删、改操做。特别是近期完成的部件级SDB管理平台,经过通用表单实现了对整个系统的数据维护,经过高度抽象化,作到了业务的无代码化生产。

 

 

以上是通用表单的完整构建过程,而对于这个一个组件的实现,咱们用了不到1000行的GO代码,就解决了对数据表数据维护整个面上的问题。

 

GO语言自己支持协程并发,协程很是轻量,能够快速启动成千上万个协程工做单元。若是对协程任务的数量控制不当,最后的结果极可能拔苗助长,从而对外部或自己的服务形成没必要要的压力。协程池能够在必定程度上控制执行单元的数量,保证执行的安全性。而在Golang中要实现这样一个协程池,是很是简单的,只须要对channel和goroutine稍加封装,就能够完成,整个构建过程不到80行代码。

 

在API开发过程当中,数据校验永远是必不可或缺的一个环节。若是只是简单的数据校验,几行代码也许就完成了,但是当赶上复杂的数据校验时,极可能几百行的代码量也未必能完成,特别是遇到递归类型的数据校验,那简直就是一个噩梦。

 

数据校验组件,能够经过一种数据模板的配置方式,使用特定的逻辑来完成通用校验,开发者只须要配置好相应的数据模板,进行简单的调用,便可完成整个校验过程。而对于这样一个通用性的数据校验组件,在GO语言中只用了不到700行的代码量就完成了整个构建。

 

小结

在实际项目开发过程当中,对开发效率提高最大的,无疑是符合系统业务场景的公共组件能力,这点也正好应证了Rob Pike那句话(Less is lessor Less is more),真正的高效率开发,是配置化的,并不须要写太多的代码,甚至根本就不须要写代码,便可完成逻辑实现,而这种方式对于后期的维护成本也是最优的,由于作到了高度的统一。

GO的语言描述效率毋庸置疑,对上述全部公共组件的实现,均未超过1000行代码,就解决了某个面上的问题。

(以上的部分代码已经在Uwork开源项目seine中提供)

 

 

性能评测

压力测试环境说明:

  • 服务运行机器:单台空闲B6,24核CPU、64G内存。

  • PHP API环境:Nginx+PHP-FPM,CI框架。其中Nginx启动10个子进程,每一个子进程最大接收1024个链接,php-fpm使用static模式,启动2000个常驻子进程。

  •  Golang API环境:使用go1.8.6编译,直接拉起Golang API Server进程(HttpServer),不考虑调优。

  • 客户发起请求测试程序:使用Golang编写,协程并发,运行在独立的另一台空闲B6上,24核CPU,64G内存,依次在1-2000个不一样级别(并发数步长为50)的并发上分别请求20000次。

 

压力测试结果对比

 

在Golang API框架中,当并发数>50时,处理QPS在6.5w/s附近波动。表现稳定,压力测试过程无报错。

 

Nginx+php-fpm,只在index.php中输出exit('ok'),当并发数>50时,处理QPS在1w/s附近波动。表现稳定,压力测试过程无报错。

 

Nginx+php-fpm+CI框架中,逻辑执行到具体业务逻辑点,输出exit('ok'),当并发数>50时,处理QPS在750/s附近波动。而且表现不稳定,压力测试过程当中随着并发数的增大,错误量随之增长。

 

经过压力测试能够发现,Golang和PHP在执行性能上,并无什么可比性;而使用Golang实现的HTTP API框架,空载时单机性能QPS达到6.5w/s,仍是很是使人满意的。

 

 

开发过程当中须要注意的点

如下是在实际开发过程当中遇到的一些问题,仅供参考:

  1. 异常处理统一使用error,不要使用panic/recover来模拟throw…catch,最初我是这么作的,后来发现这彻底是自觉得是的作法。

  2. 原生的error过于简单,而在实际的API开发过程当中,不一样的异常状况须要附带不一样的返回码,基于此,有必要对error再进行一层封装。

  3. 任何协程逻辑执行体,逻辑最开始处必需要有defer recover()异常恢复处理,不然goroutine内出现的panic,将致使整个进程宕掉,须要避免部分逻辑BUG形成全局影响。

  4. 在Golang中,变量(chan类型除外)的操做是非线程安全的,也包括像int这样的基本类型,所以并发操做全局变量时必定要考虑加锁,特别是对map的并发操做。

  5. 全部对map键值的获取,都应该判断存在性,最好是对同类操做进行统一封装,避免出现没必要要的运行时异常。

  6. 定义slice数据类型时,尽可能预设长度,避免内部出现没必要要的数据重组。

相关文章
相关标签/搜索