大红大紫的 Golang 真的是后端开发中的万能药吗?

2021年3月25日 284点热度 0人点赞 0条评论

前言

城外的人想进去,城里的人想出来。-- 钱钟书《围城》

随着容器编排(Container Orchestration)、微服务(Micro Services)、云技术(Cloud Technology)等在 IT 行业不断盛行,2009 年诞生于 Google 的 Golang(Go 语言,简称 Go)越来越受到软件工程师的欢迎和追捧,成为如今炙手可热的后端编程语言。在用 Golang 开发的软件项目列表中,有 Docker[1](容器技术)、Kubernetes[2](容器编排)这样的颠覆整个 IT 行业的明星级产品,也有像 Prometheus[3](监控系统)、Etcd[4](分布式存储)、InfluxDB[5](时序数据库)这样的强大实用的知名项目。当然,Go 语言的应用领域也绝不局限于容器和分布式系统。如今很多大型互联网企业在大量使用 Golang 构建后端 Web 应用,例如今日头条、京东、七牛云等;长期被 Python 统治的框架爬虫领域也因为简单而易用的爬虫框架 Colly[6] 的崛起而不断受到 Golang 的挑战。Golang 已经成为了如今大多数软件工程师最想学习的编程语言。下图是 HackerRank[7] 在 2020 年调查程序员技能的相关结果。

图片
hackerrank-survey-2020

那么,**Go 语言真的是后端开发人员的救命良药呢?它是否能够有效提高程序员们的技术实力和开发效率,从而帮助他们在职场上更进一步呢?Go 语言真的值得我们花大量时间深入学习么?**本文将详细介绍 Golang 的语言特点以及它的优缺点和适用场景,带着上述几个疑问,为读者分析 Go 语言的各个方面,以帮助初入 IT 行业的程序员以及对 Go 感兴趣的开发者进一步了解这个热门语言。

Golang 简介

图片
golang

Golang 诞生于互联网巨头 Google,而这并不是一个巧合。我们都知道,Google 有一个 20% 做业余项目(Side Project)的企业文化,允许工程师们能够在轻松的环境下创造一些具有颠覆性创新的产品。而 Golang 也正是在这 20% 时间中不断孵化出来。Go 语言的创始者也是 IT 界内大名鼎鼎的行业领袖,包括 Unix 核心团队成员 Rob Pike、C 语言作者 Ken Thompson、V8 引擎核心贡献者 Robert Griesemer。Go 语言被大众所熟知还是源于容器技术 Docker 在 2014 年被开源后的爆发式发展。之后,Go 语言因为其简单的语法以及迅猛的编译速度受到大量开发者的追捧,也诞生了很多优秀的项目,例如 Kubernetes。

Go 语言相对于其他传统热门编程语言来说,有很多优点,特别是其「高效编译速度」「天然并发特性」,让其成为「快速开发分布式应用」的首选语言。Go 语言是「静态类型语言」,也就是说 Go 语言跟 Java、C# 一样需要编译,而且有完备的类型系统,可以有效减少因类型不一致导致的「代码质量问题」。因此,Go 语言非常适合构建对「稳定性」「灵活性」均有要求的大型 IT 系统,这也是很多大型互联网公司用 Golang 重构老代码的重要原因:传统的静态 OOP 语言(例如 Java、C#)稳定性高但缺乏灵活性;而动态语言(例如 PHP、Python、Ruby、Node.js)灵活性强但缺乏稳定性。因此,“熊掌和鱼兼得” 的 Golang,受到开发者们的追捧是自然而然的事情,毕竟,“天下苦 Java/PHP/Python/Ruby 们久矣“。

不过,「Go 语言并不是没有缺点」。用辩证法的思维方式可以推测,Golang 的一些突出特性将成为它的双刃剑。例如,Golang 语法简单的优势特点将限制它处理复杂问题的能力。尤其是 Go 语言缺乏泛型(Generics)的问题,导致它构建通用框架的复杂度大增。虽然这个突出问题在 2.0 版本很可能会有效解决,但这也反映出来明星编程语言也会有缺点。当然,Go 的缺点还不止于此,Go 语言使用者还会吐槽其啰嗦的错误处理方式(Error Handling)、缺少严格约束的鸭子类型(Duck Typing)、日期格式问题等。下面,我们将从 Golang 语言特点开始,由浅入深多维度深入分析 Golang 的优缺点以及项目适用场景。

语言特点

简洁的语法特征

Go 语言的语法非常简单,至少在变量声明、结构体声明、函数定义等方面显得非常简洁。

变量的声明不像 Java 或 C 那样啰嗦,在 Golang 中可以用 := 这个语法来声明新变量。例如下面这个例子,当你直接使用 := 来定义变量时,Go 会自动将赋值对象的类型声明为赋值来源的类型,这节省了大量的代码。

func main() {
  valInt := 1  // 自动推断 int 类型
  valStr := "hello"  // 自动推断为 string 类型
  valBool := false  // 自动推断为 bool 类型
}

