本文由公众号EAWorld翻译发表,转载需注明出处。
作者:Velotio Technologies
译者:白小白
原文:http://t.cn/EaQTFFv
原题:Introduction to the Modern Server-side Stack — Golang, Protobuf, and gRPC
译注:并发与并行:并发是虚拟的并行,比如通过时间切片技术在单核CPU上运行多个任务,让每个使用者“以为”自己在独占这一CPU资源;并行是实际的同一时间多任务同时运行,多数是指在多核CPU的场景下。
队列与双端队列:队列遵循先入先出的原则,从一端存数,从另一端取数,双端队列支持从队列的两端存数和取数。
阻塞和非阻塞:阻塞和非阻塞描述了程序等待返回结果时的状态,阻塞代表不返回结果就挂起,不进行任何操作;非阻塞是在没返回结果时可以执行其他任务。
合作和抢占:高优先级任务可以打断其他正在运行的低优先级任务,则调度器是抢占式的;反之,则是合作式的。
服务端编程的阵营中有很多新面孔,一水儿的谷歌血统。在谷歌开始将Golang应用于其产品系统后,Golang快速的吸引了大量的关注。随着微服务架构的兴起,人们开始关注一些现代的数据通信解决方案,如gRPC和Protobuf。在本文中,我们会对以上这些概念作一些简要的介绍。
一、GolangGolang又称Go语言,是一个开源的、多用途的编程语言,由Google研发,并由于种种原因,正在日益流行。Golang已经有10年的历史,并且据Google称已经在生产环境中使用了接近7年的时间,这一点可能让大多数人大跌眼镜。
Golang的设计理念是,简单、现代、易于理解和快速上手。Golang的创造者将Golang设计为一个普通的程序员可以在一个周末的时间就可以掌握,并达到使用Golang进行工作的程度。这一点我已经亲身证实。Golang的创造者,都是C语言原始草案的专家组成员,可以说,Golang根红苗正,值得信赖。
理都懂,但话说回来,为什么我们需要另一门编程语言呢
多数场景下,确实并不需要。事实上,Go语言并不能解决其他语言或工具无法解决的新问题。但在一些强调效率、优雅与直观的场景下,人们通常会面临一系列的相关问题,这正是Go所致力于解决的领域。Go的主要特点是:
- 一流的并发支持
- 内核十分简单,语言优雅、现代
- 高性能
- 提供现代软件开发所需要的原生工具支持
我将简要介绍Go是如何提供上述的支持的。在Go语言的官网可以了解更多的特性和细节。
一流的并发支持
并发是多数服务端应用所需要考虑的主要问题之一,考虑到现代微处理器的特性,并发也成为编程语言的主要关切之一。Go语言引入了“goroutine”的理念。可以把“goroutine”理解为一个“轻量级的用户空间线程”(现实中,当然远比这要复杂得多,同一线程可能会附着多路的goroutine,但这样的提法可以让你有一个大致的概念)。所谓“轻量级”,可以这样理解,由于采用了十分袖珍的堆栈,你可以同时启动数以百万计的goroutine,事实上这也是Go语言官方所推荐的方式。在Go语言中,任何函数或方法都可以生成一个goroutine。比如,只需要运行“go myAsyncTask()”就可以从“myAsyncTask”函数生成一个goroutine。示例代码如下:
// This function performs the given task concurrently by spawing a goroutine// for each of those tasks.func performAsyncTasks(task []Task) { for _, task := range tasks { // This will spawn a separate goroutine to carry out this task. // This call is non-blocking go task.Execute() }}
goroutineExample.go hosted with ❤ by GitHub
怎么样,是不是很简单?Go是一门简单的语言,因此注定是以这样的方式来解决问题。你可以为每个独立的异步任务生成一个goroutine而不需要顾虑太多事情。如果处理器支持多核运行,Go语言运行时会自动的以并行的方式运行所有的goroutine。那么,goroutine之间是如何通信的呢,答案是channel。
“channel”也是Go语言的一个概念,用于进行goroutine之间的通信。通过channel,你可以向另一个goroutine传递各种信息(比如Go语言概念里的type或者struct甚至是channel)。一个channel大体上是一个“双端阻塞队列”(也可以单端的)。如果需要goroutine基于特定条件触发下一步的行动,也可以利用channel来实现goroutine的协作阻塞任务模式。
在编写异步或者并发的代码时,goroutine和channel这两个概念赋予了编程者大量的灵活性和简便性。可以籍此很容易的建立其他很有用的库,比如goroutine pool,举个简单的例子:
package executorimport ( "log" "sync/atomic")// The Executor struct is the main executor for tasks.// 'maxWorkers' represents the maximum number of simultaneous goroutines.// 'ActiveWorkers' tells the number of active goroutines spawned by the Executor at given time.// 'Tasks' is the channel on which the Executor receives the tasks.// 'Reports' is channel on which the Executor publishes the every tasks reports.// 'signals' is channel that can be used to control the executor. Right now, only the termination// signal is supported which is essentially is sending '1' on this channel by the client.type Executor struct { maxWorkers int64 ActiveWorkers int64 Tasks chan Task Reports chan Report signals chan int}// NewExecutor creates a new Executor.// 'maxWorkers' tells the maximum number of simultaneous goroutines.// 'signals' channel can be used to control the Executor.func NewExecutor(maxWorkers int, signals chan int) *Executor { chanSize := 1000 if maxWorkers > chanSize { chanSize = maxWorkers } executor := Executor{ maxWorkers: int64(maxWorkers), Tasks: make(chan Task, chanSize), Reports: make(chan Report, chanSize), signals: signals, } go executor.launch() return &executor}// launch starts the main loop for polling on the all the relevant channels and handling differents// messages.func (executor *Executor) launch() int { reports := make(chan Report, executor.maxWorkers) for { select { case signal := 0 { task :=
executor.go hosted with ❤ by GitHub
内核十分简单,语言优雅、现代
与其他多数的现代语言不同,Golang本身并没有提供太多的特性。事实上,严格限制特性集的范围正是Go语言的显著特征,且Go语言着意于此。Go语言的设计与Java的编程范式不同,也不支持如Python一样的多语言的编程范式。Go只是一个编程的骨架结构。除了必要的特性,其他一无所有。
看过Go语言之后,第一感觉是其不遵循任何特定的哲学或者设计指引,所有的特性都是以引用的方式解决某一个特定的问题,不会画蛇添足做多余的工作。比如,Go语言提供方法和接口但没有类;Go语言的编译器生成动态链接库,但同时保留垃圾回收器;Go语言有严格的类型但不支持泛型;Go语言有一个轻量级的运行时但不支持异常。
Go的这一设计理念的主要用意在于,在表达想法、算法或者编码的环节,开发者可以尽量少想或者不去想“在某种编程语言中处理此事的最佳方案”,让不同的开发者可以更容易理解对方的代码。不支持泛型和异常使得Go语言并不那么完美,也因此在很多场景下束手束脚 ,因此在“Go 2”版本中,官方加入了对这些必要特性的考虑。
高性能
单线程的执行效率并不足以评估一门语言的优劣,当语言本身聚焦于解决并发和并行问题的时候尤其如此。即便如此,Golang还是跑出了亮眼的成绩,仅次于一些硬核的系统编程语言,如C/C++/Rust等等,并且Golang还在不断的改进。考虑到Go是有垃圾回收机制的语言,这一成绩实际上相当的令人印象深刻,这使得Go语言的性能可以应付几乎所有的使用场景。
(Image Source: Medium)
提供现代软件开发所需要的原生工具支持
是否采用一种新的语言或工具,直接取决于开发者体验的好坏。就Go语言来说,其工具集是用户采纳的主要考量。同最小化的内核一样,Go的工具集也采用了同样的设计理念,最小化,但足够应付需要。执行所有Go语言工具,都采用 go 命令及其子命令,并且全部是以命令行的方式。
Go语言中并没有类似pip或者npm这类包管理器。但只需要下面的命令,就可以得到任何的社区包:
go get github.com/farkaskid/WebCrawler/blob/master/executor/executor.go
是的,这样就行。可以直接从Github或其他地方拉取所需要的包。所有的包都是源代码文件的形态。
对于package.json这类的包,我没有看到与 goget 等价的命令。事实上也没有。在Go语言中,无须在一个单一文件中指定所有的依赖,可以在源文件中直接使用下面的命令:
import "github.com/xlab/pocketsphinx-go/sphinx"
那么,当执行go build命令的时候,运行时会自动的运行 goget 来获取所需要的依赖。完整的源码如下:
package mainimport ( "encoding/binary" "bytes" "log" "os/exec" "github.com/xlab/pocketsphinx-go/sphinx" pulse "github.com/mesilliac/pulse-simple" // pulse-simple)var buffSize intfunc readInt16(buf []byte) (val int16) { binary.Read(bytes.NewBuffer(buf), binary.LittleEndian, &val) return}func createStream() *pulse.Stream { ss := pulse.SampleSpec{pulse.SAMPLE_S16LE, 16000, 1} buffSize = int(ss.UsecToBytes(1 * 1000000)) stream, err := pulse.Capture("pulse-simple test