标签 并行 下的文章

图解Go运行时调度器

本文翻译自《Illustrated Tales of Go Runtime Scheduler》

译注:原文章结构有些乱,笔者自行在译文中增加了一些分级标题,让结构显得更清晰一些:)。

goroutines形式的Go并发是编写现代并发软件的一种非常方便的方法,但是您的Go程序是如何高效地运行这些goroutines的呢?

在这篇文章中,我们将深入Go运行时底层,从设计角度了解Go运行时调度程序是如何实现其魔法的,并运用这些原理去解释在Go性能调试过程中产生的Go调度程序跟踪信息

所有的工程奇迹都源于需要。因此,要了解为什么需要一个Go运行时调度程序以及它是如何工作的,我们可以让时间回到操作系统兴起的那个时代,回顾操作系统的历史可以使我们深入的了解问题的根源。如果不了解问题的根源,就没有解决它的希望。这就是历史所能做的。

一. 操作系统的历史

  1. 单用户(无操作系统)。
  2. 批处理,独占系统,直到运行完成。
  3. 多道程序(译注:允许多个程序同时进入内存并运行)

多道程序的目的是使CPU和I/O重叠(overlap)。(译注:多道程序出现之前,当操作系统执行I/O操作时,CPU是空闲的;多道程序的引入实现了在一个程序占用CPU的时候,另一个程序在执行I/O操作)

那怎么实现多道程序(的CPU与I/O重叠)呢?两种方式:多道批处理系统和分时系统。

  • 多道批处理系统

    • IBM OS/MFT(具有固定数量的任务的多道程序)
    • IBM OS/MVT(具有可变数量的任务的多道程序)在这里,每个作业(job)仅获得其所需的内存量。随着job的进出,内存的划分会发生变化。
  • 分时

    • 这是一种多道程序设计,可以在作业之间快速切换。决定何时切换以及切换到哪个作业的过程就称为调度(scheduling)

当前,大多数操作系统使用分时调度程序

那么这些调度程序将用来调度什么实体(entity)呢?

  • 不同的正在执行的程序(即进程process)
  • 或作为进程子集存在使用CPU的基本单元:线程

但是在这些实体的切换是有代价的。

  • 调度成本

img{512x368}

图: 进程和线程的状态变量

因此,使用一个包含多个线程的进程的效率更高,因为进程创建既耗时又耗费资源。但是随后出现了多线程问题:C10k成为主要问题。

例如,如果将调度周期定为10ms(毫秒),并且有2个线程,则每个线程将分别获得5ms。如果您有5个线程,则每个线程将获得2ms。但是,如果有1000个线程怎么办?给每个线程一个10μs(微秒)的时间片?错,这样做很愚蠢,因为您将花费大量时间进行上下文切换,但是真正要完成的工作却进展缓慢或停滞不前。

您需要限制时间片的长度。在最后一种情况下,如果最小时间片为2ms并且有1000个线程,则调度周期需要增加到2s(10002ms)。如果有10,000个线程,则调度程序周期为20秒(100002ms)。在这个简单的示例中,如果每个线程都将分配给它的时间片用完,那么所有线程都完成一次运行需要20秒。因此,我们需要一些可以使并发成本降低而又不会造成过多开销的东西。

  • 用户层线程
    • 线程完全由运行时系统(用户级库)管理。
    • 理想情况下,快速高效:切换线程的代价不比函数调用多多少。
    • 操作系统内核对用户层线程一无所知,并像对待单线程进程(single-threaded process)一样对其进行管理。

在Go中,我们知道这样的用户层线程被称为“Goroutine”。

  • Goroutine

img{512x368}

图: goroutine vs. 线程

goroutine是由Go运行时管理的轻量级线程(lightweight thread)。要启动一个新的goroutine,只需在函数前面使用go关键字:go add(a, b)

  • Goroutine之旅
func main() {
    var wg sync.WaitGroup
    for i := 0; i <= 10; i++ {
        wg.Add(1)
        go func(i int) {
        defer wg.Done()
        fmt.Printf("loop i is - %d\n", i)
        }(i)
    }
    wg.Wait()
    fmt.Println("Hello, Welcome to Go")
}

https://play.golang.org/p/73lESLiva0A

您能猜出上面代码片段的输出吗?

loop i is - 10
loop i is - 0
loop i is - 1
loop i is - 2
loop i is - 3
loop i is - 4
loop i is - 5
loop i is - 6
loop i is - 7
loop i is - 8
loop i is - 9
Hello, Welcome to Go

如果我们看一下输出的一种组合,你可能马上就会有两个问题:

  • 11个goroutine如何并行运行?魔法?
  • goroutine以什么顺序运行?