Golang 还有很多帮你节省代码的地方。你可以发现 Go 中不会强制要求用 new 这个关键词来生成某个类(Class)的新实例(Instance)。而且,对于公共和私有属性(变量和方法)的约定不再使用传统的 publicprivate 关键词,而是直接用属性变量首字母的大小写来区分。下面一些例子可以帮助读者理解这些特点。

// 定义一个 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 代码中除了容易看花眼的多层花括号以外,还充斥着大量的 publicprivatestaticthis 等修饰用的关键词,显得异常啰嗦;而 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/awaitPromise 语法,甚至是 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 falsenil
    }
  }
  return truenil
}

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 通道选择
  • 直接编译成可执行程序
  • 非常规依赖管理(可以直接引用 Github 仓库作为依赖,例如 import "github.com/crawlab-team/go-trace"
  • 非常规日期格式(格式为 "2006-01-02 15:04:05",你没看错,据说这就是 Golang 的创始时间!)

优缺点概述

前面介绍了 Go 的很多语言特性,想必读者已经对 Golang 有了一些基本的了解。其中的一些语言特性也暗示了它相对于其他编程语言的优缺点。Go 语言虽然现在很火,在称赞并拥抱 Golang 的同时,不得不了解它的一些缺点。

这里笔者不打算长篇大论的解析 Go 语言的优劣,而是将其中相关的一些事实列举出来,读者可以自行判断。以下是笔者总结的 Golang 语言特性的不完整优缺点对比列表。

特性 优点 缺点
语法简单 提升开发效率,节省时间 难以处理一些复杂的工程问题
天然支持并发 极大减少异步编程的难度,提高开发效率 不熟悉通道和协程的开发者会有一些学习成本
类型系统
  • Go 语言是静态类型,相对于动态类型语言更稳定和可预测

  • IOP 鸭子类型比严格的 OOP 语言更简洁

  • 没有继承、抽象、静态、动态等特性

  • 缺少泛型,导致灵活性降低

  • 难以快速构建复杂通用的框架或工具

错误处理 强制约束错误管理,避免 “try/catch 一把梭” 啰嗦的错误处理代码,充斥着 if err := ...
编译迅速 这绝对是一个优点 怎么可能是缺点?
非常规依赖管理
  • 可以直接引用发布到 Github 上的仓库作为模块依赖引用,省去了依赖托管的官方网站

  • 可以随时在 Github 上发布 Go 语言编写的第三方模块

  • 自由的依赖发布意味着 Golang 的生态发展将不受官方依赖托管网站的限制

严重依赖 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 网络开发实战》[8]和韦世东的《Python 3 网络爬虫宝典》[9]。用 Python 编写的高性能爬虫框架 Scrapy,自发布以来一直是爬虫工程师的首选。

不过,由于近期 Go 语言的迅速发展,越来越多的爬虫工程师注意到用 Golang 开发网路爬虫的巨大优势。其中,用 Go 语言编写的 Colly 爬虫框架,如今在 Github 上已经有 13k+ 标星。其简洁的 API 以及高效的采集速度,吸引了很多爬虫工程师,占据了爬虫界一哥 Scrapy 的部分份额。前面已经提到,Go 语言内置的并发特性让严重依赖网络带宽的爬虫程序更加高效,很大的提高了数据采集效率。另外,Go 语言作为静态语言,相对于动态语言 Python 来说有更好的约束下,因此健壮性和稳定性都更好。

后端 API

Golang 有很多优秀的后端框架,它们大部分都非常完备的支持了现代后端系统的各种功能需求:RESTful API、路由、中间件、配置、鉴权等模块。而且用 Golang 写的后端应用性能很高,通常有非常快的响应速度。笔者曾经在开源爬虫管理平台 Crawlab[10] 中用 Golang 重构了 Python 的后端 API,响应速度从之前的几百毫秒优化到了几十毫秒甚至是几毫秒,用实践证明 Go 语言在后端性能方面全面碾压动态语言。Go 语言中比较知名的后端框架有 Gin[11]Beego[12]Echo[13]Iris[14]

当然,这里并不是说用 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 并注明 "码之道",笔者会将你拉入 "码之道" 交流群。

Reference

[1]

Docker: https://www.docker.com/

[2]

Kubernetes: https://kubernetes.io/

[3]

Prometheus: https://prometheus.io/

[4]

Etcd: https://etcd.io/

[5]

InfluxDB: https://www.influxdata.com/

[6]

Colly: https://github.com/gocolly/colly

[7]

HackerRank: https://hackerrank.com

[8]

《Python 3 网络开发实战》: https://item.jd.com/12333540.html

[9]

《Python 3 网络爬虫宝典》: https://item.jd.com/10022002756825.html

[10]

Crawlab: https://github.com/crawlab-team/crawlab

[11]

Gin: https://github.com/gin-gonic/gin

[12]

Beego: https://github.com/beego/beego

[13]

Echo: https://github.com/labstack/echo

[14]

Iris: https://github.com/kataras/iris

66070大红大紫的 Golang 真的是后端开发中的万能药吗?

这个人很懒,什么都没留下

文章评论