分类 技术 下的文章

更新:如果你是从一篇题为 《糟糕的 Go 语言》 的汇编文章看到这篇博文的话,那么我想表明的是,我很惭愧被列在这样的名单上。Go 绝对是我使用过的最不糟糕的的编程语言。在我写作本文时,我是想遏制我所看到的一种趋势,那就是过度使用 Go 的一些较复杂的部分。我仍然认为 通道 Channel 可以更好,但是总体而言,Go 很棒。这就像你最喜欢的工具箱中有 这个工具;它可以有用途(甚至还可能有更多的用途),它仍然可以成为你最喜欢的工具箱!

更新 2:如果我没有指出这项对真实问题的优秀调查,那我将是失职的:《理解 Go 中的实际并发错误》。这项调查的一个重要发现是...Go 通道会导致很多错误。

从 2010 年中后期开始,我就断断续续地在使用 Google 的 Go 编程语言,自 2012 年 1 月开始(在 Go 1.0 之前!),我就用 Go 为 Space Monkey 编写了合规的产品代码。我对 Go 的最初体验可以追溯到我在研究 Hoare 的 通信顺序进程 并发模型和 Matt MightUCombinator 研究组 下的 π-演算 时,作为我(现在已重定向)博士工作的一部分,以更好地支持多核开发。Go 就是在那时发布的(多么巧合啊!),我当即就开始学习尝试了。

它很快就成为了 Space Monkey 开发的核心部分。目前,我们在 Space Monkey 的生产系统有超过 42.5 万行的纯 Go 代码( 包括我们所有的 vendored 库中的代码量,这将使它接近 150 万行),所以也并不是你见过的最多的 Go 代码,但是对于相对年轻的语言,我们是重度用户。我们之前 写了我们的 Go 使用情况。也开源了一些使用率很高的库;许多人似乎是我们的 OpenSSL 绑定(比 crypto/tls 更快,但请保持 openssl 本身是最新的!)、我们的 错误处理库日志库度量标准收集库/zipkin 客户端 的粉丝。我们使用 Go、我们热爱 Go、我们认为它是目前为止我们使用过的最不糟糕的、符合我们需求的编程语言。

尽管我也不认为我能说服自己不要提及我的广泛避免使用 goroutine-local-storage 库 (尽管它是一个你不应该使用的魔改技巧,但它是一个漂亮的魔改),希望我的其他经历足以证明我在解释我故意煽动性的帖子标题之前知道我在说什么。

等等,什么?

如果你在大街上问一个有名的程序员,Go 有什么特别之处? 她很可能会告诉你 Go 最出名的是 通道 Channels 和 goroutine。 Go 的理论基础很大程度上是建立在 Hoare 的 CSP( 通信顺序进程 Communicating Sequential Processes )模型上的,该模型本身令人着迷且有趣,我坚信,到目前为止,它产生的收益远远超过了我们的预期。

CSP(和 π-演算)都使用通信作为核心同步原语,因此 Go 会有通道是有道理的。Rob Pike 对 CSP 着迷(有充分的理由)相当深 已经有一段时间了。(当时现在)。

但是从务实的角度来看(也是 Go 引以为豪的),Go 把通道搞错了。在这一点上,通道的实现在我的书中几乎是一个坚实的反模式。为什么这么说呢?亲爱的读者,让我细数其中的方法。

你可能最终不会只使用通道

Hoare 的 “通信顺序进程” 是一种计算模型,实际上,唯一的同步原语是在通道上发送或接收的。一旦使用 互斥量 mutex 信号量 semaphore 条件变量 condition variable 、bam,你就不再处于纯 CSP 领域。 Go 程序员经常通过高呼 “通过交流共享内存” 的 缓存的思想 来宣扬这种模式和哲学。

那么,让我们尝试在 Go 中仅使用 CSP 编写一个小程序!让我们成为高分接收者。我们要做的就是跟踪我们看到的最大的高分值。如此而已。

首先,我们将创建一个 Game 结构体。

type Game struct {
  bestScore int
  scores    chan int
}

bestScore 不会受到 互斥量 mutex 的保护!这很好,因为我们只需要一个 goroutine 来管理其状态并通过通道来接收新的分值即可。

func (g *Game) run() {
  for score := range g.scores {
    if g.bestScore < score {
      g.bestScore = score
    }
  }
}

好的,现在我们将创建一个有用的构造函数来开始 Game

func NewGame() (g *Game) {
  g = &Game{
    bestScore: 0,
    scores:    make(chan int),
  }
  go g.run()
  return g
}

接下来,假设有人给了我们一个可以返回分数的 Player。它也可能会返回错误,因为可能传入的 TCP 流可能会死掉或发生某些故障,或者玩家退出。

type Player interface {
  NextScore() (score int, err error)
}

为了处理 Player,我们假设所有错误都是致命的,并将获得的比分向下传递到通道。

func (g *Game) HandlePlayer(p Player) error {
  for {
    score, err := p.NextScore()
    if err != nil {
      return err
    }
    g.scores <- score
  }
}

好极了!现在我们有了一个 Game 类型,可以以线程安全的方式跟踪 Player 获得的最高分数。

你圆满完成了自己的开发工作,并开始拥有客户。你将这个游戏服务器公开,就取得了令人难以置信的成功!你的游戏服务器上也许正在创建许多游戏。

很快,你发现人们有时会离开你的游戏。许多游戏不再有任何玩家在玩,但没有任何东西可以阻止游戏运行的循环。死掉的 (*Game).run goroutines 让你不知所措。

挑战: 在无需互斥量或 panics 的情况下修复上面的 goroutine 泄漏。实际上,可以滚动到上面的代码,并想出一个仅使用通道来解决此问题的方案。

