当前位置: 首页 > 科技观察

这篇文章会让你知道为什么学过PHP的人都必须转Go语言

时间:2023-03-19 02:28:27 科技观察

很多人把GO语言称为21世纪的C语言,因为GO不仅有C的简单和性能,还提供21世纪的互联网。环境中服务器端开发的各种实用特性,让开发者可以轻松地在语言层面得到他们想要的。发展历程2007年9月,RobPike在Google分布式编译平台上编译C++。在漫长的等待过程中,他和RobertGriesemer讨论了编程语言的一些关键问题。他们认为,简化编程语言比起不断给臃肿的语言添加新的功能,会是一个更大的进步。然后他们在编译完成之前说服了旁边的KenThompson,他们觉得有必要为此做点什么。几天后,他们启动了一个名为Golang的项目作为业余实验。2008年5月,谷歌发现了GO语言的巨大潜力,得到了谷歌的全力支持。这些人开始全职投入到GO语言的设计和开发中。2009年11月,GO语言第一版发布。2012年3月,第一个正式版Go1.0发布。2015年8月go1.5发布,这个版本算是历史性的了。完全去掉C语言部分,使用GO编译GO,少量代码使用汇编实现。此外,他们还邀请了内存管理方面的权威专家RickHudson对GC进行了重新设计,支持并发GC,解决了长期以来广为诟病的GC时间延迟(STW)问题。并且在后续版本中,进一步优化了GC。到go1.8时,相同业务场景下的GC延迟可以从go1.1的几秒控制在1ms以内。解决GC问题,可以说GO语言几乎抹平了服务端开发的所有弱点。GO语言在版本迭代过程中,语言特性基本没有太大变化,基本保持GO1.1的基准,官方承诺新版本完全兼容旧版本下开发的代码。事实上,GO开发团队在加入新的语言特性时非常谨慎,在稳定性、编译速度、执行效率、GC性能等方面进行了持续的优化。开发团队GO语言的开发团队可以说是空前强大。主要成员中不乏计算机软件界的历史人物,他们对计算机软件的发展有着深远的影响。KenThompson,来自贝尔实验室,设计了B语言,创建了Unix操作系统(最初是用B语言实现的),然后在Unix开发过程中,和DennisRitchie一起设计了C语言,然后用C语言重构了Unix操作系统。丹尼斯·里奇和肯·汤普森被誉为Unix和C语言之父,并于1983年共同获得图灵奖,以表彰他们对计算机软件发展的杰出贡献。同样来自贝尔实验室的RobPike,Unix团队的重要成员,发明了Limbo语言,并与《Unix编程环境》、《编程实践》的作者之一KenThompson共同设计了UTF-8编码。可以说GO语言背靠的是谷歌这棵大树,人才济济。是名副其实的“牛二代”。大名鼎鼎的Docker完全是用GO实现的,业界最流行的容器编排管理系统kubernetes也是完全用GO实现的,后续的DockerSwarm也是完全用GO实现的。此外还有etcd/consul/flannel等各种知名项目,都是使用GO实现的。有人说GO语言之所以出名是赶上了云时代,但为什么不能说GO语言也推动了云的发展呢?除了云项目,还有今日头条、优步等公司。使用GO语言完全重组了我的业务。GO语言的主要特点GO语言之所以强大,是因为它总能抓住程序员在服务器开发中的痛点,以最直接、简单、高效、稳定的方式解决问题。这里我们不深入讨论GO语言的具体语法,只介绍该语言对简化编程具有重要意义的关键方面,跟随大师的脚步去感受GO的设计哲学。GO语言的关键特性主要包括以下几个方面:并发和协程基于消息传递的丰富实用的通信方式内置数据类型函数,具有多重返回值延迟机制,并发编程的意义不言而喻。当然,很多语言都支持多线程、多进程编程,但遗憾的是,实现和控制起来并不是那么轻松愉快。与Golang不同的是,语言层面支持goroutine并发(协程也叫微线程,比线程更轻量,开销更低,性能更高),操作起来非常简单。语言层面提供关键字(go)用于启动协程,同一台机器可以启动上千个协程。对比JAVA的多线程和GO的协程实现,显然更加直接简单。这就是GO的魅力,它以简单高效的方式解决问题。关键字go可能是GO语言最重要的标志。在异步并发编程过程中,基于消息传递的通信方式不足以方便快捷地启动协程。协程之间的消息通信也是非常重要的一环,否则,各个协程都会成为无法控制的野马。在GO语言中,协程间通信采用基于消息传递的通信方式(而不是大多数语言使用的基于共享内存的通信方式),以消息通道(channel)作为基本数据类型,使用类型键(chan)定义,并发操作时执行线程安全。这在语言实现方面也是革命性的。可见GO语言本身并不是简单到没有底线的地步。恰恰是他们会以最简单、最直接的形式,为用户提供最实用、最解决问题的能力。Channel不仅仅用于简单的消息通信,还可以扩展到很多非常实用的功能,实现起来也很方便。比如实现TCP连接池、限流等,其他语言不容易实现,GO语言却可以轻松做到。GO语言作为一种编译型语言,除了传统的整型、浮点型、字符型、数组型、结构型等类型外,还支持非常全面的数据类型。从实用性来看,它还支持原生的字符串类型、切片类型(变长数组)、字典类型、复数类型、错误类型、管道类型,甚至是任意类型(Interface{}),非常容易使用方便。比如string和slice类型,操作的方便程度和python差不多。另外,使用错误类型(error)作为基本数据类型,不再支持在语言层面使用try...catch,这应该算是一个非常大胆的革命性创举,难怪很多人抱怨GO语言不伦不类。但跳出传统观念,GO开发者认为在编程过程中,要保证程序的健壮性和稳定性,准确处理异常非常重要。只有在每个逻辑处理完成后,才明确通知上层。调用,是否有异常,上层调用会清晰及时的处理异常,从而高度保证程序的健壮性和稳定性。这样做虽然会在编程过程中造成很多对错误结果的判断,但无疑会增强开发者对异常处理的警惕性。实践证明,只要严格遵循GO推荐的风格,就很难写出不健壮的代码。当然,前提是你不排斥,不认可。在语言中支持具有多个返回值的函数并不是什么新鲜事,Python就是其中之一。允许函数返回多个值,在某些场景下可以有效简化编程。GO语言推荐的编程风格是函数返回的最后一个参数是错误类型(只要逻辑体可能出现异常即可),所以需要在语言层面支持多个返回值.defer延迟处理机制在GO语言中提供了关键字defer,通过defer可以指定需要延迟的逻辑体,即在函数体返回之前或发生panic时执行。这种机制非常适合事后逻辑处理,比如尽早避免可能的资源泄露。可以说defer是继goroutine和channel之后又一个非常重要和实用的语言特性。defer的引入可以在很大程度上简化编程,在语言描述上更加自然,大大增强了代码的可读性。Golang作为一种强类型的编译型语言,自然不如分析型语言灵活。比如PHP是弱类型的,可以直接对一个字符串变量的内容进行new操作,但是在编译型语言中,这显然是不可能的。但是,Golang提供了Any类型(interface{})和强大的类型反射(reflect)能力。两者的结合使得开发灵活性非常接近于分析语言。在逻辑的动态调用方面,实现起来还是很简单的。既然如此,PHP这样的解析语言相比于GO有什么优势呢?个人写PHP近10年,实现过开发框架,基础类库,各种公共组件。虽然执行性能不足,但开发效率绰绰有余;到了Golang,这些优势就显得不那么明显了。作为互联网时代出现的服务器端语言,服务用户的能力必不可少。GO在语言层面自带HTTP/TCP/UDP高性能服务器,基于协程并发,为业务开发提供最直接有效的能力支持。用GO语言只需要几行代码就可以实现一个高性能的HTTPServer,非常简单。在GO语言中,有一套标准的工程管理规范。只要按照这个规范来开发项目,后面的事情(比如包管理、编译等)就会变得非常简单。GO项目下,有两个关键目录,一个是src目录,用来存放所有的.go源码文件;另一个是bin目录,用于存放编译好的二进制文件。src目录下,除了main主包所在的目录外,其他目录名都对应direct目录下对应的包名,否则编译不通过。这样GO编译器就可以从主包所在的目录开始,完全利用目录结构和包名来推导工程结构和构建顺序,避免像C++一样引入额外的Makefile。在GO编译过程中,我们唯一需要做的就是将GO项目路径赋值给一个名为GOPATH的环境变量,这样编译器就知道要编译的GO项目所在的位置了。然后进入bin目录,执行gobuild{主包所在目录名},秒级完成项目编译。编译后的二进制文件可以推送到同一个操作系统直接运行,没有任何环境依赖。GO语言的编程规范是强制集成在语言中的,比如明确指定花括号的放置,一次强制一行,不允许导入未使用的包,不允许定义未使用的变量,提供gofmt强制格式化代码等的工具。奇怪的是,这些也引起了很多程序员的不满。有人发表了GO语言的XX罪状,其中有很多针对编程规范的指责。要知道,从工程管理的角度来看,任何一个开发团队都会针对特定的语言制定特定的编程规范,尤其是像谷歌这样的公司。GO的设计者认为,与其将规范写在文档中,不如在语言中强制集成,更直接,利用团队协作和项目管理。API快速开发框架实践编程语言是一种工具,它会告诉我们可以做什么,如何做得更好也值得探索。这部分将介绍一个用GO语言实现的开发框架和几个公共组件。当然,框架和公共组件也可以用其他语言实现,但这里的重点是成本。另外,抛开GO语言本身,我们也希望大家能够从引入的组件中得到一些解决问题的思路,即通过某种方式解决一个通用的问题,而不是一味地写代码,最终它只是解决了问题。如果您认同这种方式,相信以下内容可能会影响您后续的项目开发方式,从根本上提高开发效率。为什么我们选择GO语言,选择GO语言,主要是基于两个考虑:执行性能缩短API的响应时间,解决批量请求访问超时问题。在Uwork的业务场景中,一个API批量请求往往会涉及到对其他接口服务的多次调用,但在以往的PHP实现方式下,并行调用难度很大,串行处理也无法从根本上提升处理性能。GO语言是不同的。通过协程可以轻松实现API的并行处理,最大限度地提高处理效率。依托Golang的高性能HTTPServer,将系统吞吐量从PHP的数百级提升至数千里甚至数万级。开发效率GO语言简单易用,代码描述高效,编码标准统一,上手快捷。以少量的代码,实现框架的标准化,快速构建统一规范的API业务逻辑。可以快速构建各种通用组件和公共类库,进一步提升开发效率,实现特定场景下的功能量产。很多人在学习一门新语言或者开始一个新项目的时候,都会习惯性的在网上找一个开源的框架来开始自己的项目开发之旅。这样做没有什么不妥,但是我个人觉得这样对我们了解它的内部实现会更有帮助。也许你已经注意到,所谓的MVC框架本质上是解析请求路径,然后根据请求路径段路由到对应的控制器(C),然后控制器进一步调用数据逻辑(M),在得到数据,呈现视图(V)并返回给用户。整个过程中,核心点是逻辑的动态调用。但是API框架的实现要比WEB页面框架的实现简单,因为它不涉及视图的渲染,只需要将数据结果以协议的形式返回给用户即可。使用GO语言很容易实现一套完整的MVC开发框架。在集成HTTPServer的同时,整个框架的核心代码不会超过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开发过程中,数据校验始终是不可或缺的环节。如果只是简单的数据校验,可能几行代码就可以完成,但是遇到复杂的数据校验,很可能几百行代码都不一定能完成,尤其是遇到递归的数据校验,那简直了一个噩梦。数据验证组件可以通过数据模板配置的方式,通过特定的逻辑完成通用的验证。开发者只需配置相应的数据模板,简单调用即可完成整个验证过程。对于这样一个通用的数据验证组件,整个搭建只用了不到700行的GO语言代码。总结在实际的项目开发过程中,开发效率最大的提升无疑是满足系统业务场景的公共组件能力。这正是RobPike所说的(LessislessorLessismore),真正的高效开发是可配置的。不需要写太多代码,甚至根本不用写代码,就可以完成逻辑实现。这种方法对于后期的维护成本也是最优的,因为它实现了高度的Unite。GO语言描述的效率毋庸置疑。上述所有公共组件的实现不超过1000行代码,解决了某个表面上的问题。(以上部分代码已在Uwork开源项目seine中提供)性能测评压力测试环境说明:服务跑台:单台闲置B6,24核CPU,64G内存。PHPAPI环境:Nginx+PHP-FPM,CI框架。其中Nginx启动10个子进程,每个子进程最多接收1024个连接,php-fpm采用静态方式启动2000个常驻子进程。GolangAPI环境:用go1.8.6编译,直接启动GolangAPIServer进程(HttpServer),不用调优。客户发起请求测试程序:Golang编写,并发协程,运行在另一个独立空闲的B6,24核CPU,64G内存,顺序1-2000不同级别(并发步数50)并发20000个请求是在他们每个人身上制作的。压测结果对比在GolangAPI框架中,当并发数>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实现的HTTPAPI框架,空载下单机性能QPS达到6.5w/s,还是很满意的。开发过程中的注意点以下是实际开发过程中遇到的一些问题,仅供参考:异常处理统一使用error,不要使用panic/recover模拟throw...catch,我在一开始,后来才发现这完全是冒昧的。原生错误过于简单,在实际的API开发过程中,不同的异常情况需要伴随不同的返回码。基于此,有必要将错误封装在另一层。对于任何一个协程逻辑执行体,在逻辑的最开始必须有deferrecover()异常恢复处理,否则goroutinepanic会导致整个流程崩溃,需要避免一些逻辑bug导致全球影响。在Golang中,对变量的操作(除了chan类型)都不是线程安全的,包括int这样的基本类型,所以并发操作全局变量,尤其是对map的并发操作,一定要考虑锁。所有获取mapkey值都要判断其是否存在。类似的操作最好统一封装,避免不必要的运行时异常。在定义切片数据类型时,尽量预先设定长度,避免不必要的内部数据重组。