img{512x368}

图:gopher版奇异博士

上面的这两个提问给我们带来了问题。

  • 问题概述
    • 如何将这些goroutines分配到在CPU处理器上运行的多个操作系统线程上运行?
    • 这些goroutines应该以什么顺序运行才能保证公平?

本文后续的讨论将主要围绕Go运行时调度程序从设计角度如何解决这些问题。但是,与所有问题一样,我们的讨论也需要定义一个明确的边界。否则,问题陈述可能太含糊,无法形成结论。调度程序可能针对多个目标中的一个或多个,对于我们来说,我们将自己限制在以下需求之内:

  1. 应该是并行、可扩展且公平的。
  2. 每个进程应可扩展到数百万个goroutine(C10M
  3. 内存利用率高。(RAM很便宜,但不是免费的。)
  4. 系统调用不应导致性能下降。(最大化吞吐量,最小化等待时间)

让我们开始为调度程序建模,以逐步解决这些问题。

二. Goroutine调度程序模型 (译者自行加的标题)

1. 模型概述(译者自行加的标题)

a) 一个线程执行一个Goroutine

局限性:

  • 并行和可扩展
    • 并行(是的)
    • 可扩展(不是真的)
  • 每个进程不能扩展到数百万个goroutine(C10M)。

b) M:N线程—混合线程

M个操作系统内核线程执行N个“goroutine”

img{512x368}

图: M个内核线程执行N个goroutine

实际执行代码和并行执行都需要内核线程。但是线程创建起来很昂贵,因此我们将N个goroutines映射到M个内核线程上去执行。Goroutine是Go代码,因此我们可以完全控制它。而且它在用户空间中,创建起来很便宜。

但是由于操作系统对goroutine一无所知。因此每个goroutine都有一个状态,以帮助调度器根据goroutine状态知道要运行哪个goroutine。与内核线程的状态信息相比,goroutine的状态信息很小,因此goroutine的上下文切换变得非常快。

  • 正在运行(Running) – 当前在内核线程上运行的goroutine。
  • 可运行(Runnable) – 等待内核线程来运行的goroutine。
  • 已阻塞(Blocked) – 等待某些条件的Goroutine(例如,阻塞在channel操作,系统调用,互斥锁上的goroutine)

img{512x368}

图: 2个线程同时运行2个goroutine

因此,Go运行时调度器通过将N个Goroutine多路复用到M个内核线程的方式来管理处于各种不同状态的goroutines。

2. 简单的M:N调度器

在我们简单的M:N调度器中,我们有一个全局运行队列(global run queue),某些操作将一个新的goroutine放入运行队列。M个内核线程访问调度程序从“运行队列”中获取并运行goroutine。多个线程正在尝试访问相同的内存区域,因此使用互斥锁来同步对该运行队列的访问。

img{512x368}

图: 简单的M:N调度器

但是,那些已阻塞的goroutine在哪里?

下面是goroutine可能会阻塞的情况:

  1. 在channel上发送和接收
  2. 网络I/O操作
  3. 阻塞的系统调用
  4. 使用定时器
  5. 使用互斥锁

那么我们将这些阻塞的goroutine放在哪里呢?— 将这些阻塞的goroutine放置在哪里的设计决策基本上是围绕一个基本原理进行的:

阻塞的goroutine不应阻塞底层内核线程!(避免线程上下文切换的成本)

channel操作期间阻塞的Goroutine

每个channel都有一个recvq(waitq),用于存储试图从该channel读取数据而阻塞的goroutine。

Sendq(waitq)存储试图将数据发送到channel而被阻止的goroutine 。(channel实现原理:-https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8)

img{512x368}

图: channel操作期间阻塞的Goroutine

channel本身会将channel操作后的未阻塞goroutine放入“运行”队列(run queue)。

img{512x368}

图: channel操作后未阻碍的goroutine

那系统调用呢?

首先,让我们看一下阻塞系统调用。系统调用会阻塞底层内核线程,因此我们无法在该线程上调度任何其他Goroutine。

隐含阻塞系统调用可降低并行度。

img{512x368}

图: 阻塞系统调用可降低并行度

一旦发生阻塞系统调用,我们无法再在M2线程上安排任何其他Goroutine运行,从而导致CPU浪费。由于我们有工作要做,但没法运行它。

恢复并行度的方法是在进入系统调用时,我们可以唤醒另一个线程,该线程将从运行队列中选择可运行的goroutine。

img{512x368}

图: 恢复并行度的方法

但是现在,系统调用完成后,我们有超额等待调度的goroutine。因此,我们不会立即运行从阻塞系统调用中返回的goroutine。我们会将其放入调度程序的运行队列中。

