城外的人想进去,城里的人想出来。-- 钱钟书《围城》
随着容器编排(Container Orchestration)、微服务(Micro Services)、云技术(Cloud Technology)等在 IT 行业不断盛行,2009 年诞生于 Google 的 Golang(Go 语言,简称 Go)愈来愈受到软件工程师的欢迎和追捧,成为现在煊赫一时的后端编程语言。在用 Golang 开发的软件项目列表中,有 Docker(容器技术)、Kubernetes(容器编排)这样的颠覆整个 IT 行业的明星级产品,也有像 Prometheus(监控系统)、Etcd(分布式存储)、InfluxDB(时序数据库)这样的强大实用的知名项目。固然,Go 语言的应用领域也毫不局限于容器和分布式系统。现在不少大型互联网企业在大量使用 Golang 构建后端 Web 应用,例现在日头条、京东、七牛云等;长期被 Python 统治的框架爬虫领域也由于简单而易用的爬虫框架 Colly 的崛起而不断受到 Golang 的挑战。Golang 已经成为了现在大多数软件工程师最想学习的编程语言。下图是 HackerRank 在 2020 年调查程序员技能的相关结果。html
那么,Go 语言真的是后端开发人员的救命良药呢?它是否可以有效提升程序员们的技术实力和开发效率,从而帮助他们在职场上更进一步呢?Go 语言真的值得咱们花大量时间深刻学习么?本文将详细介绍 Golang 的语言特色以及它的优缺点和适用场景,带着上述几个疑问,为读者分析 Go 语言的各个方面,以帮助初入 IT 行业的程序员以及对 Go 感兴趣的开发者进一步了解这个热门语言。前端
Golang 诞生于互联网巨头 Google,而这并非一个巧合。咱们都知道,Google 有一个 20% 作业余项目(Side Project)的企业文化,容许工程师们可以在轻松的环境下创造一些具备颠覆性创新的产品。而 Golang 也正是在这 20% 时间中不断孵化出来。Go 语言的创始者也是 IT 界内大名鼎鼎的行业领袖,包括 Unix 核心团队成员 Rob Pike、C 语言做者 Ken Thompson、V8 引擎核心贡献者 Robert Griesemer。Go 语言被大众所熟知仍是源于容器技术 Docker 在 2014 年被开源后的爆发式发展。以后,Go 语言由于其简单的语法以及迅猛的编译速度受到大量开发者的追捧,也诞生了不少优秀的项目,例如 Kubernetes。java
Go 语言相对于其余传统热门编程语言来讲,有不少优势,特别是其高效编译速度和自然并发特性,让其成为快速开发分布式应用的首选语言。Go 语言是静态类型语言,也就是说 Go 语言跟 Java、C# 同样须要编译,并且有完备的类型系统,能够有效减小因类型不一致致使的代码质量问题。所以,Go 语言很是适合构建对稳定性和灵活性均有要求的大型 IT 系统,这也是不少大型互联网公司用 Golang 重构老代码的重要缘由:传统的静态 OOP 语言(例如 Java、C#)稳定性高但缺少灵活性;而动态语言(例如 PHP、Python、Ruby、Node.js)灵活性强但缺少稳定性。所以,“熊掌和鱼兼得” 的 Golang,受到开发者们的追捧是天然而然的事情,毕竟,“天下苦 Java/PHP/Python/Ruby 们久矣“。git
不过,Go 语言并非没有缺点。用辩证法的思惟方式能够推测,Golang 的一些突出特性将成为它的双刃剑。例如,Golang 语法简单的优点特色将限制它处理复杂问题的能力。尤为是 Go 语言缺少泛型(Generics)的问题,致使它构建通用框架的复杂度大增。虽然这个突出问题在 2.0 版本极可能会有效解决,但这也反映出来明星编程语言也会有缺点。固然,Go 的缺点还不止于此,Go 语言使用者还会吐槽其啰嗦的错误处理方式(Error Handling)、缺乏严格约束的鸭子类型(Duck Typing)、日期格式问题等。下面,咱们将从 Golang 语言特色开始,由浅入深多维度深刻分析 Golang 的优缺点以及项目适用场景。程序员
Go 语言的语法很是简单,至少在变量声明、结构体声明、函数定义等方面显得很是简洁。github
变量的声明不像 Java 或 C 那样啰嗦,在 Golang 中能够用 :=
这个语法来声明新变量。例以下面这个例子,当你直接使用 :=
来定义变量时,Go 会自动将赋值对象的类型声明为赋值来源的类型,这节省了大量的代码。golang
func main() {
valInt := 1 // 自动推断 int 类型
valStr := "hello" // 自动推断为 string 类型
valBool := false // 自动推断为 bool 类型
}docker
Golang 还有不少帮你节省代码的地方。你能够发现 Go 中不会强制要求用 new
这个关键词来生成某个类(Class)的新实例(Instance)。并且,对于公共和私有属性(变量和方法)的约定再也不使用传统的 public
和 private
关键词,而是直接用属性变量首字母的大小写来区分。下面一些例子能够帮助读者理解这些特色。数据库
// 定义一个 struct 类
type SomeClass struct {
PublicVariable string // 公共变量
privateVariable string // 私有变量
}
// 公共方法
func (c *SomeClass) PublicMethod() (result string) {
return "This can be called by external modules"
}
// 私有方法
func (c *SomeClass) privateMethod() (result string) {
return "This can only be called in SomeClass"
}
func main() {
// 生成实例
someInstance := SomeClass{
PublicVariable: "hello",
privateVariable: "world",
}
}编程
若是你用 Java 来实现上述这个例子,可能会看到冗长的 .java
类文件,例如这样。
// SomeClass.java
public SomeClass {
public String PublicVariable; // 公共变量
private String privateVariable; // 私有变量
// 构造函数
public SomeClass(String val1, String val2) {
this.PublicVariable = val1;
this.privateVariable = val2;
}
// 公共方法
public String PublicMethod() {
return "This can be called by external modules";
}
// 私有方法
public String privateMethod() {
return "This can only be called in SomeClass";
}
}
...
// Application.java
public Application {
public static void main(String[] args) {
// 生成实例
SomeClass someInstance = new SomeClass("hello", "world");
}
}
能够看到,在 Java 代码中除了容易看花眼的多层花括号之外,还充斥着大量的 public
、private
、static
、this
等修饰用的关键词,显得异常啰嗦;而 Golang 代码中则靠简单的约定,例如首字母大小写,避免了不少重复性的修饰词。固然,Java 和 Go 在类型系统上仍是有一些区别的,这也致使 Go 在处理复杂问题显得有些力不从心,这是后话,后面再讨论。总之,结论就是 Go 的语法在静态类型编程语言中很是简洁。
Go 语言之因此成为分布式应用的首选,除了它性能强大之外,其最主要的缘由就是它自然的并发编程。这个并发编程特性主要来自于 Golang 中的协程(Goroutine)和通道(Channel)。下面是使用协程的一个例子。
func asyncTask() {
fmt.Printf("This is an asynchronized task")
}
func syncTask() {
fmt.Printf("This is a synchronized task")
}
func main() {
go asyncTask() // 异步执行,不阻塞
syncTask() // 同步执行,阻塞
go asyncTask() // 等待前面 syncTask 完成以后,再异步执行,不阻塞
}
能够看到,关键词 go
加函数调用可让其做为一个异步函数执行,不会阻塞后面的代码。而若是不加 go
关键词,则会被当成是同步代码执行。若是读者熟悉 JavaScript 中的 async/await
、Promise
语法,甚至是 Java、Python 中的多线程异步编程,你会发现它们跟 Go 异步编程的简单程度不是一个量级的!
异步函数,也就是协程之间的通讯能够用 Go 语言特有的通道来实现。下面是关于通道的一个例子。
func longTask(signal chan int) {
// 不带参数的 for
// 至关于 while 循环
for {
// 接收 signal 通道传值
v := <- signal
// 若是接收值为 1,中止循环
if v == 1 {
break
}
time.Sleep(1 * Second)
}
}
func main() {
// 声明通道
sig := make(chan int)
// 异步调用 longTask
go longTask(sig)
// 等待 1 秒钟
time.Sleep(1 * time.Second)
// 向通道 sig 传值
sig <- 1
// 而后 longTask 会接收 sig 传值,终止循环
}
Go 语言不是严格的面向对象编程(OOP),它采用的是面向接口编程(IOP),是相对于 OOP 更先进的编程模式。做为 OOP 体系的一部分,IOP 更增强调规则和约束,以及接口类型方法的约定,从而让开发人员尽量的关注更抽象的程序逻辑,而不是在更细节的实现方式上浪费时间。不少大型项目采用的都是 IOP 的编程模式。若是想了解更多面向接口编程,请查看 “码之道” 我的技术博客的往期文章《为何说 TypeScript 是开发大型前端项目的必备语言》,其中有关于面向接口编程的详细讲解。
Go 语言跟 TypeScript 同样,也是采用鸭子类型的方式来校验接口继承。下面这个例子能够描述 Go 语言的鸭子类型特性。
// 定义 Animal 接口
interface Animal {
Eat() // 声明 Eat 方法
Move() // 声明 Move 方法
}
// ==== 定义 Dog Start ====
// 定义 Dog 类
type Dog struct {
}
// 实现 Eat 方法
func (d *Dog) Eat() {
fmt.Printf("Eating bones")
}
// 实现 Move 方法
func (d *Dog) Move() {
fmt.Printf("Moving with four legs")
}
// ==== 定义 Dog End ====
// ==== 定义 Human Start ====
// 定义 Human 类
type Human struct {
}
// 实现 Eat 方法
func (h *Human) Eat() {
fmt.Printf("Eating rice")
}
// 实现 Move 方法
func (h *Human) Move() {
fmt.Printf("Moving with two legs")
}
// ==== 定义 Human End ====
能够看到,虽然 Go 语言能够定义接口,但跟 Java 不一样的是,Go 语言中没有显示声明接口实现(Implementation)的关键词修饰语法。在 Go 语言中,若是要继承一个接口,你只须要在结构体中实现该接口声明的全部方法。这样,对于 Go 编译器来讲你定义的类就至关于继承了该接口。在这个例子中,咱们规定,只要既能吃(Eat)又能活动(Move)的东西就是动物(Animal)。而狗(Dog)和人(Human)恰巧均可以吃和动,所以它们都被算做动物。这种依靠实现方法匹配度的继承方式,就是鸭子类型:若是一个动物看起来像鸭子,叫起来也像鸭子,那它必定是鸭子。这种鸭子类型相对于传统 OOP 编程语言显得更灵活。可是,后面咱们会讨论到,这种编程方式会带来一些麻烦。
Go 语言的错误处理是臭名昭著的啰嗦。这里先给一个简单例子。
package main
import "fmt"
func isValid(text string) (valid bool, err error){
if text == "" {
return false, error("text cannot be empty")
}
return text == "valid text", nil
}
func validateForm(form map[string]string) (res bool, err error) {
for _, text := range form {
valid, err := isValid(text)
if err != nil {
return false, err
}
if !valid {
return false, nil
}
}
return true, nil
}
func submitForm(form map[string]string) (err error) {
if res, err := validateForm(form); err != nil || !res {
return error("submit error")
}
fmt.Printf("submitted")
return nil
}
func main() {
form := map[string]string{
"field1": "",
"field2": "invalid text",
"field2": "valid text",
}
if err := submitForm(form); err != nil {
panic(err)
}
}
虽然上面整个代码是虚构的,但能够从中看出,Go 代码中充斥着 if err := ...; err != nil { ... }
之类的错误判断语句。这是由于 Go 语言要求开发者本身管理错误,也就是在函数中的错误须要显式抛出来,不然 Go 程序不会作任何错误处理。由于 Go 没有传统编程语言的 try/catch
针对错误处理的语法,因此在错误管理上缺乏灵活度,致使了 “err
满天飞” 的局面。
不过,辩证法则告诉咱们,这种作法也是有好处的。第一,它强制要求 Go 语言开发者从代码层面来规范错误的管理方式,这驱使开发者写出更健壮的代码;第二,这种显式返回错误的方式避免了 “try/catch
一把梭”,由于这种 “一时爽” 的作法极可能致使 Bug 没法准肯定位,从而产生不少不可预测的问题;第三,因为没有 try/catch
的括号或额外的代码块,Go 程序代码总体看起来更清爽,可读性较强。
Go 语言确定还有不少其余特性,但笔者认为以上的特性是 Go 语言中比较有特点的,是区分度比较强的特性。Go 语言其余一些特性还包括但不限于以下内容。
defer
延迟执行select/case
通道选择import "github.com/crawlab-team/go-trace"
)前面介绍了 Go 的不少语言特性,想必读者已经对 Golang 有了一些基本的了解。其中的一些语言特性也暗示了它相对于其余编程语言的优缺点。Go 语言虽然如今很火,在称赞并拥抱 Golang 的同时,不得不了解它的一些缺点。
这里笔者不打算长篇大论的解析 Go 语言的优劣,而是将其中相关的一些事实列举出来,读者能够自行判断。如下是笔者总结的 Golang 语言特性的不完整优缺点对比列表。
特性
优势
缺点
语法简单
提高开发效率,节省时间
难以处理一些复杂的工程问题
自然支持并发
极大减小异步编程的难度,提升开发效率
不熟悉通道和协程的开发者会有一些学习成本
类型系统
错误处理
强制约束错误管理,避免 “try/catch
一把梭”
啰嗦的错误处理代码,充斥着 if err := ...
编译迅速
这绝对是一个优势
怎么多是缺点?
很是规依赖管理
严重依赖 Github,在 Github 上搜索 Go 语言模块相对不精准
很是规日期格式
按照 6-1-2-3-4-5(2006-01-02 15:04:05),相对来讲比较好记
对于已经习惯了 yyyy-MM-dd HH:mm:ss 格式的开发者来讲很是不习惯
其实,每个特性在某种情境下都有其相应的优点和劣势,不能一律而论。就像 Go 语言采用的静态类型和面向接口编程,既不缺乏类型约束,也不像严格 OOP 那样冗长繁杂,是介于动态语言和传统静态类型 OOP 语言之间的现代编程语言。这个定位在提高 Golang 开发效率的同时,也阉割了很多必要 OOP 语法特性,从而缺少快速构建通用工程框架的能力(这里不是说 Go 没法构建通用框架,而是它没有 Java、C# 这么容易)。另外,Go 语言 “奇葩” 的错误处理规范,让 Go 开发者们又爱又恨:能够开发出更健壮的应用,但同时也牺牲了一部分代码的简洁性。要知道,Go 语言的设计理念是为了 “大道至简”,所以才会在追求高性能的同时设计得尽量简单。
无能否认的是,Go 语言内置的并发支持是很是近年来很是创新的特性,这也是它被分布式系统普遍采用的重要缘由。同时,它相对于动辄编译十几分钟的 Java 来讲是很是快的。此外,Go 语言没有由于语法简单而牺牲了稳定性;相反,它从简单的约束规范了整个 Go 项目代码风格。所以,“快”(Fast)、“简”(Concise)、“稳”(Robust)是 Go 语言的设计目的。咱们在对学习 Golang 的过程当中不能无脑的接纳它的一切,而是应该根据它自身的特性判断在实际项目应用中的状况。
通过前文关于 Golang 各个维度的讨论,咱们能够得出结论:Go 语言并非后端开发的万能药。在实际开发工做中,开发者应该避免在任何状况下无脑使用 Golang 做为后端开发语言。相反,工程师在决定技术选型以前应该全面了解候选技术(语言、框架或架构)的方方面面,包括候选技术与业务需求的切合度,与开发团队的融合度,以及其学习、开发、时间成本等因素。笔者在学习了包括先后端的一些编程语言以后,发现它们各自有各自的优点,也有相应的劣势。若是一门编程语言能广为人知,那它绝对不会是一门糟糕语言。所以,笔者不会断言 “XXX 是世界上最好的语言“,而是给读者分享我的关于特定应用场景下技术选型的思路。固然,本文是针对 Go 语言的技术文,接下来笔者将分享一下我的认为 Golang 最适合的应用场景。
Golang 是很是适合在分布式应用场景下开发的。分布式应用的主要目的是尽量多的利用计算资源和网络带宽,以求最大化系统的总体性能和效率,其中重要的需求功能就是并发(Concurrency)。而 Go 是支持高并发和异步编程方面的佼佼者。前面已经提到,Go 语言内置了协程(Goroutine)和通道(Channel)两大并发特性,这使后端开发者进行异步编程变得很是容易。Golang 中还内置了sync
库,包含 Mutex
(互斥锁)、WaitGroup
(等待组)、Pool
(临时对象池)等接口,帮助开发者在并发编程中能更安全的掌控 Go 程序的并发行为。Golang 还有不少分布式应用开发工具,例如分布式储存系统(Etcd、SeaweedFS)、RPC 库(gRPC、Thrift)、主流数据库 SDK(mongo-driver、gnorm、redigo)等。这些均可以帮助开发者有效的构建分布式应用。
稍微了解网络爬虫的开发者应该会据说过 Scrapy,再不济也是 Python。市面上关于 Python 网络爬虫的技术书籍数不胜数,例如崔庆才的《Python 3 网络开发实战》和韦世东的《Python 3 网络爬虫宝典》。用 Python 编写的高性能爬虫框架 Scrapy,自发布以来一直是爬虫工程师的首选。
不过,因为近期 Go 语言的迅速发展,愈来愈多的爬虫工程师注意到用 Golang 开发网路爬虫的巨大优点。其中,用 Go 语言编写的 Colly 爬虫框架,现在在 Github 上已经有 13k+ 标星。其简洁的 API 以及高效的采集速度,吸引了不少爬虫工程师,占据了爬虫界一哥 Scrapy 的部分份额。前面已经提到,Go 语言内置的并发特性让严重依赖网络带宽的爬虫程序更加高效,很大的提升了数据采集效率。另外,Go 语言做为静态语言,相对于动态语言 Python 来讲有更好的约束下,所以健壮性和稳定性都更好。
Golang 有不少优秀的后端框架,它们大部分都很是完备的支持了现代后端系统的各类功能需求:RESTful API、路由、中间件、配置、鉴权等模块。并且用 Golang 写的后端应用性能很高,一般有很是快的响应速度。笔者曾经在开源爬虫管理平台 Crawlab 中用 Golang 重构了 Python 的后端 API,响应速度从以前的几百毫秒优化到了几十毫秒甚至是几毫秒,用实践证实 Go 语言在后端性能方面全面碾压动态语言。Go 语言中比较知名的后端框架有 Gin、Beego、Echo、Iris。
固然,这里并非说用 Golang 写后端就彻底是一个正确的选择。笔者在工做中会用到 Java 和 C#,用了各自的主流框架(SpringBoot 和 .Net Core)以后,发现这两门传统 OOP 语言虽然语法啰嗦,但它们的语法特性很丰富,特别是泛型,可以轻松应对一些逻辑复杂、重复性高的业务需求。所以,笔者认为在考虑用 Go 来编写后端 API 时候,能够提早调研一下 Java 或 C#,它们在写后端业务功能方面作得很是棒。
本篇文章从 Go 语言的主要语法特性入手,按部就班分析了 Go 语言做为后端编程语言的优势和缺点,以及其在实际软件项目开发中的试用场景。笔者认为 Go 语言与其余语言的主要区别在于语法简洁、自然支持并发、面向接口编程、错误处理等方面,而且对各个语言特性在正反两方面进行了分析。最后,笔者根据以前的分析内容,得出了 Go 语言做为后端开发编程语言的适用场景,也就是分布式应用、网络爬虫以及后端API。固然,Go 语言的实际应用领域还不限于此。实际上,很多知名数据库都是用 Golang 开发的,例如时序数据库 Prometheus 和 InfluxDB、以及有 NewSQL 之称的 TiDB。此外,在机器学习方面,Go 语言也有必定的优点,只是目前来讲,Google 由于 Swift 跟 TensorFlow 的意向合做,彷佛尚未大力推广 Go 在机器学习方面的应用,不过一些潜在的开源项目已经涌现出来,例如 GoLearn、GoML、Gorgonia 等。
在理解 Go 语言的优点和适用场景的同时,咱们必须意识到 Go 语言并非全能的。它相较于其余一些主流框架来讲也有一些缺点。开发者在准备采用 Go 做为实际工做开发语言的时候,须要全面了解其语言特性,从而作出最合理的技术选型。就像打网球同样,不只须要掌握正反手,还要会发球、高压球、截击球等技术动做,这样才能把网球打好。
若是您对笔者的文章感兴趣,能够加笔者微信 tikazyq1 并注明 "码之道",笔者会将你拉入 "码之道" 交流群。