PHP程序员为什么要学习GO语言

63次阅读
没有评论

共计 7873 个字符,预计需要花费 20 分钟才能阅读完成。

PHP 程序员为什么要学习 GO 语言,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

很多人将 GO 语言称为 21 世纪的 C 语言,因为 GO 不仅拥有 C 的简洁和性能,而且还很好的提供了 21 世纪互联网环境下服务端开发的各种实用特性,让开发者在语言级别就可以方便的得到自己想要的东西。

发展历史

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 语言,主要是基于两方面的考虑

执行性能 缩短 API 的响应时长,解决批量请求访问超时的问题。在 Uwork 的业务场景下,一次 API 批量请求,往往会涉及对另外接口服务的多次调用,而在之前的 PHP 实现模式下,要做到并行调用是非常困难的,串行处理却不能从根本上提高处理性能。而 GO 语言不一样,通过协程可以方便的实现 API 的并行处理,达到处理效率的最大化。依赖 Golang 的高性能 HTTP Server,提升系统吞吐能力,由 PHP 的数百级别提升到数千里甚至过万级别。开发效率 GO 语言使用起来简单、代码描述效率高、编码规范统一、上手快。通过少量的代码,即可实现框架的标准化,并以统一的规范快速构建 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,还是非常令人满意的。

开发过程中需要注意的点

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

异常处理统一使用 error,不要使用 panic/recover 来模拟 throw…catch,最初我是这么做的,后来发现这完全是自以为是的做法。原生的 error 过于简单,而在实际的 API 开发过程中,不同的异常情况需要附带不同的返回码,基于此,有必要对 error 再进行一层封装。任何协程逻辑执行体,逻辑最开始处必须要有 defer recover()异常恢复处理,否则 goroutine 内出现的 panic,将导致整个进程宕掉,需要避免部分逻辑 BUG 造成全局影响。在 Golang 中,变量 (chan 类型除外) 的操作是非线程安全的,也包括像 int 这样的基本类型,因此并发操作全局变量时一定要考虑加锁,特别是对 map 的并发操作。所有对 map 键值的获取,都应该判断存在性,最好是对同类操作进行统一封装,避免出现不必要的运行时异常。定义 slice 数据类型时,尽量预设长度,避免内部出现不必要的数据重组。

看完上述内容,你们掌握 PHP 程序员为什么要学习 GO 语言的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注丸趣 TV 行业资讯频道,感谢各位的阅读!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-28发表,共计7873字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)