img{512x368}

图: 避免超额等待调度

因此,在程序运行时,线程数远大于cpu核数。尽管没有明确说明,线程数大于cpu核数,并且所有空闲线程也由运行时管理,以避免启动过多的线程。

https://golang.org/pkg/runtime/debug/#SetMaxThreads

初始设置为10,000个线程,如果超过10,000个线程,程序将崩溃。

非阻塞系统调用-将goroutine阻塞在Integrated runtime poller上 ,并释放线程以运行另一个goroutine。

img{512x368}

例如,在非阻塞I/O(例如HTTP调用)的情况下。由于资源尚未准备就绪,第一个syscall将不会成功,这将迫使Go使用network poller并将goroutine暂停。

部分net.Read函数的实现:

    n, err := syscall.Read(fd.Sysfd, p)
        if err != nil {
            n = 0
            if err == syscall.EAGAIN && fd.pd.pollable() {
                if err = fd.pd.waitRead(fd.isFile); err == nil {
                    continue
                }
            }
    }

一旦完成第一个系统调用并明确指出资源尚未准备就绪,goroutine将暂停,直到network poller通知它资源已准备就绪。在这种情况下,线程M将不会被阻塞。

Poller将基于操作系统使用select/kqueue/epoll/IOCP等机制来知道哪个文件描述符已准备好,一旦文件描述符准备好进行读取或写入,它将把goroutine放回到运行队列中。

还有一个Sysmon OS线程,如果超过10ms未轮询网络,它就将定期轮询网络,并将已就绪的G添加到队列中。

基本上所有goroutine都被阻塞在下面操作上:

  1. channel
  2. 互斥锁
  3. 网络IO
  4. 定时器

有某种队列,可以帮助调度这些goroutine。

现在,运行时拥有具有以下功能的调度程序。

  • 它可以处理并行执行(多线程)。
  • 处理阻塞系统调用和网络I/O。
  • 处理阻塞在用户级别(在channel上)的调用。

但这不是可伸缩的(scalable)。

img{512x368}

图: 使用Mutex同步全局运行队列

您可以通过Mutex同步全局运行队列,但最终会遇到一些问题,例如

  1. 缓存一致性保证的开销。
  2. 在创建,销毁和调度Goroutine G时进行激烈的锁竞争。

使用分布式调度程序解决可伸缩性问题。

分布式调度程序-每个线程一个运行队列

img{512x368}

图: 分布式运行队列的调度程序

这样,我们可以看到的直接好处是,每个线程的本地运行队列(local run queue)现在都没有使用mutex。仍然有一个带有mutex的全局运行队列,但仅在特殊情况下使用。它不会影响可伸缩性。

但是现在,我们有多个运行队列。

  1. 本地运行队列
  2. 全局运行队列
  3. 网络轮询器(network poller)

我们应该从哪里运行下一个goroutine?

在Go中,轮询顺序定义如下:
1. 本地运行队列
2. 全局运行队列
3. 网络轮询器
4. 工作偷窃(work stealing)

即首先检查本地运行队列,如果为空则检查全局运行队列,然后检查网络轮询器,最后进行“偷窃工作”。到目前为止,我们对1,2,3有了一些概述。让我们看一下“工作偷窃(work stealing)”。

工作偷窃

如果本地工作队列为空,请尝试“从其他队列中偷窃工作”

img{512x368}

图: 偷窃工作

当一个线程有太多工作要做而另一个线程空闲时,工作偷窃可以解决这个问题。在Go中,如果本地队列为空,工作偷窃将尝试满足以下条件之一。

  • 从全局队列中拉取工作。
  • 从网络轮询器中拉取工作
  • 从其他线程的本地队列中偷窃工作

到目前为止,Go运行时的调度器具有以下功能:

  • 它可以处理并行执行(使用多线程)。
  • 处理阻塞系统调用和网络I/O。
  • 处理用户级别(比如:在channel)的阻塞调用。
  • 可伸缩扩展(scalable)

但这仍不是最有效的。

还记得我们在阻塞系统调用中恢复并行度的方式吗?

img{512x368}

图: 系统调用操作

它暗示在一个系统调用中我们可以有多个内核线程(可以是10或1000),这可能会比cpu核数多很多。这个方案将最终在以下期间产生了恒定的开销:

  • 偷窃工作时,它必须同时扫描所有内核线程(空闲的和运行goroutine的)本地运行队列,并且大多数都将是空闲的。
  • 垃圾回收,内存分配器都会遇到相同的扫描问题。(https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed)