我等着。

就其价值而言,它完全可以只通过通道来完成,但是请观察以下解决方案的简单性,它甚至没有这个问题:

type Game struct {
  mtx sync.Mutex
  bestScore int
}

func NewGame() *Game {
  return &Game{}
}

func (g *Game) HandlePlayer(p Player) error {
  for {
    score, err := p.NextScore()
    if err != nil {
      return err
    }
    g.mtx.Lock()
    if g.bestScore < score {
      g.bestScore = score
    }
    g.mtx.Unlock()
  }
}

你想选择哪一个?不要被欺骗了,以为通道的解决方案可以使它在更复杂的情况下更具可读性和可理解性。 拆解 Teardown 是非常困难的。这种拆解若用 互斥量 mutex 来做那只是小菜一碟,但最困难的是只使用 Go 专用通道来解决。另外,如果有人回复说发送通道的通道更容易推理,我马上就是感到头疼。

重要的是,这个特殊的情况可能真的 很容易 解决,而通道有一些运行时的帮助,而 Go 没有提供!不幸的是,就目前的情况来看,与 Go 的 CSP 版本相比,使用传统的 同步原语 synchronization primitives 可以更好地解决很多问题,这是令人惊讶的。稍后,我们将讨论 Go 可以做些什么来简化此案例。

练习: 还在怀疑? 试着让上面两种解决方案(只使用通道与只使用互斥量channel-only vs mutex-only)在一旦 bestScore 大于或等于 100 时,就停止向 Players 索要分数。继续打开你的文本编辑器。这是一个很小的玩具问题。

这里的总结是,如果你想做任何实际的事情,除了通道之外,你还会使用传统的同步原语。

通道比你自己实现要慢一些

Go 如此重视 CSP 理论,我认为其中一点就是,运行时应该可以通过通道做一些杀手级的调度优化。也许通道并不总是最直接的原语,但肯定是高效且快速的,对吧?

正如 Dustin HiattTyler Treat’s post about Go 上指出的那样,

在幕后,通道使用锁来序列化访问并提供线程安全性。 因此,通过使用通道同步对内存的访问,你实际上就是在使用锁。 被包装在线程安全队列中的锁。 那么,与仅仅使用标准库 sync 包中的互斥量相比,Go 的花式锁又如何呢? 以下数字是通过使用 Go 的内置基准测试功能,对它们的单个集合连续调用 Put 得出的。
> BenchmarkSimpleSet-8 3000000 391 ns/op
> BenchmarkSimpleChannelSet-8 1000000 1699 ns/o
>

无缓冲通道的情况与此类似,甚至是在争用而不是串行运行的情况下执行相同的测试。

也许 Go 调度器会有所改进,但与此同时,良好的旧互斥量和条件变量是非常好、高效且快速。如果你想要提高性能,请使用久经考验的方法。

通道与其他并发原语组合不佳

好的,希望我已经说服了你,有时候,你至少还会与除了通道之外的原语进行交互。标准库似乎显然更喜欢传统的同步原语而不是通道。

你猜怎么着,正确地将通道与互斥量和条件变量一起使用,其实是有一定的挑战性的。

关于通道的一个有趣的事情是,通道发送是同步的,这在 CSP 中是有很大意义的。通道发送和通道接收的目的是为了成为同步屏蔽,发送和接收应该发生在同一个虚拟时间。如果你是在执行良好的 CSP 领域,那就太好了。

实事求是地说,Go 通道也有多种缓冲方式。你可以分配一个固定的空间来考虑可能的缓冲,以便发送和接收是不同的事件,但缓冲区大小是有上限的。Go 并没有提供一种方法来让你拥有任意大小的缓冲区 —— 你必须提前分配缓冲区大小。 这很好,我在邮件列表上看到有人在争论,因为无论如何内存都是有限的

What。

这是个糟糕的答案。有各种各样的理由来使用一个任意缓冲的通道。如果我们事先知道所有的事情,为什么还要使用 malloc 呢?

没有任意缓冲的通道意味着在 任何 通道上的幼稚发送可能会随时阻塞。你想在一个通道上发送,并在互斥下更新其他一些记账吗?小心!你的通道发送可能被阻塞!

// ...
s.mtx.Lock()
// ...
s.ch <- val // might block!
s.mtx.Unlock()
// ...

这是哲学家晚餐大战的秘诀。如果你使用了锁,则应该迅速更新状态并释放它,并且尽可能不要在锁下做任何阻塞。

有一种方法可以在 Go 中的通道上进行非阻塞发送,但这不是默认行为。假设我们有一个通道 ch := make(chan int),我们希望在其上无阻塞地发送值 1。以下是在不阻塞的情况下你必须要做的最小量的输入:

select {
case ch <- 1: // it sent
default:      // it didn't
}

对于刚入门的 Go程序员来说,这并不是自然而然就能想到的事情。

综上所述,因为通道上的很多操作都会阻塞,所以需要对哲学家及其就餐仔细推理,才能在互斥量的保护下,成功地将通道操作与之并列使用,而不会造成死锁。

严格来说,回调更强大,不需要不必要的 goroutines

每当 API 使用通道时,或者每当我指出通道使某些事情变得困难时,总会有人会指出我应该启动一个 goroutine 来读取该通道,并在读取该通道时进行所需的任何转换或修复。

呃,不。如果我的代码位于热路径中怎么办?需要通道的实例很少,如果你的 API 可以设计为使用 互斥量 mutexes 信号量 semaphores 回调 callbacks ,而不使用额外的 goroutine (因为所有事件边缘都是由 API 事件触发的),那么使用通道会迫使我在资源使用中添加另一个内存分配堆栈。是的,goroutine 比线程轻得多,但更轻量并不意味着是最轻量。

