11 月 19 - 20 日 Apache Pulsar 社区年度盛会来啦,立即报名! 了解详情
写点什么

Go 并发编程之 Go 语言概述

  • 2014-09-28
  • 本文字数:5252 字

    阅读完需:约 17 分钟

1. Go 语言从何而来?

关于 Go 语言的萌芽时期,我们可以追溯至上个世纪。不过,直至 2009 年,它才真正被披露,并成为开源大家庭中的一员。在 2012 年,Go 语言的创造者们发布了它的 1.0 版本。大家可能有所耳闻,Go 语言出自 Google 公司。但很多人可能并不清楚,它的创造者们更是名头不小。他们包括 Unix 操作系统和 B 语言(C 语言的前身)的创造者、UTF-8 编码的发明者 Ken Thompson,Unix 项目的参与者、UTF-8 编码的联合创始人和 Limbo 编程语言(Go 语言的前身)的创造者 Rob Pike,以及著名的 Javascript 引擎 V8 的创造者 Robert Griesemer。正因为有了他们的引领,一批又一批的全球顶尖计算机软件人才都相继加入到了 Go 语言项目中。

Go 语言是一门强类型的通用编程语言。它的基础语法与 C 语言很类似,但同时也对其他的一些优秀编程语言有所借鉴。它的很多设计灵感都来源于 Tony Hoare 执笔的那篇著名的论文《Communicating Sequential Processes》。

2. Go 语言意味着什么?

Go 语言意味着更自由、更高效和一站式的编程体验,程序开发效率和运行效率之间的完美融合,以及天生的并发编程支持。

我们下面重点说说 Go 语言对各种流行的编程范式的支持,以及它对并发编程的强悍支持。

2.1 自由的编程方式

使用 Go 语言就意味着你可以用自己喜欢的方式编程。因为 Go 语言支持当今所有主流的编程范式。这包括面向对象编程、函数式编程,以及过程式编程。

面向对象编程

面向对象的设计和编程可以使我们更容易的对现实世界进行建模。这样可以编写出让人类更易懂的代码,并且还会大大增强代码的可维护性和可扩展性。Go 语言支持面向对象编程。因此,我们的 Go 语言程序也可以具备上述优势。面向对象编程中的很多重要原则都可以很容易的在 Go 语言程序中体现出来,比如:“针对接口编程,而不是针对实现编程”、“对扩展开放,对修改关闭”和“多用组合,少用继承”等等。另外,虽然 Go 语言中并没有继承的概念,但我们依然可以用类型嵌入的方式来模仿继承并达到相同的效果。

函数式编程

函数式编程同样可以让我们更容易的跟进变化。它可以让我们的程序实体的粒度更加小巧,从而使程序更加易变。此外,函数式编程还可以让我们的程序更加健壮,因为函数本身是没有状态的。要知道,对各种状态的维护会使程序更加复杂和脆弱。Go 语言对函数式编程提供支持的一个体现就是:它的函数是绝对的一等类型。这是函数式编程的一个重要特征。更具体地说,我们可以把一个函数作为传给其他函数的参数,或成为其他函数返回的结果。这是构建闭包的必要条件。同时,这也意味着,我们可以对程序的可变部分进行更加灵活的控制和管理。

过程式编程

至于过程式编程,我们自不必多说。程序员们应该再熟悉不过了。过程式编程最直接的体现了程序的本质,同时也是简单程序的最常用的编写手法。我们可以用脚本语言写出纯过程化的程序。我之前常常使用 Python 代码来做这类事情。因为它可以像 Shell 脚本那样工作,并且总是能够保持简单。我现在的选择当然是用 Go 语言。不论是语言语法还是程序运行方式,Go 语言都非常的脚本化。简约和易用是它的两个显著特点。

2.2 便捷的并发编程

毋庸置疑,Go 语言对并发编程的支持是天生的、自然的和高效的。Go 语言为此专门创造出了一个关键字“go”。使用这个关键字,我们就可以很容易的使一个函数被并发的执行。就像这样:

复制代码
go func() {
fmt.Println("Concurrent execution!")
}()

上面的这段代码使用关键字“go”并发的执行了一个匿名函数,虽然这个匿名函数只会在标准输出上打印一句话而已。更确切地说,该匿名函数会在一个单独的 Goroutine 中(或称 Go 程)被执行。我们把“go”和跟在它后面的函数以及调用符号“(”和“)”合称为 go 语句,而把那个匿名函数称为 go 函数。