使用M:P:N线程克服效率问题。

M:P:N(3级调度程序)— 引入逻辑处理器P

P —表示处理器,可以将其视为在线程上运行的本地调度程序

img{512x368}

图: M:P:N模型

逻辑进程P的数量始终是固定的。(默认为当前进程可以使用的逻辑CPU数量)

然后,我们将本地运行队列(LRQ)放入固定数量的逻辑处理器(P)中(译者注:而不是每个内核线程一个本地运行队列)。

img{512x368}

图: 分布式三级运行队列调度程序

Go运行时将首先根据计算机的逻辑CPU数量(或根据请求)创建固定数量的逻辑处理器P。

每个goroutine(G)将在分配了逻辑CPU(P)的OS线程(M)上运行。

所以现在我们在以下期间没有了恒定的开销:

  • 偷窃工作 -只需扫描固定数量的逻辑处理器(P)的本地运行队列。
  • 垃圾回收,内存分配器也将获得相同的好处。

使用固定逻辑处理器(P)的系统调用呢?

Go通过将它们包装在运行时中来优化系统调用(无论是否阻塞)。

img{512x368}

图: 阻塞系统调用的包装器

阻塞SYSCALL方法封装在runtime.entersyscall(SB)和 runtime.exitsyscall(SB)之间。

从字面上看,某些逻辑在进入系统调用之前被执行,而某些逻辑在系统调用返回之后执行。进行阻塞的系统调用时,此包装器将自动将P与线程M(即将执行阻塞系统调用的线程)解绑,并允许另一个线程在其上运行。

img{512x368}

图:阻塞Syscall的M交出P

这使得Go运行时可以高效地处理阻塞的系统调用,而无需增加运行队列(译注:本地运行队列数量始终是和P数量一致的)。

一旦阻塞系统调用返回,会发生什么?

  • 运行时会尝试获取之前绑定的那个P,然后继续执行。
  • 运行时尝试在P空闲列表中获取一个P并恢复执行。
  • 运行时将goroutine放在全局队列中,并将关联的M放回M空闲列表。

自旋线程和空闲线程

当M2线程在syscall返回后变得空闲时。如何处理这个空闲的M2线程。从理论上讲,如果线程完成了所需的操作,则应将其销毁,然后再安排进程中的其他线程到CPU上执行。这就是我们通常所说的操作系统中线程的“抢占式调度”。

考虑上述syscall中的情况。如果我们销毁了M2线程,而同时M3线程即将进入syscall。此时,在OS创建新的内核线程并将其调度执行之前,我们无法处理可运行的goroutine。频繁的线程前抢占操作不仅会增加OS的负载,而且对于性能要求更高的程序几乎是不可接受的。

因此,为了适当地利用操作系统的资源并防止频繁的线程抢占给操作系统带来的负担,我们不会销毁内核线程M2,而是使其执行自旋操作并以备将来使用。尽管这看起来是在浪费一些资源。但是,与线程之间的频繁抢占以及频繁的创建和销毁操作相比,“空闲线程”要付出的代价更少。

Spinning Thread(自旋线程) — 例如,在具有一个内核线程M(1)和一个逻辑处理器(P)的Go程序中,如果正在执行的M被syscall阻塞,则运行时会请求与P数量相同的“Spinning Threads”以允许等待的可运行goroutine继续执行。因此,在此期间,内核线程的数量M将大于P的数量(自旋线程+阻塞线程)。因此,即使将runtime.GOMAXPROCS的值设置为1,程序也将处于多线程状态。

调度中的公平性如何?—公平地选择下一个要执行的goroutine

与许多其他调度程序一样,Go也具有公平性约束,并且由goroutine的实现所强加,因为Runnable goroutine应该最终得到调度并运行。

这是Go Runtime Scheduler的四个典型的公平性约束:

任何运行时间超过10ms的goroutine都被标记为可抢占(软限制)。但是,抢占仅在函数执行开始处才能完成。Go当前在函数开始处中使用了由编译器插入的协作抢占点。

  • 无限循环 – 抢占(约10毫秒的时间片)- 软限制

但请小心无限循环,因为Go的调度程序不是抢先的(直到Go 1.13)。如果循环不包含任何抢占点(例如函数调用或分配内存),则它们将阻止其他goroutine的运行。一个简单的例子是:

package main

func main() {
    go println("goroutine ran")
    for {}
}

如果你运行:

GOMAXPROCS=1 go run main.go

直到Go(1.13)才可能打印该语句。由于缺少抢占点,main Goroutine将独占处理器。

  • 本地运行队列 -抢占(〜10ms时间片)- 软限制
  • 通过每61次调度就检查一次全局运行队列,可以避免全局运行队列处于“饥饿”状态。
  • 网络轮询器饥饿 后台线程会在主工作线程未轮询的情况下偶尔会轮询网络。