正如我以前 在一篇关于使用通道的文章的评论中争论过的(呵呵,互联网),如果你使用回调而不是通道,你的 API 总是 可以更通用,总是 更灵活,而且占用的资源也会大大减少。“总是” 是一个可怕的词,但我在这里是认真的。有证据级的东西在进行。

如果有人向你提供了一个基于回调的 API,而你需要一个通道,你可以提供一个回调,在通道上发送,开销不大,灵活性十足。

另一方面,如果有人提供了一个基于通道的 API 给你,而你需要一个回调,你必须启动一个 goroutine 来读取通道,并且 你必须希望当你完成读取时,没有人试图在通道上发送更多的东西,这样你就会导致阻塞的 goroutine 泄漏。

对于一个超级简单的实际例子,请查看 context 接口(顺便说一下,它是一个非常有用的包,你应该用它来代替 goroutine 本地存储)。

type Context interface {
  ...
  // Done returns a channel that closes when this work unit should be canceled.
  // Done 返回一个通道,该通道在应该取消该工作单元时关闭。
  Done() <-chan struct{}

  // Err returns a non-nil error when the Done channel is closed
  // 当 Done 通道关闭时,Err 返回一个非 nil 错误
  Err() error
  ...
}

想象一下,你要做的只是在 Done() 通道触发时记录相应的错误。你该怎么办?如果你没有在通道中选择的好地方,则必须启动 goroutine 进行处理:

go func() {
  <-ctx.Done()
  logger.Errorf("canceled: %v", ctx.Err())
}()

如果 ctx 在不关闭返回 Done() 通道的情况下被垃圾回收怎么办?哎呀!这正是一个 goroutine 泄露!

现在假设我们更改了 Done 的签名:

// Done calls cb when this work unit should be canceled.
Done(cb func())

首先,现在日志记录非常容易。看看:ctx.Done(func() { log.Errorf ("canceled:%v", ctx.Err()) })。但是假设你确实需要某些选择行为。你可以这样调用它:

ch := make(chan struct{})
ctx.Done(func() { close(ch) })

瞧!通过使用回调,不会失去表现力。 ch 的工作方式类似于用于返回的通道 Done(),在日志记录的情况下,我们不需要启动整个新堆栈。我必须保留堆栈跟踪信息(如果我们的日志包倾向于使用它们);我必须避免将其他堆栈分配和另一个 goroutine 分配给调度程序。

下次你使用通道时,问问你自己,如果你用互斥量和条件变量代替,是否可以消除一些 goroutine ? 如果答案是肯定的,那么修改这些代码将更加有效。而且,如果你试图使用通道只是为了在集合中使用 range 关键字,那么我将不得不请你放下键盘,或者只是回去编写 Python 书籍。

more like Zooey De-channel, amirite

通道 API 不一致,只是 cray-cray

在通道已关闭的情况下,执行关闭或发送消息将会引发 panics!为什么呢? 如果想要关闭通道,你需要在外部同步它的关闭状态(使用互斥量等,这些互斥量的组合不是很好!),这样其他写入者才不会写入或关闭已关闭的通道,或者只是向前冲,关闭或写入已关闭的通道,并期望你必须恢复所有引发的 panics。

这是多么怪异的行为。 Go 中几乎所有其他操作都有避免 panic 的方法(例如,类型断言具有 , ok = 模式),但是对于通道,你只能自己动手处理它。

好吧,所以当发送失败时,通道会出现 panic。我想这是有一定道理的。但是,与几乎所有其他带有 nil 值的东西不同,发送到 nil 通道不会引发 panic。相反,它将永远阻塞!这很违反直觉。这可能是有用的行为,就像在你的除草器上附加一个开罐器,可能有用(在 Skymall 可以找到)一样,但这肯定是意想不到的。与 nil 映射(执行隐式指针解除引用),nil 接口(隐式指针解除引用),未经检查的类型断言以及其他所有类型交互不同,nil 通道表现出实际的通道行为,就好像为该操作实例化了一个全新的通道一样。

接收的情况稍微好一点。在已关闭的通道上执行接收会发生什么?好吧,那会是有效操作——你将得到一个零值。好吧,我想这是有道理的。奖励!接收允许你在收到值时进行 , ok = 样式的检查,以确定通道是否打开。谢天谢地,我们在这里得到了 , ok =

但是,如果你从 nil 渠道接收会发生什么呢? 也是永远阻塞! 耶!不要试图利用这样一个事实:如果你关闭了通道,那么你的通道是 nil!

通道有什么好处?

当然,通道对于某些事情是有好处的(毕竟它们是一个通用容器),有些事情你只能用它们来做(比如 select)。

它们是另一种特殊情况下的通用数据结构

Go 程序员已经习惯于对泛型的争论,以至于我一提起这个词就能感觉到 PTSD(创伤后应激障碍)的到来。我不是来谈论这件事的,所以擦擦额头上的汗,让我们继续前进吧。

无论你对泛型的看法是什么,Go 的映射、切片和通道都是支持泛型元素类型的数据结构,因为它们已经被特殊封装到语言中了。

在一种不允许你编写自己的泛型容器的语言中,任何允许你更好地管理事物集合的东西都是有价值的。在这里,通道是一个支持任意值类型的线程安全数据结构。

所以这很有用!我想这可以省去一些陈词滥调。

我很难把这算作是通道的胜利。

Select

使用通道可以做的主要事情是 select 语句。在这里,你可以等待固定数量的事件输入。它有点像 epoll,但你必须预先知道要等待多少个套接字。