如果我们要在不同的 Goroutine 中运行的函数之间传递数据,那么我们可以使用 Channel(也可称其为通道)。这也是 Go 语言强烈推荐的做法。

我们可以用程序模仿自来水厂的净水设施。这个净水设施会引导水流先后流经三个净水装置,最后产出可饮用的自来水。为了更好理解,我们用过滤数字来代表对水的过滤。

我们构建两个数字过滤装置、一个数字输出装置和三个数字通道。下面是三个数字通道的创建和初始化代码:

复制代码
numberChan1 := make(chan int64, 3) // 数字通道 1。
numberChan2 := make(chan int64, 3) // 数字通道 2。
numberChan3 := make(chan int64, 3) // 数字通道 3。

Go 语言的内建函数 make 可以用于创建和初始化一个通道。我们在这里定义,通道的元素类型都是 int64,而长度都是 3。另外,特殊标记“:=”可被用来在函数中声明并初始化变量。这种情况下,我们可以省略掉变量的类型的声明。

这里有一点需要特别说明,运行 main 函数的 Goroutine(也被称为主 Goroutine)会一条接一条地执行 main 函数中的语句,不论这些语句中是否存在以及有多少条 go 语句。换句话说,主 Goroutine 并不会等到其他被启用的 Goroutine 运行结束之后再结束自身的运行。因此,如果我们不采取任何措施的话,很可能在欲执行的 go 函数得到执行机会之前主 Goroutine 就已经结束运行了。一旦如此,当前程序的执行也就会宣告结束。这也意味着,那些 go 函数中的语句根本就不会被执行。

所以,我们在这里需要再声明一个变量,并稍微设置一下。这个变量代表了一个同步工具。对它的合理运用可以避免上述情况的发生。对这个变量的声明和初始化的代码如下:

复制代码
var waitGroup sync.WaitGroup // 用于等待一组操作执行完毕的同步工具。
waitGroup.Add(3) // 该组操作的数量是 3

标识符 sync.WaitGroup 代表了一个类型。该类型的声明存在于代码包 sync 中,类型名为 WaitGroup。另外,上面的第二条语句预示着我们将要后面启用三个 Goroutine,或者说要并发的执行三个 go 函数。请记住,我们在这里进行了一个“加 3”的操作。

我们下面依次展现这三个 go 函数,并说明它们的功用。先来看第一个 go 函数,包含它的 go 语句如下:

复制代码
go func() { // 数字过滤装置 1。
for n := range numberChan1 { // 不断的从数字通道 1 中接收数字,直到该通道关闭。
if n%2 == 0 { // 仅当数字可以被 2 整除,才将其发送到数字通道 2.
numberChan2 <- n
} else {
fmt.Printf("Filter %d. [filter 1]\n", n)
}
}
close(numberChan2) // 关闭数字通道 2。
waitGroup.Done() // 表示一个操作完成。
}()

这段代码代表了数字装置 1 的功能。下面是对其中的 go 函数的解释:

  • 函数的第一条语句为 for 语句。它会不停的从数字通道 numberChan1 中接收元素值(在这里是数字)并进行处理,直到 numberChan1 被关闭。
  • 只有可以被 2 整除的数才可以被送往数字通道 numberChan2,否则就会被过滤掉。为了方便查看,我们每过滤一个数字都会打印出一句话。
  • 符号“<-”被称为接收操作符。在这里,它会把标识符 n 代表的数字发送给数字通道 numberChan2。
  • 由于 numberChan1 通道的关闭会使这里的 for 语句结束执行。这意味着上游不会再有任何数字“流出”。所以,我们在这条 for 语句的后面顺势关闭通道 numberChan2。这也是为了告诉它的下游,没有更多的数字需要被过滤了。
  • 对 waitGroup 的 Done 方法的调用表示了数字装置 1 已经完成了所有工作。该方法会进行相应的“减 1”操作。请记住它,我们后面会对此进行说明。

我们完成了数字过滤装置 1 的编写。数字过滤装置 2 的功能与此如出一辙。只不过它会过滤掉不能被 5 整除的数字。大家应该可以仿照上面的代码写出数字过滤装置 2 的实现代码。注意,数字过滤装置 2 会试图从数字通道 numberChan2 中接收数字,并将未被过滤的数字发送给数字通道 numberChan3。如此一来,数字过滤装置 1 和 2 就经由数字通道 2 串联起来了。请注意,不要忘记在数字过滤装置 2 中的 for 语句后面添加对数字通道 numberChan3 的关闭操作,以及调用 waitGroup 变量的 Done 方法。这非常重要。