Go 1.14有一个新的“非合作抢占”机制。

有了这种机制,Go运行时便有了具有所有必需功能的Scheduler。

  • 它可以处理并行执行(多线程)。
  • 处理阻塞系统调用和网络I/O。
  • 处理用户级别(在channel上)的阻塞调用。
  • 可扩展
  • 高效
  • 公平

这提供了大量的并发性,并且始终尝试实现最大的利用率和最小的延迟。

现在,我们总体上对Go运行时调度程序有了一些了解,我们如何使用它?Go为我们提供了一个跟踪工具,即调度程序跟踪(scheduler trace),目的是提供有关调度行为的信息并用来调试与goroutine调度器伸缩性相关的问题。

三. 调度器跟踪

使用GODEBUG=schedtrace=DURATION环境变量运行Go程序以启用调度程序跟踪。(DURATION是以毫秒为单位的输出周期。)

img{512x368}

图:以100ms粒度对schedtrace输出采样

有关调度器跟踪的内容,Go Wiki拥有更多信息。

参考:Dmitry Vyukov的可扩展Go Scheduler设计文档和演讲 https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit

Gopher艺术作品致谢:Ashley Mcnamara。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go正走在成为下一个企业级编程语言的轨道上

发展演化了十年的Go语言已经被证明了是云计算时代的首选编程语言,但Go的用武之地显然不局限于此。Kevin Goslar近期在Hacker Noon发表了一篇名为:《Go is on a Trajectory to Become the Next Enterprise Programming Language》的文章,阐述了Go可能成为下一个企业编程语言的理由,这里是那篇文章的中文译文,分享给大家。

img{512x368}

摘要

Go是一种专门为大规模软件开发而设计的编程语言。它提供了强大的开发体验并避免了现有编程语言存在的许多问题。这些因素使其成为最有可能在未来替代Java主导企业软件平台的候选者之一。对于那些寻求在未来几十年内构建大规模云基础架构的安全和前瞻性技术的公司和开源计划而言,我建议它们将Go视为其主要的编程语言。Go的优势如下:

  • 基于现实世界的经验
  • 专注于大型工程
  • 专注于可维护性
  • 保持简单明了
  • 使事情显式且明显
  • 很容易学习
  • 仅提供了一种做事方式
  • 支持简单地内置并发
  • 提供面向计算的语言原语
  • 使用OO – 好的部分
  • 拥有现代化的标准库
  • 强制执行标准化格式
  • 有一个非常快的编译器
  • 使交叉编译变得容易
  • 执行得非常快
  • 需要较小的内存占用
  • 部署规模小
  • 部署完全独立
  • 支持vendor依赖
  • 提供兼容性保证
  • 鼓励提供良好的文档
  • 商业支持的开源

请继续阅读有关上述每个优势点的更多详细信息。然而,在进入Go之前,你应该注意:

  • 不成熟的库
  • 即将到来的改变
  • 没有“硬实时”支持

简介

Go是Google开发的一种编程语言,在过去几年中取得了很大的成功。大部分现代云计算,网络和DevOps平台都是Go语言编写的,例如:DockerKubernetesTerraformETCDistio等。许多公司也将它用于通用软件开发。Go所具备的功能让这些项目吸引了大量用户,而Go的易用性也使得这些项目有了很多的贡献者。

Go的优势来自于简单和经过验证的想法的结合,同时避免了其他语言中出现的许多问题。这篇博客文章概述了Go背后的一些设计原则和工程智慧,并展示它们是如何结合在一起的 – 它们使Go成为下一代大型软件开发平台的优秀候选者。许多编程语言在个别领域都比较强大,但是在将所有领域都结合起来时,没有其他语言能够如此一致地“得分”,特别是在大型软件工程方面。

基于现实世界的经验

Go是由经验丰富的软件行业资深人士创建的,他们长期以来一直感受到现有语言的缺点带来的痛苦。几十年前,Rob PikeKen Thompson在Unix,C和Unicode的发明中发挥了重要作用。在实现了用于JavaScript和Java的V8和HotSpot虚拟机之后,Robert Griesemer在编译器和垃圾收集方面拥有着数十年的经验。在太多次的不得不等待他们的谷歌规模的C++/Java代码库的编译过程的推动下,他们开始着手创建一门新的编程语言,这门语言中凝聚了他们通过编写半个世纪代码过程中所学到的一切。

专注于大型工程