这是真正有用的语言功能。如果不是 select,通道将被彻底清洗。但是我的天呐,让我告诉你,第一次决定可能需要在多个事物中选择,但是你不知道有多少项,因此必须使用 reflect.Select

通道如何才能更好?

很难说 Go 语言团队可以为 Go 2.0 做的最具战术意义的事情是什么(Go 1.0 兼容性保证很好,但是很费劲),但这并不能阻止我提出一些建议。

在条件变量上的 Select !

我们可以不需要通道!这是我提议我们摆脱一些“ 圣牛 sacred cows ”(LCTT 译注:神圣不可质疑的事物)的地方,但是让我问你,如果你可以选择任何自定义同步原语,那会有多棒?(答:太棒了。)如果有的话,我们根本就不需要通道了。

GC 可以帮助我们吗?

在第一个示例中,如果我们能够使用定向类型的通道垃圾回收(GC)来帮助我们进行清理,我们就可以轻松地解决通道的高分服务器清理问题。

如你所知,Go 具有定向类型的通道。 你可以使用仅支持读取的通道类型(<-chan)和仅支持写入的通道类型(chan<-)。 这太棒了!

Go 也有垃圾回收功能。 很明显,某些类型的记账方式太繁琐了,我们不应该让程序员去处理它们。 我们清理未使用的内存! 垃圾回收非常有用且整洁。

那么,为什么不帮助清理未使用或死锁的通道读取呢? 与其让 make(chan Whatever) 返回一个双向通道,不如让它返回两个单向通道(chanReader, chanWriter:= make(chan Type))。

让我们重新考虑一下最初的示例:

type Game struct {
  bestScore int
  scores    chan<- int
}

func run(bestScore *int, scores <-chan int) {
  // 我们不会直接保留对游戏的引用,因为这样我们就会保留着通道的发送端。
  for score := range scores {
    if *bestScore < score {
      *bestScore = score
    }
  }
}

func NewGame() (g *Game) {
  // 这种 make(chan) 返回风格是一个建议
  scoreReader, scoreWriter := make(chan int)
  g = &Game{
    bestScore: 0,
    scores:    scoreWriter,
  }
  go run(&g.bestScore, scoreReader)
  return g
}

func (g *Game) HandlePlayer(p Player) error {
  for {
    score, err := p.NextScore()
    if err != nil {
      return err
    }
    g.scores <- score
  }
}

如果垃圾回收关闭了一个通道,而我们可以证明它永远不会有更多的值,那么这个解决方案是完全可行的。是的,是的,run 中的评论暗示着有一把相当大的枪瞄准了你的脚,但至少现在这个问题可以很容易地解决了,而以前确实不是这样。此外,一个聪明的编译器可能会做出适当的证明,以减少这种脚枪造成的损害。

其他小问题

  • Dup 通道吗? —— 如果我们可以在通道上使用等效于 dup 的系统调用,那么我们也可以很容易地解决多生产者问题。 每个生产者可以关闭自己的 dup 版通道,而不会破坏其他生产者。
  • 修复通道 API! —— 关闭不是幂等的吗? 在已关闭的通道上发送信息引起的 panics 没有办法避免吗? 啊!
  • 任意缓冲的通道 —— 如果我们可以创建没有固定的缓冲区大小限制的缓冲通道,那么我们可以创建非阻塞的通道。

那我们该怎么向大家介绍 Go 呢?

如果你还没有,请看看我目前最喜欢的编程文章:《你的函数是什么颜色》。虽然不是专门针对 Go,但这篇博文比我更有说服力地阐述了为什么 goroutines 是 Go 最好的特性(这也是 Go 在某些应用程序中优于 Rust 的方式之一)。

如果你还在使用这样的一种编程语言写代码,它强迫你使用类似 yield 关键字来获得高性能、并发性或事件驱动的模型,那么你就是活在过去,不管你或其他人是否知道这一点。到目前为止,Go 是我所见过的实现 M:N 线程模型(非 1:1 )的语言中最好的入门者之一,而且这种模型非常强大。

所以,跟大家说说 goroutines 吧。

如果非要我选择 Go 的另一个主要特性,那就是接口。静态类型的 鸭子模型 duck typing 使得扩展、使用你自己或他人的项目变得如此有趣而令人惊奇,这也许值得我改天再写一组完全不同的文章来介绍它。

所以…

我一直看到人们争先恐后冲进 Go,渴望充分利用通道来发挥其全部潜力。这是我对你的建议。

够了!

当你在编写 API 和接口时,尽管“绝不”的建议可能很糟糕,但我非常肯定,通道从来没有什么时候好过,我用过的每一个使用通道的 Go API,最后都不得不与之抗争。我从来没有想过“哦 太好了,这里是一个通道;”它总是被一些变体取代,这是什么新鲜的地狱?

所以,请在适当的地方,并且只在适当的地方使用通道。

在我使用的所有 Go 代码中,我可以用一只手数出有多少次通道真的是最好的选择。有时候是这样的。那很好!那就用它们吧。但除此之外,就不要再使用了。

特别感谢我的校对读者 Jeff Wendling、Andrew HardingGeorge ShankTyler Treat 提供的宝贵反馈。

如果你想和我们一起用 Go 在 Space Monkey 项目工作,请给我打个招呼


via: https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad

作者:jtolio.com 选题:lujun9972 译者:gxlct008 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你对 Git 了解得越多,使用 Git 就会越容易。一起来回顾下年度最佳 Git 文章。

Git 是开源开发者工具箱中最基本的工具。这个强大的版本控制系统有很多复杂的功能。使用 Git 不需要了解它所有的功能,但是对 Git 了解得越多,使用 Git 就会越容易。