现在我们来看数字输出装置。它即由第三条 go 语句代表。以下是具体代码:

复制代码
go func() { // 数字输出装置。
for n := range numberChan3 { // 不断的从数字通道 3 中接收数字,直到该通道关闭。
fmt.Println(n) // 打印数字。
}
waitGroup.Done() // 表示一个操作完成。
}()

这个 go 函数的功能就非常简单了。它只是打印出“通过净化”的数字。不过,waitGroup.Done() 语句依然被包含在内。

好了,我们已经编写出了所有的装置,并合理运用了那三个数字通道。有一点值得说明,到相应的数字通道中没有任何数字可取时,for 语句的执行会被阻塞。也正因为如此,我们可以把这三条 go 语句放置在前,并在之后激活这一过滤数字的流程。具体的激活方法是,向数字通道 numberChan1 发送数字。相关的代码如下:

复制代码
for i := 0; i < 100; i++ { // 先后向数字通道 1 传送 100 个范围在 [0,100) 的随机数。
numberChan1 <- rand.Int63n(100)
}
close(numberChan1) // 数字发送完毕,关闭数字通道 1

这段代码的意图很明显。我们先向数字通道 numberChan1 发送 100 个随机数,然后关闭 numberChan1 通道以表示所以需要过滤的数字都发送完毕。放心,对通道的关闭并不会影响到对已存于其中的数字的接收操作。

好了,我们至此实现了一个完整的数字过滤流程。为了能够让这个流程能够被完整的执行,我们还需要在最后加入这样一条语句:

复制代码
waitGroup.Wait() // 等待前面那组操作(共 3 个)的完成。

对 waitGroup 的 Wait 方法的调用会一直被阻塞,直到前面三个 go 函数中的三个 waitGroup.Done() 语句(即那三个“减 1 操作”)都被执行完毕。“加 3”操作使变量 waitGroup 的状态有所改变,并以此阻塞住了之后的 waitGroup.Wait() 语句的执行。而后续被并发执行的三个“减 1”操作的执行又使变量 waitGroup 的状态回归初始。这才能让对 waitGroup.Wait() 语句的执行从阻塞中恢复并完成。这是防止主 Goroutine 过早的被运行结束的有效手段之一。

下面是一幅可以宏观的展示数字过滤流程的图示。

图 1-1 数字过滤流程

我们把上述代码都放入到一个命令源码文件的 main 函数中。并在文件的开始处添加一条代码包导入语句:

复制代码
import (
"fmt"
"math/rand"
"sync"
)

大家可以试着使用“go run”命令运行这个命令源码文件,并观察输出结果。

我们配合使用 Goroutine 和 Channel 让数字过滤流程实现了全异步化,但却没有增加开发的难度。Channel 可以让我们以管道的方式在多个 Goroutine 之间交换数据。这也恰恰实践和印证了这句话:

Do not communicate by sharing memory; instead, share memory by communicating.

在这之中起到关键作用的 Channel 有着非常灵活、多样的使用方法。在后续的文章中,我会专门就此进行论述。

虽然我们可以使用其他语言的代码实现这样的异步化,但是它们不会像 Go 语言这样为此提供语言级别的原生支持。也正是由于这个原因,那些代码会看起来复杂得多。它们不得不使用若干个类库或辅助工具来满足异步化的要求。另一方面,Go 语言先进、高效的并发编程模型及其实现系统会使得程序对系统资源的消耗大大减少,并且会在很大程度上提高对这些资源的使用效率。这一优势是很多其他语言望尘莫及的。同样,我会在后面简明扼要的说明 Goroutine 的运作机理。

3. Go 语言的哲学

通过对前面内容的阅读,大家应该能够隐约的感觉到 Go 语言的关注点,以及它想为软件开发者们带来的启示和新思想。

作为本篇文章的总结,我在下面列出几点最重要的 Go 语言的哲学:

  1. Go 语言集众多编程范式之所长,并以自己独到的方式将它们融合在一起。程序员们可以用他们喜欢的风格去设计程序。
  2. 相对于设计规则上的灵活,Go 语言有着明确且近乎严格的编码规范。我们可以通过“go fmt”命令来按照官方的规范格式化代码。
  3. Go 语言是强调软件工程的编程语言。它自带了非常丰富的标准命令,涵盖了软件生命周期(开发、测试、部署、维护等等)的各个环节。
  4. Go 语言是云计算时代的编程语言。它关注高并发程序,并旨在开发效率和运行效率上取得平衡。
  5. Go 语言提倡交换数据,而不是共享数据。它的并发原语 Goroutine 和 Channel 是其中的两大并发编程利器。同时,Go 语言也提供了丰富的同步工具,以供程序员们根据场景选用。然而,后者就不属于语言级别的支持了。