几乎任何编程语言都可以成功构建小型工程项目。当成千上万的开发人员在数十年的持续时间压力下在包含数千万行代码的大量代码库上进行协作时,真正痛苦的问题就会发生。这会导致以下问题:

  • 超长的编译时长会中断开发过程
  • 代码库由几个人/团队/部门/公司拥有,混合了不同的编程风格
  • 该公司雇佣了数千名工程师,架构师,测试人员,Ops专家,审计员,实习生等,他们需要了解代码库,但需要具有广泛的编码经验
  • 依赖于许多外部库或运行时,其中一些不再以其最初的形式存在
  • 每行代码在代码库的生命周期内平均被重写了10次,留下了疤痕,瑕疵和技术偏移
  • 文档不完整

Go专注于减轻这些大规模的工程难题,有时是以使小型工程变得更加繁琐为代价,例如在这里和那里需要一些额外的代码。

专注于可维护性

Go强调尽可能多地将工作转交到自动代码维护工具中。Go工具链提供了最常用的功能,如格式化代码和自动package导入、查找符号的定义和用法、简单的重构以及代码味道的识别。由于标准化的代码格式化和单一的惯用方式,机器生成的代码更改看起来非常接近Go中人为生成的更改。并而使用类似的模式,使得人和机器的协作更加无缝。

保持简单直接

初级程序员为简单问题创建简单的解决方案。高级程序员为复杂问题创建复杂的解决方案。伟大的程序员找到复杂问题的简单解决方案。-  查尔斯康奈尔

很多人都对Go不包含他们喜欢的其他语言概念感到惊讶。Go确实是一种非常小而简单的语言,只包含最少的正交和经过验证的概念。这鼓励开发人员以最少的认知开销编写最简单的代码,以便许多其他人可以理解并使用它。

使事情显式而明显

良好的代码是显而易见的,避免聪明,模糊的语言功能,扭曲的控制流和间接性。

许多语言都致力于使编写代码变得高效。然而,在其生命周期中,人们将花费大约(100倍)的时间阅读代码,而不是首先编写所需的代码。例如,审查,理解,调试,更改,重构或重用它。在查看代码时,通常只能看到并理解它的一小部分,通常没有对整个代码库的完整理解。为了解释这一点,Go将一切都显式化了。

一个例子是错误处理。让异常在各个点中断代码并使沿着调用链处理可能会更容易。Go需要手动处理或返回每个错误。这使得它可以准确地显示代码可以被中断的位置以及如何处理或包装错误。总的来说,这使得错误处理更容易编写,但更容易理解。

简单易学

Go非常小而且简单,可以在短短几天内研究整个语言及其基本概念。根据我们的经验,经过不超过一周的培训(与其他语言的以月为单位相比),初学者可以理解Go专家编写的代码,并为此做出贡献。为了方便大量人群,Go网站提供了所需的所有教程和深入的文章。这些教程在浏览器中运行,允许人们在将Go安装到本地计算机上之前学习和使用Go。

一种做事方式

Go语言通过个人自我表达赋予团队合作能力。

在Go(和Python)中,所有语言特征都是正交的并且彼此互补,通常做某事只有一种方法。如果您要求10位Python或Go程序员解决问题,您将获得10个相对类似的解决方案。不同的程序员在彼此的代码库中感觉更有家的感觉。在查看其他人的代码时,每分钟的WTF更少,而且人们的工作更好地融合在一起,从而形成一个人人都为之骄傲并且喜欢工作的一致性。这避免了大规模的工程问题,例如:

  • 开发人员将良好的工作代码视为“混乱”,并要求在他们可以使用之前重写它,因为他们不会像原作者那样思考。
  • 不同的团队成员在该语言的不同子集中编写相同代码库的部分内容。

img{512x368}
来源:https://www.osnews.com/story/19266/wtfsm

简单,内置并发

Go专为现代多核硬件而设计。

目前使用的大多数编程语言(Java,JavaScript,Python,Ruby,C,C ++)都是在20世纪80年代到2000年代设计的,当时大多数CPU只有一个计算核心。这就是为什么它们本质上是单线程的,并将并行化视为事后增加的边缘情况,通过诸如线程和同步点之类的附加组件实现,这些附加组件既麻烦又难以正确使用。第三方库提供了更简单的并发形式,如Actor模型,但总有多个选项可用,导致语言生态系统碎片化。今天的硬件拥有越来越多的计算内核,软件必须并行化才能在其上高效运行。Go是在多核CPU时代编写的,并且在语言中内置了简单,高级的CSP风格的并发特性。

面向计算的语言原语