下面每篇文章都提供了一些奇技淫巧来提升和增强你的 Git 技能。

怎么解决 git 合并时的冲突

Brian Breniser 的这篇教程从 git merge 的定义以及解释什么是冲突开始。然后他详细解释了在合并时如果有冲突如何解决冲突。Breniser 还提了一些能学习更多关于解决冲突和其他 Git 功能的建议。

4 个不可或缺的 Git 脚本

Vince Power 分享了他最重要的 Git 脚本。这些脚本可以从 Git Extras 包中获得,该包提供了 60 多个 Git 增强脚本。Power 最爱的脚本有:在无需打开文本编辑器的情况下编辑 .git-ignoregit-ignore ;用于提供 Git 仓库的摘要的 git-info;用来处理 GitLab 的合并请求(MR)和 GitHub 的拉取请求(PR)的 git-pr;把 Git 的提交(commit)、标签(tag)和推送(push)合为一体的 git-release

完美生活:git rebase -i

在 Dave Neary 的这篇文章中可以学习使用 git rebase -i 来修改你的 Git 提交历史。Neary 从解释 Git 是如何把提交历史记录到仓库中的以及 git commitgit rebase 的区别。之后,他又解释了如何使用 git rebase -i 让 Git 仓库的提交历史变得简洁。这个命令能让你把“修复书写错误”的提交合进其它提交里,把几个相似的小提交合并成一个大的提交。

Git Cola 让使用 Git 变得简单

Seth Kenlon 演示了如何使用 Git Cola。Git 是个命令行工具,这对于有些人来说是有学习门槛的。Git Cola 提供了一个图形界面,因此不习惯用命令行的用户也可以使用 Git。在此文中,Kenlon 展示了如何安装 Git Cola,并使用 Git Cola 的图形用户界面完成了很多 Git 提交任务。

6 个在团队中使用 Git 的最佳实践

从设计上讲,Git 是个协同工具,但是关于如何协同的很多细节是由团队自行决定的。Ravi Chandran 提了一些建议,团队应该采用这些建议更高效地使用 Git。Chandran 在文中列出的 6 个最佳实践是:“使约定正式化”,“正确地合并修改”,“经常变基你的功能分支”,“在合并之前把压扁你的提交”,“使用标签”,“让软件的可执行程序打印标签”。

改变我使用 Git 工作方式的七个技巧

Rajeev Bera 分享了 7 个 Git 技巧,这些技巧能提升 Git 的用户体验。文章探讨了 Git 的自动更正、提交计数、仓库优化、备份未追踪的文件、了解 .git 目录、在另一个分支查看文件以及在 Git 下搜索。

使用 tmux 和 Git 定制化我的 Linux 终端

Moshe Zadka 展示了他是如何使用 tmux 和 Git定制化他的 Linux 终端的。Zadka 的文章是个人工作流的优秀探索。他使用 GNOME 终端,用 tmux 和一些能让他快速查看 Git 仓库状态的功能来增强终端。他只需要用一个字母就可以提交文件或把提交推送到远程仓库。

使用 Lazygit 让复杂的 Git 任务简单化

Jesse Duffield 解释了如何使用Lazygit,一个能让使用 Git 变得简单的终端界面。Lazygit 的开发者 Duffield 详细阐述了如何使用这个界面来暂存文件、以交互方式变基、进行筛选、搜索提交以及创建一个 PR。

使用子模块和子树来管理 Git 项目

子模块和子树是两种在 Git 仓库中引入嵌套的子项目的方式。在使用子模块和子树来管理 Git 项目中,Manaswini Das 解释了两个选项的工作原理和区别。

不喜欢 diff?那么试试 Meld

Ben Nuttall 展示了如何使用 Meld 代替 diff来进行对比和合并修改。Meld 是图形化的 diff,输出更容易理解。Nuttall 演示了使用 diff 和 Meld 进行对比的区别。他还解释了 Meld 是如何识别 Git 项目的,这意味着在 Git 中一个文件被提交之后,可以用 Meld 来搜索修改。

你想学习关于 Git 的什么内容?请在评论去分享你的想法。


via: https://opensource.com/article/20/12/git

作者:Joshua Allen Holm 选题:lujun9972 译者:lxbwolf 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你可以尝试以多种语言编程一个简单的游戏来开始编程之路。

 title=

当你想学习一门新的编程语言时,不妨关注一下编程语言的共同点。

  • 变量
  • 表达式
  • 语句

这些概念是大多数编程语言的基础。一旦你理解了它们,你就可以开始弄清楚其余的东西。

因为编程语言通常具有相似性,一旦你懂了一种语言,你就可以通过理解其差异来学习另一种语言的基础知识。

学习新语言的一个好方法是使用一个你可以用来练习的标准程序。这可以让你专注于语言,而不是程序的逻辑。我在这一系列文章中使用了一个“猜数字”的程序,在这个程序中,电脑会在 1 到 100 之间选一个数字让你猜。程序一直循环,直到你猜对数字为止。

这个程序锻炼了编程语言的几个概念:

  • 变量
  • 输入
  • 输出
  • 条件评估
  • 循环

这是学习一门新编程语言的很好的实践实验。

安装 Rust

你可以使用 Rustup 安装一个 Rust 工具链,或者你可以在线尝试 Rust 而不在本地安装它。

如果你在本地安装,你应该定期用 rustup update 来更新它,以保持你的工具链的新鲜,并使用 cargo update 来保持你的库的最新版本。

Rust 语言版本的猜数字

Rust 是一门赋予任何人构建可靠和高效的软件能力的语言。你可以通过编写一个 Rust 版本的“猜数字”游戏来探索 Rust。