4. 下期预告

在下一篇文章中,我会带领大家真正进入 Go 语言并发编程的世界。作为入门,学会怎样正确的利用 go 语句并发的执行 go 函数总是必须的。在搞清 go 语句的使用之后,我们还会一窥 Goroutine 背后的运行机理。正所谓知其然更知其所以然。

5. 作者介绍

郝林,微博名:特价萝卜。Go 语言爱好者、高级 Java 软件工程师、Python 程序员和 Linux 爱好者。目前在宜信公司的小微企业增值服务中心任软件系统架构师。曾在搜狐网多年,并任 Java 项目经理。在互联网软件的设计和开发方面拥有丰富的实战经验。著有《Go 并发编程实战》一书,并在 GitHub 上发布了免费的《Go 命令教程》


感谢郭蕾对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2014-09-28 04:0513843
用户头像

发布了 21 篇内容, 共 15.2 次阅读, 收获喜欢 82 次。

关注

评论

发布
暂无评论
发现更多内容

MobLink Android 快速集成文档

MobTech袤博科技

sdk Android;

了解布隆过滤器

自然

Java core 9月月更

你必须知道的Java泛型

自然

Java core 9月月更

[MyBatisPlus]DQL编程控制①(条件查询)

十八岁讨厌编程

Java 后端开发 9月月更

[MyBatisPlus]DQL编程控制②(查询投影、查询条件)

十八岁讨厌编程

Java 后端开发 9月月更

私有化的即时通讯工具能为企业带来哪些帮助?

WorkPlus Lite

Databend 特性系列(1)|Databend 数据生命周期

Databend

大数据 大数据 开源 数据生命周期

阿里云EMAS移动测试|快速掌握移动端兼容性测试技巧

移动研发平台EMAS

阿里云 应用开发 兼容性测试 移动测试

新零售数智化转型,需要怎样的数据底座?

OceanBase 数据库

前端二面面试题(附答案)

helloworld1024fd

JavaScript 前端

大学三年狂拿国内外十几个3D挑战赛大奖?!国内CG新星崛起

Renderbus瑞云渲染农场

CG 云渲染 3D动画 渲染农场 Renderbus瑞云渲染

Python中的super函数,你熟吗

华为云开发者联盟

Python 开发 企业号九月金秋榜

MobLink for Flutter

MobTech袤博科技

flutter ios android

[Spring boot] Spring boot 整合RabbitMQ实现通过RabbitMQ进行项目的连接

Java快了!

Spring Boot

[SpringBoot系列]基础过渡与夯实(基础配置)

十八岁讨厌编程

Java 后端开发 9月月更

开奖啦!看看8月月更获奖名单有没有你?

InfoQ写作社区官方

热门活动 8月月更

字节跳动基于ClickHouse优化实践之“高可用”

字节跳动数据平台

数据库 大数据 Clickhouse 数据开发 数据计算

虚实交互,重磅开启|共建多元、互联的元宇宙产业生态圈,赋能上海打造产业高地

Geek_2d6073

SpringBoot 源码 | applicationContext.refresh() 方法解析

六月的雨在InfoQ

springboot 源码阅读 Refresh 9月月更 SpringBoot启动流程

开源云管平台有哪些?有哪些优势?

行云管家

云计算 云平台 云管平台 云管理

毫末智行董事长张凯:渐进式路线将在智能驾驶竞赛中赢得终局

科技大数据

【云原生】Kubernetes操作精讲

陈橘又青

9月月更

高频面试题:谈谈你对 Spring Boot 自动装配机制的理解

Java快了!

Spring Boot

网络安全周是什么意思?为什么要开展网络安全周?

行云管家

网络安全 网络安全周

新一代开源时序数据库TDengine有哪些优势?

TDengine

数据库 tdengine 开源 企业号九月金秋榜

Istio Ambient Mesh 介绍

Se7en

WorkPlus移动应用管理平台 | 政企数字化的超级“连接器”

WorkPlus Lite

led显示屏有污垢时该怎么清洗?

Dylan

LED显示屏 led显示屏厂家

Go并发编程之Go语言概述_Google_郝林_InfoQ精选文章