在基础层面上,计算机系统接收数据,处理它(通常经过几个步骤),并输出结果数据。例如,Web服务器从客户端接收HTTP请求,并将其转换为一系列数据库或后端调用。一旦这些调用返回,它就会将接收到的数据转换为HTML或JSON并将其输出给调用者。Go的内置语言原语直接支持这种范例:

  • 结构体代表数据
  • reader和writer代表流式IO
  • 函数处理数据
  • goroutines提供(几乎无限制的)并发
  • 通道用于管理并发处理步骤之间的数据

由于所有计算原语都是由语言以直接的形式提供的,因此Go源代码可以更直接地表达服务器执行的操作。

OO – 好的部分

img{512x368}
在基类中改变某些东西的副作用

面向对象非常有用。这几十年OO的应用是富有成效的,并且让我们了解它的哪些部分比其他部分可以更好地扩展。基于这些认知,Go采用面向对象的新方法。它保留了封装和消息传递等优点。Go避免了继承,因为它现在被认为是有害的,Go为组合提供头等的支持

现代标准库

许多当前使用的编程语言(Java,JavaScript,Python,Ruby)是在互联网成为当今无处不在的计算平台之前设计的。因此,这些语言的标准库仅为未针对现代互联网优化的网络提供相对通用的支持。Go是十年前创建的,当时互联网已经全面展开。Go的标准库允许在没有第三方库的情况下创建更复杂的网络服务。这可以防止使用第三方库的常见问题:

  • 碎片化:实现相同功能的总有多种选择
  • 膨胀:库通常实现的不仅仅是它们的用途
  • 依赖地狱:库通常依赖于特定版本的其他库
  • 质量未知:第三方代码可能具有可疑的质量和安全性
  • 未知支持:第三方库的开发可以随时停止
  • 意外更改:第三方库通常不像标准库那样进行严格的版本管理

Russ Cox的更多背景信息。

标准化格式

Gofmt的风格是没有人喜欢的,但gofmt是每个人的最爱。 – Rob Pike

Gofmt是一种以标准化方式格式化Go代码的程序。它不是最漂亮的格式化方式,而是最简单,最不讨厌的方式。标准化的源代码格式化具有惊人的积极影响:

  • 重点讨论重要主题:它消除了围绕标签与空格,缩进深度,每行长度,空行,花括号放置等的一系列无意义的争论
  • 开发人员在彼此的代码库中感到宾至如归,因为其他代码看起来很像他们编写的代码。每个人都喜欢自由地按照自己喜欢的方式格式化代码,但如果其他人冒昧地按照他们自己喜欢的方式格式化>代码,那么每个人都讨厌它。
  • 自动代码更改不会弄乱手写代码的格式,例如通过引入意外的空白更改。

许多其他语言社区现在正在开发gofmt等价物。当构建为第三方解决方案时,通常会有几种竞争格式标准。例如,JavaScript世界提供PrettierStandardJS。可以一起使用其中之一或两者。许多JS项目都没有采用它们,因为这是一个额外的决定。Go的格式化程序内置于该语言的标准工具链中,因此只有一个标准,每个人都在使用它。

快速编译

img{512x368}
来源:https://xkcd.com/303

大型代码库的长编译时间是引发Go语言起源的一个微小的原因。Google主要使用C++和Java,与Haskell,Scala或Rust等更复杂的语言相比,它可以相对快速地编译。尽管如此,当编译大型代码库时,即使是少量的慢速也会把人激怒,编译工作流中断导致编译延迟。Go是从头开始设计的,以使编译更有效,因此编译器速度非常快,几乎没有编译延迟。这为Go开发人员提供了类似于脚本语言的即时反馈,并具有静态类型检查的额外好处。

交叉编译

由于语言运行时非常简单,因此它已被移植到许多平台,如macOS,Linux,Windows,BSD,ARM等。Go可以开箱即用于编译所有这些平台的二进制文件。这使得我们可以轻松地从一台机器来进行部署。

快速执行

Go有着接近C的速度。与JITed(即时编译)语言(Java,JavaScript,Python等)不同,Go二进制文件不需要启动或预热时间,因为它们作为已编译和完全优化的本机代码提供。Go垃圾收集器仅以微秒的指令引入可忽略的暂停。在其快速的单核性能上面,Go使得利用所有的CPU内核更容易

小内存占用

像JVM,Python或Node这样的运行时不仅仅在运行时加载程序代码。它们还会加载大型且高度复杂的基础架构,以便在每次运行时编译和优化程序。这使得它们的启动时间变慢并导致它们使用大量(数百MB)的RAM。Go进程的开销较小,因为它们已经完全编译和优化,只需要运行。Go还以非常节省内存的方式存储数据。这在内存有限且昂贵的云环境中以及在开发期间非常重要,在开发期间我们希望在单个机器上快速启动整个堆栈,同时为其他软件留下内存。