第一步是编写一个 Cargo.toml 文件。你可以使用 cargo new 命令生成一个骨架 Cargo.toml。这几乎是开始一个 Rust 项目的最佳方式。

$ cargo new guess
$ cd guess
$ ls -1
Cargo.toml
src/

Cargo.toml 文件为你的包命名,并给它一些元数据,最重要的是,指明了它依赖于 rand crate

[package]
name = "guess"
version = "2020.11.0"
authors = ["Moshe Zadka <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "*"

Rust 中的许多东西不是由语言或标准库提供的。取而代之的是,你可以从某个外部 crate 得到它们,这些 crate 可以做许多事情。

程序逻辑在 src/main.rs 中:

use rand::Rng;
use std::io::BufRead;

fn main() {
    let mut rng = rand::thread_rng();
    let random = rng.gen_range(1..101);
    println!("Guess a number between 1 and 100");
    for line in std::io::stdin().lock().lines() {
        let parsed = line.ok().as_deref().map(str::parse::<i64>);
        if let Some(Ok(guess)) = parsed {
            match guess {
                _ if guess < random => println!("Too low"),
                _ if guess > random => println!("Too high"),
                _ => {
                    println!("That's right");
                    break;
                }
            }
        }
    }
}

代码的前两行声明你要做什么。在本例中,rand::Rng 生成一个猜测值,而 trait std::io::BufRead 使得可以从标准输入中读取。

Rust 代码的入口在 main() 函数中,所以下一步就是定义 main()

要给一个变量赋值,先放 let,再放变量的名字,后面放 = 号。这样就创建了一个不可变变量。

Rust 中大多数变量都是不可变的,但是 rng 对象必须是可变的(mut)。例如,语句 let random = 0random 变量分配一个零值。

函数的第一行创建了一个线程安全的 Rng 对象,并将其分配给变量 rng。Rust 是建立在线程和内存安全的基础上的,所以你必须在开始写代码时就考虑到这些事情。

程序的下一行读取函数 gen_range() 的结果,并将其分配给名为 random 的变量。该函数取一个最小值(包含)和一个上界(不包含)。为了也包含上界,你可以用一个等号来标记较大的数字(例如,1...=100),或者你也可以像我在代码中做的那样,将上界设置为比你的预期最大值大 1。在这种情况下,该范围是 1100,使得游戏刚好有足够的挑战性。

中央循环在 std::io::stdin() 中迭代行。由于有各种可能导致行不能读取的例外情况,Rust 要求你用一个 Result 来包裹一行。有时候可能无法将一行解析为一个整数。

这段代码使用条件模式匹配来忽略所有会导致错误的行:

        let parsed = line.ok().as_deref().map(str::parse::<i64>);
        if let Some(Ok(guess)) = parsed {
            // ...
        }

第一行创建了一个 Result<Option<i64>, ...> 对象,因为它可能在读取或解析步骤中失败。由于下一行只在 Some(Ok(guess)) 上匹配,每当一行的结果是一个不匹配的值时,它就会跳过 if 语句。这是一种强大的忽略错误的方法。

Rust 支持条件表达式和流程控制,比如循环。在“猜数字”游戏中,只要猜中的值不等于 random,Rust 就会继续循环。

if 语句的主体包含一个 Rust 的 match 语句的三向分支。虽然 match 最常用于模式匹配,但它也可以检查任意条件。在这种情况下是打印一个适当的信息,如果猜测是正确的,则中断(break)循环。

示例输出

现在你已经写好了你的 Rust 程序,你可以运行它来玩“猜数字”游戏。每次运行程序时,Rust 都会选择一个不同的随机数,所以继续猜,直到找到正确的数字。

$ cargo run
   Compiling guess v2020.11.0 (/Users/mzadka/src/guess)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/guess`
Guess a number between 1 and 100
50
Too high
25
Too high
12
Too low
18
Too high
15
Too high
13
Too low
14
That's right

典型的做法是用 cargo run 来测试程序。最终,你可能会使用 cargo build 分成两个独立的步骤构建一个可执行文件并运行它

学习 Rust

这个“猜数字”游戏是学习一门新的编程语言的一个很好的入门程序,因为它以一种相当直接的方式锻炼了几个常见的编程概念。通过在不同的编程语言中实现这个简单的游戏,你可以展示语言的一些核心概念,并比较它们的细节。

你有喜欢的编程语言吗?你会如何用它来写“猜数字”游戏呢?请关注本系列文章,看看你可能感兴趣的其他编程语言的例子吧!


via: https://opensource.com/article/20/12/learn-rust

作者:Moshe Zadka 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这个不起眼的终端功能在紧要关头提供一个文本编辑器。

在 Linux 和 Unix 的 shell 中有一个不为人知的功能,它能让你用 cat 命令打开一个 do-while 循环。它被称为 heredoc,无论你使用什么 shell,它都能让你或多或少地拥有一个文本编辑器。它的语法是:

$ cat << EOF >> example.txt

中间的字符串(EOF),本质上是一个停止循环的条件。也就是说,如果你在一行中单独输入它,循环就会结束。在循环过程中,无论你在终端中输入什么,都会被管道传送到目标文件中(在本例中)。

安装

只要你有一个终端,你就能够启动 heredoc。我在 Bashtsh 和 Korn shell 中使用过这个语法技巧。

使用 heredoc

要打开一个 heredoc “会话”,你可以使用带重定向的 cat 命令。首先用终止字符串(常见约定是 EOF,代表 “End Of File”,但它实际上可以是任何字符串)指向 cat 命令。在终止字符串之后,将输出重定向到一个目标文件。然后,你就可以直接在终端中输入了,可以使用最常见的 shell 键盘快捷键来处理你的工作。当你在一行上输入你指定的终止字符串时,你的会话就结束了。你可以通过唯一的提示符(通常是 >)知道你是在一个 heredoc 循环中。

$ cat << EOF >> example.txt
> Everything you type here will be placed into example.txt when I type EOF on a line by itself. Until then, you can type...
>
> whatever...
>
> you want to type.
>
> EOF
$  

在终端等待 EOF 时,你输入的所有内容都会被放入目标文件中,提示符被忽略,EOF 本身也不是文件的一部分。

Everything you type here will be placed into example.txt when I type EOF on a line by itself. Until then, you can type...

whatever...

you want to type.

在现实中,你可能不会用 heredoc 语法来代替一个正常的文本编辑器。它是一个很好的快速处理方式,可以输入多行,但超过 10 行左右就开始限制它的作用了。例如,如果不触发你 shell 的 history 功能,你就不能编辑以前的行。根据你的 shell 和配置,你可能需要先按向上键,然后按向下键来找回你的文本,然后用 Ctrl+B 来后退。

你的 shell 的大部分功能都能正常工作,但可能没有撤销功能,也没有什么错误恢复功能。

此外,即使是最简安装的系统,可能也至少安装了 Vied

然而 heredoc 还是很有用的!它比 echo 更灵活,当你在编写 shell 脚本时,它是不可缺少的。例如,想象一下你正在编写一个安装脚本,以便你可以自动安装一组自定义应用。其中一个应用没有生成 .dekstop 文件,所以它不会出现在你的应用菜单中。为了解决这个问题,你决定在安装时生成一个 .desktop 文件。

与其编写一个 .desktop 文件,然后作为安装脚本的外部依赖,不如在安装脚本中使用 heredoc:

#!/bin/sh

VERSION=${VERSION:-x.y.z}
PKGNAM=${VERSION:-example}
PKG="${PKGNAM}"-"${VERSION}"-`arch`.tgz

# download package
wget "${PKG}"
tar txvf "${PKG}"

# use here doc to create missing .desktop file
cat << EOF >> $HOME/.local/share/applications/example.desktop
[Desktop Entry]
Version=1.0
Type=Application
Name="${PKGNAM}"
Comment="${PKGNAM}"
Exec="${PKGNAM}" %F
EOF

# insert the rest of an install script...

你自动地将文本输入到了一个文件中,而不需要文本编辑器(当然,除了你用来写脚本的那个)。下面是生成的 .desktop 文件的样子:

[Desktop Entry]
Version=1.0
Type=Application
Name=example
Comment=example
Exec=example %F

正如你所看到的,你可以在 heredoc 中使用变量,而且它们得到了正确的解析。EOF 字符串并没有出现在文件中,它只是标志着 heredoc 的结束。

比 echo 更好

heredoc 技术通常被认为比 echoprintf 更容易,因为一旦你“进入”了文档,你就可以自由地做任何你想做的事情。从这个意义上说,它是自由的,但与合适的文本编辑器相比,它是有限的。

使用 heredoc 来做快速笔记和 shell 脚本,再也不用为如何动态生成配置文件而烦恼了。


via: https://opensource.com/article/20/12/heredoc

作者:Seth Kenlon 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

花上一小时,用 Fedora 让一台过时的 Mac 重新有用。

最近,我偶然找到了一台 2011 年底的老款 13 英寸 MacBook Pro,有 125GB SSD 和 8GB 内存。我曾带着这台机器去世界各地旅行,当年,我开了很多场会议、研讨会或演示,分享 JBoss 技术带来的各种 AppDev 优势。

在验证了它的电池能用,充了电,重新安装了一个新的 OS X 之后,我发现 Safari 浏览器的版本受限于旧的安全规范,这意味着它现在无法连接到很多 HTTPS 网站。这就使得这个解决方案失效了。

这个老伙计该怎么处理呢?

自从我作为开发人员专门在 Linux 工作站上工作以来已经有几年了。我只使用 Fedora,所以我决定尝试在这台 MacBook Pro 上安装它的最新版本。

我只花了一个多小时就用下面的步骤让 Fedora 33 在这台笔记本上工作了。

下载 Fedora 33 并创建一个临场 USB

第一步是找到正确的安装 Fedora 的方法。这台机器有一个 CD 插槽,所以可以刻录一个 ISO 并从它启动,但我选择直接使用可启动的 USB 方式。

我登上了另一台 MacBook,访问了 Fedora Workstation 网站,它有 Fedora Media Writer 的链接。点击你的机器类型的图标(在我的例子中是苹果标志),你会得到一个安装包。

 title=

开始安装,可以看到一个引导你完成安装过程的图形用户界面(GUI)。选择 Fedora Workstation 33 选项。

 title=

接下来,选择右上角的“Create Live USB”选项。

 title=

镜像将开始下载,你将看到一个下拉菜单来选择安装位置。

 title=

插入一个有足够空间的 U 盘,下载完成后,就可以选择它并在上面安装镜像。完成后,关闭 GUI,取出 U 盘。

安装 Linux

将你创建的 U 盘插入 MacBook Pro 左侧的端口,并按住 Cmd 键左侧的 Option(或 Alt)键的同时重新启动。这将打开一个启动机器的选项菜单:使用 EFI 选项,因为那是 USB 镜像。

笔记本电脑将从 USB 设备启动,你可以按照正常的 Fedora 安装过程进行。如果你能将 MacBook Pro 插入网线连接,会有帮助,因为它的 Broadcom WiFi 设备无法开箱即用。

 title=

你现在也可以将 Fedora 安装到你的硬盘上,并将它永久地放在你的机器上。

 title=

一旦安装程序完成,重新启动机器,Fedora 33 现在应该是启动选项。

 title=

唯一缺少的就是 WiFi 驱动,所以要保持网线连接,安装你正在运行的内核的开发包,并为该内核构建 broadcom-wl 驱动。

验证你需要用于 WiFi 的卡。

$ lspci -vnn -d 14e4:

在输出中会有几项,包括如下内容:

Network controller [0280]: Broadcom Inc. and subsidiaries....

Subsystem: Apple Inc. AirPort Extreme...

安装一个仓库来拉取 Broadcom 相关的部分:

$ su -c 'dnf install -y http://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm'

接下来的部分很有趣:如果你查看正在运行的内核时,你会看到 v5.9.8-200.fc33,但是你要使用开发内核包来构建你的 Broadcom 无线驱动。所以,你需要安装 v5.8.15-301.fc33(在写这篇文章的时候可用)。使用 uname -r 检查它们,并使用 sudo dnf list kernel 列出已安装的内核包:

$ sudo dnf list kernel
kernel.x86_64                     5.8.15-301.fc33
kernel.x86_64                     5.9.8-200.fc33

安装开发包:

$ sudo dnf install -y akmods kernel-devel-5.8.15-301.fc33

 title=

安装 Broadcom 无线软件包:

$ sudo dnf install -y broadcom-wl

构建内核模块:

$ sudo akmods

 title=

重新启动你的机器,你应该可以看到无线驱动(wl)。

$ lsmod | grep wl

在 Fedora 中设置你的无线连接:

 title=

这篇文章对我来说有些出乎意料,但我希望它能帮助别人在周末享受一些老硬件的乐趣!

现在要走不寻常路了……在 2011 年的 Macbook Pro 上安装 #Fedora。祝我好运! pic.twitter.com/zlsESnq2Px

此文原载于 Schabell.org,经许可转载。


via: https://opensource.com/article/20/12/linux-macbook

作者:Eric D. Schabell 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

通过编写一个“猜数字”游戏来探索 Python(和其他编程语言)。

在这个系列中,我们要用不同的编程语言编写相同的应用,以比较各种语言是如何工作的,并说明使用标准测试程序是学习新编程好方法。

当你学习一门新的编程语言时,关注它们的共同点是件好事。变量、表达式和语句是大多数编程语言的基础。一旦你理解了这些概念,你就可以开始弄清楚其余的东西。

因为编程语言有许多相似之处,一旦你知道一种语言,你通常可以通过观察它与你所知道的语言的不同之处来学习另一种语言的基础知识。使用你用其他语言编写的标准测试程序,可以让你专注于语言,而不是程序的逻辑。

为了证明这点,我们正在测试如何用多种语言编写一个“猜数字”程序。计算机选择一个 1 到 100 之间的数字,然后让你猜。程序循环,直到你猜出正确答案。

“猜数字”程序练习了编程语言的几个概念:

  • 变量
  • 输入
  • 输出
  • 条件判断
  • 循环

这是一个很好的学习新编程语言的实践实验。

用 Python 猜数字

Python 软件基金会的话来说。“Python 是一种解释性的、交互式的、面向对象的程序设计语言,它包含了模块、异常、动态类型、非常高层的动态数据类型和类。”它是一种很好的通用编程语言,从简单的脚本到复杂的 GUI 应用都很适用。

你可以通过编写一个版本的“猜数字”游戏来探索 Python。这是我的实现:

import random as randomlib
random = randomlib.randint(1, 100)
print("Guess a number between 1 and 100")
while True:
    guess = int(input())
    if guess < random:
        print("Too low")
    elif guess > random:
        print("Too high")
    else:
        print("That's right!")
        break

要给一个变量赋值,请列出变量的名称,然后是 = 号。例如,语句 random = 0random 变量分配了一个零值。

脚本的第一行就导入了 random 模块。由于本系列中的所有程序都使用 random 作为变量的名称,你可以使用 import random as randomlib 以别名导入它,以避免命名冲突。

很少有函数被内置到 Python 中,大多数函数必须从标准库中显式导入。random 标准库模块有生成各种随机值的功能。

脚本的第二行读取函数 randint() 的结果,并将其赋值给名为 random 的变量。函数需要两个参数:一个最小值和一个最大值。在本例中,范围是 1100,以使游戏具有足够的挑战性。

你可以使用 input() 函数提示用户输入一个值。如果你写 guess = int(input()),Python 会等待用户输入一些文本,将其转换为一个整数,然后将值存储在 guess 变量中。

Python 支持条件表达式和循环等流程控制。在“猜数字”游戏中,只要 guess 中的值不等于 random,Python 就会继续循环。

如果猜测值小于随机数,Python 会打印 Too low,如果猜测值大于这个数字,Python 会打印 Too high

示例输出

现在你已经写好了 Python 程序,运行它来玩“猜数字”游戏。每次运行程序,Python 都会随机选取一个不同的数字。为了完成这个游戏,你需要猜测,直到找到正确的数字:

$ python guess.py
Guess a number between 1 and 100
 50
Too high
 25
Too high
 12
Too high
 7
Too high
 3
Too low
 5
Too low
 6
That's right!

在学习一门新的编程语言时,这个“猜数字”游戏是一个很好的入门程序,因为它以一种相当直接的方式练习了几个常见的编程概念。通过在不同的编程语言中实现这个简单的游戏,你可以展示不同编程语言的一些核心概念,并比较每种语言的细节。

你有喜欢的编程语言吗?你会如何编写“猜数字”游戏?请关注本系列文章,看看你可能感兴趣的其他编程语言的例子吧!


via: https://opensource.com/article/20/12/learn-python

作者:Moshe Zadka 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出