@zhongdao
2019-05-24T15:51:28.000000Z
字数 27725
阅读 1775
未分类
https://www.toptal.com/go/go-programming-a-step-by-step-introductory-tutorial
The relatively new Go programming language sits neatly in the middle of the landscape, providing lots of good features and deliberately omitting many bad ones. It compiles fast, runs fast-ish, includes a runtime and garbage collection, has a simple static type system and dynamic interfaces, and an excellent standard library. This is why so many developers are keen to learn Go programming.
相对较新的 Go 编程语言整齐地坐落在风景的中间,提供了许多好的特性,故意省略了许多不好的特性。 它编译速度快,运行速度快,包括运行时和垃圾收集,具有简单的静态类型系统和动态接口,以及优秀的标准库。 这就是为什么那么多开发人员热衷于学习围棋编程。
Go and OOP 2. Go and OOP
OOP is one of those features that Go deliberately omits. It has no subclassing, and so there are no inheritance diamonds or super calls or virtual methods to trip you up. Still, many of the useful parts of OOP are available in other ways.
面向对象程序设计是 Go 故意省略的特性之一。 它没有子类化,因此没有继承的菱形或超级调用或虚拟方法来绊倒您。 尽管如此,OOP 的许多有用部分还是可以通过其他方式获得的
Mixins are available by embedding structs anonymously, allowing their methods to be called directly on the containing struct (see
* mixin * 可以匿名嵌入 struct,允许直接在包含 struct 上调用它们的方法(参见
embedding 嵌入
). Promoting methods in this way is called forwarding, and it's not the same as subclassing: the method will still be invoked on the inner, embedded struct.
). 以这种方式提升方法被称为 * forwarding * ,它与子类化不同: 该方法仍将在内部的嵌入式结构上调用
Embedding also doesn't imply polymorphism. While A
may
嵌入也不意味着多态性。 而 a'may
have 有
a B
, that doesn't mean it
A'b',那不是真的
is是
a B
-- functions which take a B
won't take an A
instead. For that, we need
A'b'---- 带 a'b'的函数不会带 a'。 为此,我们需要
interfaces 界面
, which we'll encounter briefly later.
我们稍后会简单介绍一下
Meanwhile, Golang takes a strong position on features that can lead to confusion and bugs. It omits OOP idioms such as inheritance and polymorphism, in favor of composition and simple interfaces. It downplays exception handling in favour of explicit errors in return values. There is exactly one correct way to lay out Go code, enforced by the gofmt
tool. And so on.
与此同时,Golang 对可能导致混乱和 bug 的特性采取了强硬的立场。 它省略了诸如继承和多态之类的 OOP 习惯用法,而是采用了组合和简单的接口。 它降低了异常处理的重要性,代之以返回值中的显式错误。 确实有一种正确的方法来布局 Go 代码,由 gofmt 工具执行。 诸如此类。
Go is also a great language for writing concurrent programs: programs with many independently running parts. An obvious example is a webserver: Every request runs separately, but requests often need to share resources such as sessions, caches, or notification queues. This means skilled Go programmers need to deal with concurrent access to those resources.
Go 也是编写并发程序的一种很好的语言: 包含许多独立运行部件的程序。 一个明显的例子是 webserver: 每个请求都单独运行,但是请求通常需要共享资源,比如会话、缓存或通知队列。 这意味着熟练的围棋程序员需要处理对这些资源的并发访问。
While Golang has an excellent set of low-level features for handling concurrency, using them directly can become complicated. In many cases, a handful of reusable abstractions over those low-level mechanisms makes life much easier.
虽然 Golang 在处理并发性方面有一系列优秀的底层特性,但是直接使用它们可能会变得复杂。 在许多情况下,这些低级机制上的少量可重用抽象使得工作更加容易。
In today’s Go programming tutorial, we’re going to look at one such abstraction: A wrapper which can turn any data structure into a transactional service. We’ll use a Fund
type as an example – a simple store for our startup’s remaining funding, where we can check the balance and make withdrawals.
在今天的 Go 编程教程中,我们将看到一个这样的抽象: 一个可以将任何数据结构转换为事务服务的包装器。 我们将使用一个基金类型作为一个例子-一个简单的商店为我们的创业公司的剩余资金,在那里我们可以检查余额和提款。
To demonstrate this in practice, we’ll build the service in small steps, making a mess along the way and then cleaning it up again. As we progress through our Go tutorial, we’ll encounter lots of cool Go language features, including:
为了在实践中演示这一点,我们将分小步构建服务,在构建过程中将其弄得一团糟,然后再次清理它。 随着我们学习围棋教程,我们会遇到很多很酷的围棋语言特性,包括:
Let’s write some code to track our startup’s funding. The fund starts with a given balance, and money can only be withdrawn (we’ll figure out revenue later).
让我们编写一些代码来跟踪我们创业公司的融资情况。 该基金从一个给定的余额开始,资金只能提取(我们稍后再计算收入)。
Go is deliberately not an object-oriented language: There are no classes, objects, or inheritance. Instead, we’ll declare a struct type called Fund
, with a simple function to create new fund structs, and two public methods.
Go 故意不是面向对象的语言: 没有类、对象或继承。 相反,我们将声明一个名为 Fund 的结构类型,其中包含一个创建新的 Fund 结构的简单函数和两个公共方法。
fund.go
乐趣,去吧
package funding
type Fund struct {
// balance is unexported (private), because it's lowercase
balance int
}
// A regular function returning a pointer to a fund
func NewFund(initialBalance int) *Fund {
// We can return a pointer to a new struct without worrying about
// whether it's on the stack or heap: Go figures that out for us.
return &Fund{
balance: initialBalance,
}
}
// Methods start with a *receiver*, in this case a Fund pointer
func (f *Fund) Balance() int {
return f.balance
}
func (f *Fund) Withdraw(amount int) {
f.balance -= amount
}
Next we need a way to test Fund
. Rather than writing a separate program, we’ll use Go’s testing package, which provides a framework for both unit tests and benchmarks. The simple logic in our Fund
isn’t really worth writing unit tests for, but since we’ll be talking a lot about concurrent access to the fund later on, writing a benchmark makes sense.
接下来我们需要一个方法来测试基金。 与编写单独的程序不同,我们将使用 Go 的测试包,它为单元测试和基准测试提供了一个框架。 我们基金中的简单逻辑实在不值得编写单元测试,但由于我们稍后将讨论很多关于同时获得基金的问题,编写一个基准测试是有意义的。
Benchmarks are like unit tests, but include a loop which runs the same code many times (in our case, fund.Withdraw(1)
). This allows the framework to time how long each iteration takes, averaging out transient differences from disk seeks, cache misses, process scheduling, and other unpredictable factors.
基准测试类似于单元测试,但是包含一个循环,它多次运行相同的代码(在我们的例子中是 fund)。 撤回(1))。 这允许框架计算每次迭代所需的时间,平均化磁盘查找、缓存错误、进程调度和其他不可预测因素的瞬时差异。
The testing framework wants each benchmark to run for at least 1 second (by default). To ensure this, it will call the benchmark multiple times, passing in an increasing “number of iterations” value each time (the b.N
field), until the run takes at least a second.
测试框架希望每个基准至少运行1秒(默认情况下)。 为了确保这一点,它将多次调用基准测试,每次传入一个不断增加的"迭代次数"值(b.N 字段) ,直到运行至少需要一秒钟。
For now, our benchmark will just deposit some money and then withdraw it one dollar at a time.
目前,我们的基准只是存入一些钱,然后一次取出一美元。
fund_test.go
基金试验。 走
package funding
import "testing"
func BenchmarkFund(b *testing.B) {
// Add as many dollars as we have iterations this run
fund := NewFund(b.N)
// Burn through them one at a time until they are all gone
for i := 0; i < b.N; i++ {
fund.Withdraw(1)
}
if fund.Balance() != 0 {
b.Error("Balance wasn't zero:", fund.Balance())
}
}
Now let’s run it:
现在让我们运行它:
$ go test -bench . funding
testing: warning: no tests to run
PASS
BenchmarkWithdrawals 2000000000 1.69 ns/op
ok funding 3.576s
That went well. We ran two billion (!) iterations, and the final check on the balance was correct. We can ignore the “no tests to run” warning, which refers to the unit tests we didn’t write (in later Go programming examples in this tutorial, the warning is snipped out).
进展顺利。 我们运行了20亿(!) 最终的平衡检查是正确的。 我们可以忽略"no test to run"警告,它指的是我们没有编写的单元测试(在本教程后面的 Go 编程示例中,警告被剪掉了)。
Now let’s make the benchmark concurrent, to model different users making withdrawals at the same time. To do that, we’ll spawn ten goroutines and have each of them withdraw one tenth of the money.
现在让我们使基准并发,以模型不同的用户在同一时间进行提款。 为了做到这一点,我们将产卵10个金发女郎,并让她们每人取出十分之一的钱。
Goroutines are the basic building block for concurrency in the Go language. They are green threads – lightweight threads managed by the Go runtime, not by the operating system. This means you can run thousands (or millions) of them without any significant overhead. Goroutines are spawned with the go
keyword, and always start with a function (or method call):
Goroutine 是 Go 语言中并发性的基本构建块。 它们是绿线程——由 Go 运行时(而不是由操作系统)管理的轻量级线程。 这意味着您可以在没有任何重大开销的情况下运行数千个(或数百万个)它们。 Goroutine 由 go 关键字生成,始终以函数(或方法调用)开头:
// Returns immediately, without waiting for `DoSomething()` to complete
go DoSomething()
Often, we want to spawn off a short one-time function with just a few lines of code. In this case we can use a closure instead of a function name:
通常,我们只需要几行代码就可以生成一个简短的一次性函数。 在这种情况下,我们可以使用闭包代替函数名:
go func() {
// ... do stuff ...
}() // Must be a function *call*, so remember the ()
Once all our goroutines are spawned, we need a way to wait for them to finish. We could build one ourselves using channels, but we haven’t encountered those yet, so that would be skipping ahead.
一旦我们所有的 goroutines 都产生了,我们需要一种方法来等待它们完成。 我们可以使用通道自己建立一个,但是我们还没有遇到那些,所以那将是跳过。
For now, we can just use the WaitGroup
type in Go’s standard library, which exists for this very purpose. We’ll create one (called “wg
”) and call wg.Add(1)
before spawning each worker, to keep track of how many there are. Then the workers will report back using wg.Done()
. Meanwhile in the main goroutine, we can just say wg.Wait()
to block until every worker has finished.
现在,我们可以只使用 Go 的标准库中的 WaitGroup 类型,它的存在就是为了这个目的。 我们将创建一个(称为"wg")并调用 wg。 在产生每个工蜂之前添加(1) ,以跟踪有多少工蜂。 然后工作人员将使用 wg 返回报告。 完成()。 与此同时,在主要的 goroutine,我们可以说 wg。 等待()阻塞,直到所有工作人员完成。
Inside the worker goroutines in our next example, we’ll use defer
to call wg.Done()
.
在下一个示例中的 worker goroutines 内部,我们将使用 defer 调用 wg。 完成()。
defer
takes a function (or method) call and runs it immediately before the current function returns, after everything else is done. This is handy for cleanup:
Defer 接受一个函数(或方法)调用,并在完成其他所有操作后,在当前函数返回之前运行它。 这对于清理非常方便:
func() {
resource.Lock()
defer resource.Unlock()
// Do stuff with resource
}()
This way we can easily match the Unlock
with its Lock
, for readability. More importantly, a deferred function will run even if there is a panic in the main function (something that we might handle via try-finally in other languages).
这样我们就可以很容易地将解锁和它的锁匹配起来,从而提高可读性。 更重要的是,即使在 main 函数中出现错误(我们可以通过其他语言的 try-finally 来处理这个错误) ,延迟函数也会运行。
Lastly, deferred functions will execute in the reverse order to which they were called, meaning we can do nested cleanup nicely (similar to the C idiom of nested goto
s and label
s, but much neater):
最后,延迟函数将以相反的顺序执行,这意味着我们可以很好地进行嵌套清理(类似于嵌套 gotos 和标签的 c 语言习惯,但要简洁得多) :
func() {
db.Connect()
defer db.Disconnect()
// If Begin panics, only db.Disconnect() will execute
transaction.Begin()
defer transaction.Close()
// From here on, transaction.Close() will run first,
// and then db.Disconnect()
// ...
}()
OK, so with all that said, here’s the new version:
好了,综上所述,下面是新版本:
fund_test.go
基金试验。 走
package funding
import (
"sync"
"testing"
)
const WORKERS = 10
func BenchmarkWithdrawals(b *testing.B) {
// Skip N = 1
if b.N < WORKERS {
return
}
// Add as many dollars as we have iterations this run
fund := NewFund(b.N)
// Casually assume b.N divides cleanly
dollarsPerFounder := b.N / WORKERS
// WaitGroup structs don't need to be initialized
// (their "zero value" is ready to use).
// So, we just declare one and then use it.
var wg sync.WaitGroup
for i := 0; i < WORKERS; i++ {
// Let the waitgroup know we're adding a goroutine
wg.Add(1)
// Spawn off a founder worker, as a closure
go func() {
// Mark this worker done when the function finishes
defer wg.Done()
for i := 0; i < dollarsPerFounder; i++ {
fund.Withdraw(1)
}
}() // Remember to call the closure!
}
// Wait for all the workers to finish
wg.Wait()
if fund.Balance() != 0 {
b.Error("Balance wasn't zero:", fund.Balance())
}
}
We can predict what will happen here. The workers will all execute Withdraw
on top of each other. Inside it, f.balance -= amount
will read the balance, subtract one, and then write it back. But sometimes two or more workers will both read the same balance, and do the same subtraction, and we’ll end up with the wrong total. Right?
我们可以预测这里会发生什么。 工人们将一个接一个地执行撤退。 在里面,f balance-amount 将读取余额,减去一,然后写回去。 但有时两个或两个以上的工作人员都读取相同的余额,并做相同的减法,我们最终会得到错误的总数。 对吧?
$ go test -bench . funding
BenchmarkWithdrawals 2000000000 2.01 ns/op
ok funding 4.220s
No, it still passes. What happened here?
不,还是会过去的。 这里发生了什么?
Remember that goroutines are green threads – they’re managed by the Go runtime, not by the OS. The runtime schedules goroutines across however many OS threads it has available. At the time of writing this Go language tutorial, Go doesn’t try to guess how many OS threads it should use, and if we want more than one, we have to say so. Finally, the current runtime does not preempt goroutines – a goroutine will continue to run until it does something that suggests it’s ready for a break (like interacting with a channel).
记住 goroutine 是绿色线程——它们由 Go 运行时管理,而不是由 OS 管理。 运行时在所有可用的操作系统线程之间调度 goroutine。 在写这篇 Go 语言教程的时候,Go 并没有尝试去猜测它应该使用多少操作系统线程,如果我们想要更多的线程,我们必须这么说。 最后,当前运行时不抢占 goroutine —— goroutine 将继续运行,直到它执行一些表明它已准备好中断的操作(比如与通道交互)。
All of this means that although our benchmark is now concurrent, it isn’t parallel. Only one of our workers will run at a time, and it will run until it’s done. We can change this by telling Go to use more threads, via the GOMAXPROCS
environment variable.
所有这些都意味着,尽管我们的基准现在是并发的,但它并不是并行的。 一次只能有一个工人运行,而且会一直运行到完成为止。 我们可以通过告诉 Go 使用更多的线程来改变这种情况,通过 gomax / procs 环境变量。
$ GOMAXPROCS=4 go test -bench . funding
BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4
account_test.go:39: Balance wasn't zero: 4238
ok funding 0.007s
That’s better. Now we’re obviously losing some of our withdrawals, as we expected.
这样好多了。 正如我们所料,现在我们显然损失了一部分资金。
At this point we have various options. We could add an explicit mutex or read-write lock around the fund. We could use a compare-and-swap with a version number. We could go all out and use a CRDT scheme (perhaps replacing the balance
field with lists of transactions for each client, and calculating the balance from those).
在这一点上,我们有各种选择。 我们可以在基金周围增加一个明确的互斥锁或读写锁。 我们可以使用带有版本号的比较并交换。 我们可以全力以赴,使用 CRDT 方案(也许可以用每个客户的交易列表替换 balance 字段,并根据这些列表计算余额)。
But we won’t do any of those things now, because they’re messy or scary or both. Instead, we’ll decide that a fund should be a server. What’s a server? It’s something you talk to. In Go, things talk via channels.
但是我们现在不会做任何这些事情,因为它们要么凌乱,要么令人害怕,或者两者兼而有之。 相反,我们会决定一个基金应该是一个服务器。 什么是服务器? 这是你可以谈论的事情。 在围棋中,事物通过渠道进行交流。
Channels are the basic communication mechanism between goroutines. Values are sent to the channel (with channel <- value
), and can be received on the other side (with value = <- channel
). Channels are “goroutine safe”, meaning that any number of goroutines can send to and receive from them at the same time.
信道是网络间最基本的通信机制。 值被发送到通道(使用 channel-value) ,并且可以在另一端(使用 value-channel)接收。 频道是"goroutine 安全",这意味着任何数量的 goroutine 可以发送和接收他们在同一时间。
Buffering 缓冲
Buffering communication channels can be a performance optimization in certain circumstances, but it should be used with great care (and benchmarking!).
在某些情况下,缓冲通信通道可能是一种性能优化,但是应该非常小心地使用它(并进行基准测试!)
However, there are uses for buffered channels which aren't directly about communication.
然而,缓冲通道的一些用途并不直接与通信有关
For instance, a common throttling idiom creates a channel with (for example) buffer size 10
and then sends ten tokens into it immediately. Any number of worker goroutines are then spawned, and each receives a token from the channel before starting work, and sends it back afterward. Then, however many workers there are, only ten will ever be working at the same time.
例如,一个常见的节流习惯用法创建一个缓冲区大小为"10"的通道,然后立即向其发送10个令牌。 然后产生任意数量的工作者 goroutine,每个工作者在开始工作之前从通道接收一个令牌,并在开始工作之后将其发送回来。 然后,不管有多少工人,只有十个人会在同一时间工作
By default, Go channels are unbuffered. This means that sending a value to a channel will block until another goroutine is ready to receive it immediately. Go also supports fixed buffer sizes for channels (using make(chan someType, bufferSize)
). However, for normal use, this is usually a bad idea.
默认情况下,Go 通道是不缓冲的。 这意味着向通道发送一个值将阻塞,直到另一个 goroutine 准备好立即接收它。 Go 也支持通道的固定缓冲区大小(使用 make (chan someType,bufferSize))。 然而,对于正常使用,这通常是一个坏主意。
Imagine a webserver for our fund, where each request makes a withdrawal. When things are very busy, the FundServer
won’t be able to keep up, and requests trying to send to its command channel will start to block and wait. At that point we can enforce a maximum request count in the server, and return a sensible error code (like a 503 Service Unavailable
) to clients over that limit. This is the best behavior possible when the server is overloaded.
想象一下,我们的基金有一个网络服务器,每个请求都会提款。 当事情非常繁忙时,FundServer 将无法跟上,并且试图发送到其命令通道的请求将开始阻塞和等待。 在这一点上,我们可以在服务器中强制执行最大的请求计数,并返回一个合理的错误代码(如503服务不可用)超过该限制的客户端。 这是服务器超载时可能出现的最佳行为。
Adding buffering to our channels would make this behavior less deterministic. We could easily end up with long queues of unprocessed commands based on information the client saw much earlier (and perhaps for requests which had since timed out upstream). The same applies in many other situations, like applying backpressure over TCP when the receiver can’t keep up with the sender.
向通道中添加缓冲会降低这种行为的确定性。 我们可以很容易地结束基于客户机更早看到的信息(也许对于已经超时的上游请求)的未处理命令的长队列。 同样的情况也适用于许多其他情况,比如当接收方跟不上发送方时,在 TCP 上施加反向压力。
In any case, for our Go example, we’ll stick with the default unbuffered behavior.
在任何情况下,对于我们的 Go 示例,我们将坚持使用默认的未缓冲行为。
We’ll use a channel to send commands to our FundServer
. Every benchmark worker will send commands to the channel, but only the server will receive them.
我们将使用一个通道向 FundServer 发送命令。 每个基准测试工作者将向通道发送命令,但只有服务器将接收命令。
We could turn our Fund type into a server implementation directly, but that would be messy – we’d be mixing concurrency handling and business logic. Instead, we’ll leave the Fund type exactly as it is, and make FundServer
a separate wrapper around it.
我们可以直接将基金类型转换为服务器实现,但这样做会很麻烦——我们将混合并发处理和业务逻辑。 相反,我们将保持 Fund 类型的原样,并将 FundServer 作为一个单独的包装器围绕它。
Like any server, the wrapper will have a main loop in which it waits for commands, and responds to each in turn. There’s one more detail we need to address here: The type of the commands.
与任何服务器一样,包装器将有一个主循环,在这个循环中等待命令,并依次响应每个命令。 这里还有一个细节我们需要解决: 命令的类型。
Pointers 指针
We could have made our commands channel take pointers to commands (chan *TransactionCommand
). Why didn't we?
我们可以让我们的命令通道将 * 指针 * 指向命令('chan * TransactionCommand')。 为什么我们没有呢?
Passing pointers between goroutines is risky, because either goroutine might modify it. It's also often less efficient, because the other goroutine might be running on a different CPU core (meaning more cache invalidation).
在 goroutine 之间传递指针是有风险的,因为任何 goroutine 都可能对其进行修改。 它的效率通常也较低,因为另一个 goroutine 可能运行在不同的 CPU 核心上(这意味着更多的缓存失效)
Whenever possible, prefer to pass plain values around.
只要有可能,就倾向于传递普通值
In the next section below, we’ll be sending several different commands, each with its own struct type. We want the server’s Commands channel to accept any of them. In an OOP language we might do this via polymorphism: Have the channel take a superclass, of which the individual command types were subclasses. In Go, we use interfaces instead.
在下一节中,我们将发送几个不同的命令,每个命令都有自己的结构类型。 我们希望服务器的命令通道接受它们中的任何一个。 在 OOP 语言中,我们可以通过多态性来实现这一点: 让通道接受一个超类,其中单独的命令类型是子类。 在围棋中,我们使用接口来代替。
An interface is a set of method signatures. Any type that implements all of those methods can be treated as that interface (without being declared to do so). For our first run, our command structs won’t actually expose any methods, so we’re going to use the empty interface, interface{}
. Since it has no requirements, any value (including primitive values like integers) satisfies the empty interface. This isn’t ideal – we only want to accept command structs – but we’ll come back to it later.
接口是一组方法签名。 任何实现所有这些方法的类型都可以被视为该接口(不需要声明)。 对于我们的第一次运行,我们的命令 struct 实际上不会公开任何方法,因此我们将使用空接口 interface {}。 因为它没有要求,所以任何值(包括像整数这样的原始值)都满足空接口。 这并不理想——我们只想接受命令结构——但是稍后我们会回过头来讨论它。
For now, let’s get started with the scaffolding for our Go server:
现在,让我们开始为 Go 服务器搭建脚手架:
server.go
服务员,去吧
package funding
type FundServer struct {
Commands chan interface{}
fund Fund
}
func NewFundServer(initialBalance int) *FundServer {
server := &FundServer{
// make() creates builtins like channels, maps, and slices
Commands: make(chan interface{}),
fund: NewFund(initialBalance),
}
// Spawn off the server's main loop immediately
go server.loop()
return server
}
func (s *FundServer) loop() {
// The built-in "range" clause can iterate over channels,
// amongst other things
for command := range s.Commands {
// Handle the command
}
}
Now let’s add a couple of Golang struct types for the commands:
现在让我们为这些命令添加几个 Golang 结构类型:
type WithdrawCommand struct {
Amount int
}
type BalanceCommand struct {
Response chan int
}
The WithdrawCommand
just contains the amount to withdraw. There’s no response. The BalanceCommand
does have a response, so it includes a channel to send it on. This ensures that responses will always go to the right place, even if our fund later decides to respond out-of-order.
Withdrawcommand 只包含要提取的金额。 没有回应。 Balanchecommand 确实有一个响应,因此它包含一个发送它的通道。 这确保了反应总是会到正确的地方,即使我们的基金后来决定反应的秩序。
Now we can write the server’s main loop:
现在我们可以写服务器的主循环:
func (s *FundServer) loop() {
for command := range s.Commands {
// command is just an interface{}, but we can check its real type
switch command.(type) {
case WithdrawCommand:
// And then use a "type assertion" to convert it
withdrawal := command.(WithdrawCommand)
s.fund.Withdraw(withdrawal.Amount)
case BalanceCommand:
getBalance := command.(BalanceCommand)
balance := s.fund.Balance()
getBalance.Response <- balance
default:
panic(fmt.Sprintf("Unrecognized command: %v", command))
}
}
}
Hmm. That’s sort of ugly. We’re switching on the command type, using type assertions, and possibly crashing. Let’s forge ahead anyway and update the benchmark to use the server.
嗯。 这有点难看。 我们切换到命令类型,使用类型断言,可能会崩溃。 无论如何,让我们继续前进,更新基准以使用服务器。
func BenchmarkWithdrawals(b *testing.B) {
// ...
server := NewFundServer(b.N)
// ...
// Spawn off the workers
for i := 0; i < WORKERS; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < dollarsPerFounder; i++ {
server.Commands <- WithdrawCommand{ Amount: 1 }
}
}()
}
// ...
balanceResponseChan := make(chan int)
server.Commands <- BalanceCommand{ Response: balanceResponseChan }
balance := <- balanceResponseChan
if balance != 0 {
b.Error("Balance wasn't zero:", balance)
}
}
That was sort of ugly too, especially when we checked the balance. Never mind. Let’s try it:
这也有点难看,尤其是当我们检查余额的时候。 没关系。 让我们试试:
$ GOMAXPROCS=4 go test -bench . funding
BenchmarkWithdrawals-4 5000000 465 ns/op
ok funding 2.822s
Much better, we’re no longer losing withdrawals. But the code is getting hard to read, and there are more serious problems. If we ever issue a BalanceCommand
and then forget to read the response, our fund server will block forever trying to send it. Let’s clean things up a bit.
更好的是,我们不再失去撤资。 但是代码越来越难以阅读,而且还有更严重的问题。 如果我们发布了一个 balanccommand,然后忘记读取响应,我们的基金服务器将永远阻止试图发送它。 我们把事情弄清楚一点。
A server is something you talk to. What’s a service? A service is something you talk to with an API. Instead of having client code work with the command channel directly, we’ll make the channel unexported (private) and wrap the available commands up in functions.
服务器是与你交谈的东西。 什么是服务? 服务是与 API 对话的东西。 与让客户机代码直接使用命令通道不同,我们将使通道不导出(私有的) ,并将可用的命令封装在函数中。
type FundServer struct {
commands chan interface{} // Lowercase name, unexported
// ...
}
func (s *FundServer) Balance() int {
responseChan := make(chan int)
s.commands <- BalanceCommand{ Response: responseChan }
return <- responseChan
}
func (s *FundServer) Withdraw(amount int) {
s.commands <- WithdrawCommand{ Amount: amount }
}
Now our benchmark can just say server.Withdraw(1)
and balance := server.Balance()
, and there’s less chance of accidentally sending it invalid commands or forgetting to read responses.
现在我们的基准测试只能说服务器。 撤回(1)并平衡: 服务器。 Balance () ,意外发送无效命令或忘记读取响应的几率更小。
There’s still a lot of extra boilerplate for the commands, but we’ll come back to that later.
对于这些命令还有很多额外的样板文件,但是我们稍后会回来讨论这个问题。
Eventually, the money always runs out. Let’s agree that we’ll stop withdrawing when our fund is down to its last ten dollars, and spend that money on a communal pizza to celebrate or commiserate around. Our benchmark will reflect this:
最终,钱总会用完。 让我们一致同意,当我们的基金只剩下最后10美元时,我们将停止提款,并将这些钱花在公共披萨上,以庆祝或同情周围的人。 我们的基准将反映这一点:
// Spawn off the workers
for i := 0; i < WORKERS; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < dollarsPerFounder; i++ {
// Stop when we're down to pizza money
if server.Balance() <= 10 {
break
}
server.Withdraw(1)
}
}()
}
// ...
balance := server.Balance()
if balance != 10 {
b.Error("Balance wasn't ten dollars:", balance)
}
This time we really can predict the result.
这次我们真的可以预测结果了。
$ GOMAXPROCS=4 go test -bench . funding
BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4
fund_test.go:43: Balance wasn't ten dollars: 6
ok funding 0.009s
We’re back where we started – several workers can read the balance at once, and then all update it. To deal with this we could add some logic in the fund itself, like a minimumBalance
property, or add another command called WithdrawIfOverXDollars
. These are both terrible ideas. Our agreement is amongst ourselves, not a property of the fund. We should keep it in application logic.
我们回到了开始的地方——几个工作人员可以一次读取余额,然后所有人都可以更新余额。 为了解决这个问题,我们可以在基金本身中添加一些逻辑,比如最小平衡属性,或者添加另一个命令 WithdrawIfOverXDollars。 这些都是可怕的想法。 我们之间的协议是我们自己的,不是基金的财产。 我们应该把它放在应用逻辑中。
What we really need is transactions, in the same sense as database transactions. Since our service executes only one command at a time, this is super easy. We’ll add a Transact
command which contains a callback (a closure). The server will execute that callback inside its own goroutine, passing in the raw Fund
. The callback can then safely do whatever it likes with the Fund
.
我们真正需要的是事务,就像数据库事务一样。 因为我们的服务一次只执行一个命令,所以这非常简单。 我们将添加一个包含回调(闭包)的 Transact 命令。 服务器将在自己的 goroutine 内执行回调,传入原始基金。 然后,回调可以安全地对基金做它喜欢做的任何事情。
Semaphores and errors 信号量和错误
In this next example we're doing two small things wrong.
在下一个例子中,我们犯了两个小错误
First, we're using a Done
channel as a semaphore to let calling code know when its transaction has finished. That's fine, but why is the channel type bool
? We'll only ever send true
into it to mean "done" (what would sending false
even mean?). What we really want is a single-state value (a value that has no value?). In Go, we can do this using the empty struct type: struct{}
. This also has the advantage of using less memory. In the example we'll stick with bool
so as not to look too scary.
首先,我们使用一个"Done"通道作为信号量,让调用代码知道它的事务何时结束。 那很好,但是为什么频道类型是'bool'? 我们只会把"true"发送进去,意思是"完成了"(发送"false"甚至意味着什么?) . 我们真正想要的是一个单状态值(一个没有值的值?) . 在 Go 中,我们可以使用空的 struct type:'struct {}'来完成此操作。 这也有使用较少内存的优点。 在这个例子中,我们将坚持使用"bool",以免看起来太吓人
Second, our transaction callback isn't returning anything. As we'll see in a moment, we can get values out of the callback into calling code using scope tricks. However, transactions in a real system would presumably fail sometimes, so the Go convention would be to have the transaction return an error
(and then check whether it was nil
in calling code).
其次,我们的事务回调不返回任何东西。 稍后我们将看到,我们可以使用作用域技巧将回调中的值转换为调用代码。 然而,实际系统中的事务有时可能会失败,所以 Go 约定是让事务返回一个"error"(然后检查调用代码是否为"nil")
We're not doing that either for now, since we don't have any errors to generate.
我们现在也没有这样做,因为我们没有任何错误可以生成
// Typedef the callback for readability
type Transactor func(fund *Fund)
// Add a new command type with a callback and a semaphore channel
type TransactionCommand struct {
Transactor Transactor
Done chan bool
}
// ...
// Wrap it up neatly in an API method, like the other commands
func (s *FundServer) Transact(transactor Transactor) {
command := TransactionCommand{
Transactor: transactor,
Done: make(chan bool),
}
s.commands <- command
<- command.Done
}
// ...
func (s *FundServer) loop() {
for command := range s.commands {
switch command.(type) {
// ...
case TransactionCommand:
transaction := command.(TransactionCommand)
transaction.Transactor(s.fund)
transaction.Done <- true
// ...
}
}
}
Our transaction callbacks don’t directly return anything, but the Go language makes it easy to get values out of a closure directly, so we’ll do that in the benchmark to set the pizzaTime
flag when money runs low:
我们的事务回调不会直接返回任何东西,但是 Go 语言使得直接从闭包获取值变得很容易,所以我们将在基准测试中设置 pizzaTime 标志,当资金不足时:
pizzaTime := false
for i := 0; i < dollarsPerFounder; i++ {
server.Transact(func(fund *Fund) {
if fund.Balance() <= 10 {
// Set it in the outside scope
pizzaTime = true
return
}
fund.Withdraw(1)
})
if pizzaTime {
break
}
}
And check that it works:
并检查它是否有效:
$ GOMAXPROCS=4 go test -bench . funding
BenchmarkWithdrawals-4 5000000 775 ns/op
ok funding 4.637s
You may have spotted an opportunity to clean things up some more now. Since we have a generic Transact
command, we don’t need WithdrawCommand
or BalanceCommand
anymore. We’ll rewrite them in terms of transactions:
你可能已经发现了一个机会来清理更多的东西。 因为我们有一个通用的 Transact 命令,所以我们不再需要 WithdrawCommand 或 balanccommand。 我们将在交易方面重写它们:
func (s *FundServer) Balance() int {
var balance int
s.Transact(func(f *Fund) {
balance = f.Balance()
})
return balance
}
func (s *FundServer) Withdraw(amount int) {
s.Transact(func (f *Fund) {
f.Withdraw(amount)
})
}
Now the only command the server takes is TransactionCommand
, so we can remove the whole interface{}
mess in its implementation, and have it accept only transaction commands:
现在服务器接受的唯一命令是 TransactionCommand,所以我们可以在它的实现中移除整个接口{}的混乱,让它只接受事务命令:
type FundServer struct {
commands chan TransactionCommand
fund *Fund
}
func (s *FundServer) loop() {
for transaction := range s.commands {
// Now we don't need any type-switch mess
transaction.Transactor(s.fund)
transaction.Done <- true
}
}
Much better.
好多了。
There’s a final step we could take here. Apart from its convenience functions for Balance
and Withdraw
, the service implementation is no longer tied to Fund
. Instead of managing a Fund
, it could manage an interface{}
and be used to wrap anything. However, each transaction callback would then have to convert the interface{}
back to a real value:
我们还有最后一步要做。 除了平衡和提款的便利功能外,服务的实施不再与基金挂钩。 与管理基金不同,它可以管理一个接口{}并用于包装任何东西。 然而,每个事务回调都必须将接口{}转换回一个真实的值:
type Transactor func(interface{})
server.Transact(func(managedValue interface{}) {
fund := managedValue.(*Fund)
// Do stuff with fund ...
})
This is ugly and error-prone. What we really want is compile-time generics, so we can “template” out a server for a particular type (like *Fund
).
这是丑陋和容易出错的。 我们真正需要的是编译时泛型,这样我们就可以为特定类型(如 * Fund)提供服务器的"模板"。
Unfortunately, Go doesn’t support generics – yet. It’s expected to arrive eventually, once someone figures out some sensible syntax and semantics for it. In the meantime, careful interface design often removes the need for generics, and when they don’t we can get by with type assertions (which are checked at runtime).
不幸的是,Go 还不支持泛型。 一旦有人为它找出一些合理的语法和语义,它最终会到来。 与此同时,仔细的接口设计通常会消除对泛型的需求,如果不需要泛型,我们可以使用类型断言(在运行时进行检查)。
Yes.
是的。
Well, okay, no.
好吧,没有。
For instance:
例如:
A panic in a transaction will kill the whole service.
交易中的恐慌会扼杀整个服务。
There are no timeouts. A transaction that never returns will block the service forever.
没有暂停。 永远不返回的事务将永远阻塞服务。
If our Fund grows some new fields and a transaction crashes halfway through updating them, we’ll have inconsistent state.
如果我们的基金增加了一些新的字段,并且一个交易在更新它们的过程中中途崩溃了,我们将会有不一致的状态。
Transactions are able to leak the managed Fund
object, which isn’t good.
交易会泄露管理的基金对象,这是不好的。
There’s no reasonable way to do transactions across multiple funds (like withdrawing from one and depositing in another). We can’t just nest our transactions because it would allow deadlocks.
没有合理的方法可以跨多个基金进行交易(比如从一个基金中提款,然后存入另一个基金)。 我们不能仅仅嵌套我们的交易,因为它会允许死锁。
Running a transaction asynchronously now requires a new goroutine and a lot of messing around. Relatedly, we probably want to be able to read the most recent Fund
state from elsewhere while a long-running transaction is in progress.
现在异步运行一个事务需要一个新的系统和大量的混乱。 与此相关的是,我们可能希望在一项长期交易正在进行时,能够从其他地方读取基金的最新状态。
In our next Go programming language tutorial, we’ll look at some ways to address these issues.
在我们的下一个 Go 编程语言教程中,我们将研究一些解决这些问题的方法。