小部署规模

Go二进制文件的大小非常简洁。Go应用程序的Docker镜像通常比用Java或Node编写的等效文件小10倍,因为它不需要包含编译器,JIT,并且需要更少的运行时基础结构。这在部署大型应用程序时很重要。想象一下,将一个简单的应用程序部署到100个生产服务器上 使用Node / JVM时,我们的docker仓库必须提供100个docker镜像@ 200 MB = 20 GB(总共)。这需要镜像仓库耗费一些时间来服务。想象一下,我们希望每天部署100次。使用Go服务时,Docker镜像仓库只需提供100个Docker镜像@ 20 MB = 2 GB。可以更快,更频繁地部署大型Go应用程序,从而允许重要更新更快地实现生产。

自包含部署

Go应用程序部署为包含所有依赖项的单个可执行文件。不需要安装特定版本的JVM,Node或Python运行时。不必将库下载到生产服务器上。不需要对运行Go二进制文件的机器进行任何更改。甚至不需要将Go二进制文件包装到Docker中来共享它们。您只需将Go二进制文件拖放到服务器上,无论该服务器上运行的是什么,它都会在那里运行。上述描述的唯一例外是使用net和os/user包时的动态链接glibc库时。

vendor依赖关系

Go故意避免使用第三方库的中央存储库。Go应用程序直接链接到相应的Git存储库,并将所有相关代码下载(vendor保存)到他们自己的代码库中。这有很多好处:

  • 我们可以在使用之前查看,分析和测试第三方代码。此代码与我们自己的代码一样,是我们应用程序的一部分,应符合相同的质量,安全性和可靠性标准。
  • 无需永久访问存储依赖项的各个位置。可以一次性的从任何地方(包括私人Git仓库)获取您的第三方库,并永久拥有它们。
  • 在checkout后编译代码库不需要进一步下载依赖项。
  • 如果互联网上某处的代码存储库突然提供不同的代码,也不会造成surprises。
  • 即使软件包存储库服务性能变慢或托管软件包不再存在,部署也不会中断。

兼容性保证

Go团队承诺,现有的程序将继续适用于新版本语言。这使得即使是大型项目也可以轻松升级到更新编译器的版本,并从新版本带来的许多性能和安全性改进中受益。同时,由于Go二进制文件包含了他们需要的所有依赖项,因此可以在同一服务器计算机上并行运行使用不同版本的Go编译器编译的二进制文件,而无需进行复杂的设置多个版本的运行时或虚拟化。

文档

在大型工程中,文档对于使软件易于访问和维护非常重要。与其他功能类似,Go中的文档简单实用:

  • 它嵌入在源代码中,因此两者可以同时维护。
  • 它不需要特殊的语法 – 文档只是普通的源代码注释。
  • 可运行的单元测试通常是最好的文档形式,所以Go允许你将它们嵌入到文档中
  • 所有文档实用程序都内置在工具链中,因此每个人都使用它们。
  • Go linter需要导出元素的文档,以防止“文档债务”的积累。

商业支持的开源

当商业实体在公开场合发展时,一些最流行和最全面设计的软件就会发生。这种设置结合了商业软件开发的优势 – 一致性和优化,使系统健壮,可靠,高效 – 具有开放式开发的优势,如来自许多行业的广泛支持,来自多个大型实体和许多用户的支持,以及长期支持,即使商业支持停止。Go就是这样开发的。

缺点

当然,Go并不完美,每种技术选择总是有利有弊。在进入Go之前,这里有一小部分需要考虑的方面。

未成熟

虽然Go的标准库在支持HTTP/2服务器推送等许多新概念方面处于行业领先地位,但与JVM生态系统中存在的相比,用于外部API的第三方Go库可能还不那么成熟。

即将到来的变化

Go团队知道几乎不可能改变现有的语言元素,因此只有在完全开发后才会添加新功能。在经历了10年稳定的故意阶段后,Go团队正在考虑对语言进行一系列更大的改进,作为Go 2.0之旅的一部分。

没有硬实时

虽然Go的垃圾收集器只引入了非常短的中断,但支持硬实时需要没有垃圾收集的技术,例如Rust。

结论

这篇博客文章给出了一些明智的背景知识,但往往没有那么明显的选择进入Go的设计,以及当他们的代码库和团队成数量级增长时,他们将如何从许多痛苦中拯救大型工程项目。总的来说,他们将Go定位为寻求Java之外的现代编程语言的大型开发项目的绝佳选择。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats