一旦您熟悉了 Go 中并发特性的基本和中间用法,您可能会发现您可以使用双向通道和标准并发工具处理大多数开发用例。
在第 2 章、理解并发模型和第 3 章、开发并发策略中,我们不仅研究了 Go 的并发模型,还研究了其他语言的并发模型,并比较了它们和分布式模型的工作方式。在本章中,我们将讨论这些以及一些与设计和管理并发应用有关的更高级别的概念。
特别是,我们将研究 goroutines 的集中管理及其相关频道,您可能会发现 goroutines 是一个集 it 和遗忘 it 于一身的主张;然而,在某些情况下,我们可能需要对通道的状态进行更精确的控制。
我们还从较高的层次研究了很多测试和基准测试,但我们将研究一些更详细、更复杂的测试方法。我们还将探索谷歌应用引擎的入门知识,这将使我们能够使用一些尚未使用的特定测试工具。
最后,我们将讨论一些通用的 Go 最佳实践,这些实践肯定不仅适用于并发应用设计,而且也适用于您将来使用该语言的总体工作。
我们已经讨论了许多不同类型的通道实现(接口、函数、结构和通道),并讨论了缓冲通道和非缓冲通道的差异。然而,在频道和 Goroutine 的设计和流程方面,我们还有很多事情可以做。
按照设计,Go 希望你让事情保持简单。这对于你将要使用 Go 的 90%来说是非常棒的。但在其他情况下,您需要更深入地挖掘解决方案,或者需要通过保留开放的 goroutine 进程、通道等来节省资源。
在某些情况下,您可能需要一些对大小和状态的控制,以及对正在运行或已关闭的 goroutine 的控制,因此我们将考虑这样做。
同样重要的是,设计 goroutines 与整个应用设计协同工作对单元测试至关重要,这是我们将在最后一章中讨论的主题。
在本书的前面,我们讨论了并发模式和一些 worker。在上一章中,我们甚至在构建我们的日志系统时,将工人的概念引入其中。
真正地说,“工作者”是一个相当普遍和模糊的概念,不仅在Go中,而且在一般的编程和开发中。在某些语言中,它是一个对象/实例化类,而在另一些语言中,它是一个并发参与者。在函数式编程语言中,worker 是传递给另一个函数的分级函数返回。
如果我们回到前言,我们会发现我们已经用 go gopher 作为一个工人的例子。简言之,worker 比单个函数调用或编程操作要复杂得多,它们将执行一个或多个任务。
那我们现在为什么要谈论它呢?当我们建立我们的渠道时,我们正在创建一种工作机制。当我们有一个结构或接口时,我们将在一个地方组合方法和值,然后使用该对象作为工作机制和存储该工作信息的地方进行工作。
这在应用设计中特别有用,因为我们能够将应用功能的各种元素委托给不同的、定义良好的工作人员。例如,考虑一个服务器 PrPing 应用,它有特定的片段以独立的、分块的方式来做特定的事情。
我们将尝试通过 HTTP 包检查服务器可用性,检查状态代码和错误,如果发现任何特定服务器存在问题,请退出。您可能会看到这一点,这是实现负载平衡的最基本方法。但一个重要的设计考虑是我们管理渠道的方式。
我们将有一个主通道,所有重要的全局事务都应该在主通道中进行累积和评估,但每个服务器也将有自己的通道来处理仅对单个结构重要的任务。
以下代码中的设计可以看作是一个基本的管道,它大致类似于我们在前面章节中讨论的生产者/消费者模型:
package main
import
(
"fmt"
"time"
"net/http"
)
const INIT_DELAY = 3000
const MAX_DELAY = 60000
const MAX_RETRIES = 4
const DELAY_INCREMENT = 5000
前面的代码给出了应用的配置部分,设置了检查服务器的频率范围、后退的最长时间以及完全放弃之前的最大重试次数。
DELAY_INCREMENT
值表示每次发现问题时,我们将在服务器检查过程中增加多少时间。下面让我们来看看如何在下面的章节中创建一个服务器
var Servers []Server
type Server struct {
Name string
URI string
LastChecked time.Time
Status bool
StatusCode int
Delay int
Retries int
Channel chan bool
}
现在,我们设计了基本服务器(使用以下代码),其中包含其当前状态、上次检查的时间、检查之间的当前延迟、其自身用于评估状态和建立新状态的通道以及更新的重试延迟:
func (s *Server) checkServerStatus(sc chan *Server) {
var previousStatus string
if s.Status == true {
previousStatus = "OK"
}else {
previousStatus = "down"
}
fmt.Println("Checking Server",s.Name)
fmt.Println("\tServer was",previousStatus,"on last check at",s.LastChecked)
response, err := http.Get(s.URI)
if err != nil {
fmt.Println("\tError: ",err)
s.Status = false
s.StatusCode = 0
}else {
fmt.Println(response.Status)
s.StatusCode = response.StatusCode
s.Status = true
}
s.LastChecked = time.Now()
sc <- s
}
checkServerStatus()
方法是我们在这里应用的肉和土豆。我们通过main()
函数中的此方法将所有服务器传递给cycleServers()
循环,之后它将自我实现。
如果我们的Status
设置为为true
,我们将状态发送到控制台为OK
(否则为down
),如果出现网络或其他错误,则将我们的Server
状态代码设置为s.StatusCode
为 HTTP 代码或0
。
最后,将Server
的最后一次检查时间设置为Now()
并通过serverChan
通道传递Server
。在下面的代码中,我们将演示如何在可用服务器之间轮换:
func cycleServers(sc chan *Server) {
for i := 0; i < len(Servers); i++ {
Servers[i].Channel = make(chan bool)
go Servers[i].updateDelay(sc)
go Servers[i].checkServerStatus(sc)
}
}
这是我们的初始循环,从 main 调用。它只是在我们可用的服务器中循环,初始化其侦听 goroutine 并发送第一个checkServerStatus
请求。
It's worth noting two things here: first, the channel invoked by Server
will never actually die, but instead the application will stop checking the server. That's fine for all practical purposes here, but if we have thousands and thousands of servers to check, we're wasting resources on what essentially amounts to an unclosed channel and a map element that has not been removed. Later, we'll broach the concept of manually killing goroutines, something we've only been able to do through abstraction by stopping the communication channel. Let's now take a look at the following code that controls a server's status and its next steps:
func (s *Server) updateDelay(sc chan *Server) {
for {
select {
case msg := <- s.Channel:
if msg == false {
s.Delay = s.Delay + DELAY_INCREMENT
s.Retries++
if s.Delay > MAX_DELAY {
s.Delay = MAX_DELAY
}
}else {
s.Delay = INIT_DELAY
}
newDuration := time.Duration(s.Delay)
if s.Retries <= MAX_RETRIES {
fmt.Println("\tWill check server again")
time.Sleep(newDuration * time.Millisecond)
s.checkServerStatus(sc)
}else {
fmt.Println("\tServer not reachable after",MAX_RETRIES,"retries")
}
default:
}
}
}
这是每个Server
将监听checkServerStatus()
报告的状态变化的地方。当任何给定的Server
结构接收到一条消息,表明已通过我们的初始循环报告状态的变化时,它将评估该消息并相应地采取行动。
如果Status
设置为false
,我们知道服务器由于某种原因无法访问。Server
引用本身将在下次检查时增加延迟。如果设置为true
,则服务器可访问,延迟将被设置或重置为默认重试值INIT_DELAY
。
在重新初始化自身的checkServerStatus()
方法之前,它最终在该 goroutine 上设置了一个睡眠模式,并在main()
函数中的初始 goroutine 循环中传递serverChan
引用:
func main() {
endChan := make(chan bool)
serverChan := make(chan *Server)
Servers = []Server{ {Name: "Google", URI: "http://www.google.com", Status: true, Delay: INIT_DELAY}, {Name: "Yahoo", URI: "http://www.yahoo.com", Status: true, Delay: INIT_DELAY}, {Name: "Bad Amazon", URI: "http://amazon.zom", Status: true, Delay: INIT_DELAY} }
在我们的Servers
片段中,我们特意在最后一个元素中引入了一个拼写错误。您会注意到amazon.zom
,这将在checkServerStatus()
方法中引发 HTTP 错误。以下是循环浏览服务器以查找适当匹配项的函数:
go cycleServers(serverChan)
for {
select {
case currentServer := <- serverChan:
currentServer.Channel <- false
default:
}
}
<- endChan
}
以下是包含打字错误的输出示例:
Checking Server Google
Server was OK on last check at 0001-01-01 00:00:00 +0000 UTC
200 OK
Will check server again
Checking Server Yahoo
Server was OK on last check at 0001-01-01 00:00:00 +0000 UTC
200 OK
Will check server again
Checking Server Amazon
Server was OK on last check at 0001-01-01 00:00:00 +0000 UTC
Error: Get http://amazon.zom: dial tcp: GetAddrInfoW: No such host is known.
Will check server again
Checking Server Google
Server was OK on last check at 2014-04-23 12:49:45.6575639 -0400 EDT
我们将在本章后面的一些并发模式中使用前面的代码进行最后一次旋转,将其转化为更实用的内容。
在设计管道或生产商/消费者模型时,一个更大的问题是,在任何给定的时间,当它处于任何给定的 goroutine 状态时,都有某种黑洞的存在。
考虑下面的循环,其中生产者渠道创建任意一组消费者渠道,并期望每个人做一件事和唯一一件事:
package main
import (
"fmt"
"time"
)
const CONSUMERS = 5
func main() {
Producer := make(chan (chan int))
for i := 0; i < CONSUMERS; i++ {
go func() {
time.Sleep(1000 * time.Microsecond)
conChan := make(chan int)
go func() {
for {
select {
case _,ok := <-conChan:
if ok {
Producer <- conChan
}else {
return
}
default:
}
}
}()
conChan <- 1
close(conChan)
}()
}
给定随机产生的消费者数量,我们为每个消费者附加一个通道,并通过该消费者的通道向Producer
上游传递一条消息。我们只发送一条消息(我们可以用缓冲通道处理),但我们只是在发送后关闭通道。
无论是在多线程应用、分布式应用还是高度并发应用中,生产者-消费者模型的一个基本属性是数据能够以稳定、可靠的方式跨队列/通道移动。这需要生产者和消费者之间分享一些共同的知识。
与分布式(或多核)环境不同,我们确实对这种安排两端的状态有一些固有的认识。接下来,我们来看看生产者消息的监听循环:
for {
select {
case consumer, ok := <-Producer:
if ok == false {
fmt.Println("Goroutine closed?")
close(Producer)
} else {
log.Println(consumer)
// consumer <- 1
}
fmt.Println("Got message from secondary channel")
default:
}
}
}
主要的问题是Producer
频道之一对任何给定的Consumer
都不太了解,包括它何时在积极运行。如果我们取消对// consumer <- 1
行的注释,我们会感到恐慌,因为我们试图在一个封闭的频道上发送消息。
当一条消息通过辅助 goroutine 的通道(上游到Producer
的通道)传递时,我们得到了适当的接收,但无法检测到下游 goroutine 何时关闭。
知道 GOODUTE 何时终止,在很多情况下是无关紧要的,但是考虑一个应用,它在一定数量的任务完成时产生新的 GORDOTINE,有效地将任务分解成迷你任务。也许每个区块都取决于最后一个区块的总体完成情况,广播公司在继续之前必须知道当前 goroutines 的状态。
在 Go 的早期版本中,您可以跨未初始化的、因此为零或 0 值的通道进行通信,而不会产生恐慌(尽管您的结果是不可预测的)。从 Go 版本 1 开始,跨越 nil 通道的通信产生了一致的,但有时会产生混乱的效果。
必须注意的是,在选择开关中,nil 通道上的传输本身仍会导致死锁和死机。这是在使用全局通道时最常出现的问题,并且永远不会正确初始化它们。以下是在 nil 信道上的这种传输的示例:
func main() {
var channel chan int
channel <- 1
for {
select {
case <- channel:
default:
}
}
}
当通道被设置为其0
值(在本例中为 nil)时,它会永久阻塞,Go 编译器将检测到这一点,至少在较新的版本中是如此。您也可以在select
语句之外复制该语句,如下代码所示:
var done chan int
defer close(done)
defer log.Println("End of script")
go func() {
time.Sleep(time.Second * 5)
done <- 1
}()
for {
select {
case <- done:
log.Println("Got transmission")
return
default:
}
}
由于select
语句中的默认值,在等待通道上的通信时,保持主循环处于活动状态,因此前面的代码将永远阻塞而不会死机。但是,如果我们初始化通道,应用将按预期运行。
对于这两种边缘情况,闭合通道和零通道,我们需要一种主通道了解 goroutine 状态的方法。
正如许多这样的问题一样,无论是利基问题还是普通问题,都存在着一个第三方实用程序来抓住你的goroutines。
坟墓是一个库,它提供了与任何 goroutine 和通道一起运行的诊断,它可以告诉主通道另一个 goroutine 是否死亡或死亡。
此外,它允许您显式地杀死 goroutine,这比简单地关闭它所连接的频道要微妙得多。如前所述,关闭通道实际上是对 goroutine 的阉割,尽管它最终可能仍然处于活动状态。
您将找到一个简单的 fetch-and-grab 主体脚本,该脚本获取 URL 结构的一部分(带有状态和 URI),并尝试获取每个结构的 HTTP 响应,并将其应用于该结构。但是,我们将能够向“主”结构的每个子 goroutine 发送“kill 消息”,而不仅仅是报告 goroutine 中的信息。
在本例中,我们将运行该脚本 10 秒,如果任何 goroutine 未能在分配的时间内完成其工作,它将响应由于调用它的主结构发出的 kill 发送而无法获取 URL 的正文:
package main
import (
"fmt"
"io/ioutil"
"launchpad.net/tomb"
"net/http"
"strconv"
"sync"
"time"
)
var URLS []URL
type GoTomb struct {
tomb tomb.Tomb
}
This is the minimum necessary structure required to create a parent or a master struct for all of your spawned goroutines. The tomb.Tomb
struct is simply a mutex, two channels (one for dead and dying), and a reason error struct. The structure of the URL
struct looks like the following code:
type URL struct {
Status bool
URI string
Body string
}
Our URL
struct is fairly basic—Status
, set to false
by default and true
when the body has been retrieved. It consists of the URI
variable—which is the reference to the URL—and the Body
variable for storing the retrieved data. The following function allows us to execute a "kill" on a GoTomb
struct:
func (gt GoTomb) Kill() {
gt.tomb.Kill(nil)
}
前面的方法在我们的GoTomb
结构上调用tomb.Kill
。在这里,我们将唯一的参数设置为nil
,但这很容易更改为更具描述性的错误,例如errors.New("Time to die, goroutine")
。在这里,我们将显示GoTomb
结构的侦听器:
func (gt *GoTomb) TombListen(i int) {
for {
select {
case <-gt.tomb.Dying():
fmt.Println("Got kill command from tomb!")
if URLS[i].Status == false {
fmt.Println("Never got data for", URLS[i].URI)
}
return
}
}
}
我们调用GoTomb
附带的TombListen
,设置一个监听Dying()
频道的选择,如下代码所示:
func (gt *GoTomb) Fetch() {
for i := range URLS {
go gt.TombListen(i)
go func(ii int) {
timeDelay := 5 * ii
fmt.Println("Waiting ", strconv.FormatInt(int64(timeDelay), 10), " seconds to get", URLS[ii].URI)
time.Sleep(time.Duration(timeDelay) * time.Second)
response, _ := http.Get(URLS[ii].URI)
URLS[ii].Status = true
fmt.Println("Got body for ", URLS[ii].URI)
responseBody, _ := ioutil.ReadAll(response.Body)
URLS[ii].Body = string(responseBody)
}(i)
}
}
When we invoke Fetch()
, we also set the tomb to TombListen()
, which receives those "master" messages across all spawned goroutines. We impose an intentionally long wait to ensure that our last few attempts to Fetch()
will come after the Kill()
command. Finally, our main()
function, which handles the overall setup:
func main() {
done := make(chan int)
URLS = []URL{{Status: false, URI: "http://www.google.com", Body: ""}, {Status: false, URI: "http://www.amazon.com", Body: ""}, {Status: false, URI: "http://www.ubuntu.com", Body: ""}}
var MasterChannel GoTomb
MasterChannel.Fetch()
go func() {
time.Sleep(10 * time.Second)
MasterChannel.Kill()
done <- 1
}()
for {
select {
case <-done:
fmt.Println("")
return
default:
}
}
}
通过将time.Sleep
设置为10
秒,然后杀死我们的 goroutine,我们保证Fetch()
之间的 5 秒延迟会阻止我们的最后一个 goroutine 在被杀死之前成功完成。
对于坟墓包,转到http://godoc.org/launchpad.net/tomb 并使用go get launchpad.net/tomb
命令进行安装。
通道和select
循环的一个关键点是,我们没有特别仔细地研究,即在某个超时后杀死select
循环的能力和必要性。
到目前为止,我们编写的许多应用都是长期运行或永久运行的,但有时我们希望对 goroutines 的运行时间设置一个有限的时间限制。
到目前为止,我们使用的for { select { } }
开关要么永久存在(默认情况下),要么等待从一个或多个情况中断开。
毫不奇怪,有两种方法可以管理基于时间间隔的任务,这两种方法都是时间包的一部分。
The time.Ticker
struct allows for any given operation after the specified period of time. It provides C, a blocking channel that can be used to detect activity sent after that period of time; refer to the following code:
package main
import (
"log"
"time"
)
func main() {
timeout := time.NewTimer(5 * time.Second)
defer log.Println("Timed out!")
for {
select {
case <-timeout.C:
return
default:
}
}
}
我们可以在一定时间后将其扩展到终端通道和并发执行。请看以下修改:
package main
import (
"fmt"
"time"
)
func main() {
myChan := make(chan int)
go func() {
time.Sleep(6 * time.Second)
myChan <- 1
}()
for {
select {
case <-time.After(5 * time.Second):
fmt.Println("This took too long!")
return
case <-myChan:
fmt.Println("Too little, too late")
}
}
}
当我们在本章前面构建我们的服务器 ping 应用时,很容易想象将其带到一个更有用、更有价值的空间。
ping 服务器通常是负载平衡器健康检查的第一步。正如 Go 提供了一个可用的现成 web 服务器解决方案一样,它还提供了一个非常干净的Proxy
和ReverseProxy
结构和方法,这使得创建负载平衡器变得非常简单。
Of course, a round-robin load balancer will need a lot of background work, specifically on checking and rechecking as it changes the ReverseProxy
location between requests. We'll handle these with the goroutines triggered with each request.
Finally, note that we have some dummy URLs at the bottom in the configuration—changing those to production URLs should immediately turn the server that runs this into a working load balancer. Let's look at the main setup for the application:
package main
import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"time"
)
const MAX_SERVER_FAILURES = 10
const DEFAULT_TIMEOUT_SECONDS = 5
const MAX_TIMEOUT_SECONDS = 60
const TIMEOUT_INCREMENT = 5
const MAX_RETRIES = 5
在前面的代码中,我们定义了常量,与前面的代码非常相似。我们有一个MAX_RETRIES
,它限制了我们可以有多少故障,MAX_TIMEOUT_SECONDS
定义了我们在重试之前等待的最长时间,以及我们的TIMEOUT_INCREMENT
用于在故障之间更改该值。接下来,让我们看看我们的Server
结构的基本构造:
type Server struct {
Name string
Failures int
InService bool
Status bool
StatusCode int
Addr string
Timeout int
LastChecked time.Time
Recheck chan bool
}
正如我们在前面的代码中所看到的,我们有一个通用的Server
结构,它维护当前状态、最后一个状态代码以及上次检查服务器时的信息。
Note that we also have a Recheck
channel that triggers the delayed attempt to check the Server
again for availability. Each Boolean passed across this channel will either remove the server from the available pool or reannounce that it is still in service:
func (s *Server) serverListen(serverChan chan bool) {
for {
select {
case msg := <-s.Recheck:
var statusText string
if msg == false {
statusText = "NOT in service"
s.Failures++
s.Timeout = s.Timeout + TIMEOUT_INCREMENT
if s.Timeout > MAX_TIMEOUT_SECONDS {
s.Timeout = MAX_TIMEOUT_SECONDS
}
} else {
if ServersAvailable == false {
ServersAvailable = true
serverChan <- true
}
statusText = "in service"
s.Timeout = DEFAULT_TIMEOUT_SECONDS
}
if s.Failures >= MAX_SERVER_FAILURES {
s.InService = false
fmt.Println("\tServer", s.Name, "failed too many times.")
} else {
timeString := strconv.FormatInt(int64(s.Timeout), 10)
fmt.Println("\tServer", s.Name, statusText, "will check again in", timeString, "seconds")
s.InService = true
time.Sleep(time.Second * time.Duration(s.Timeout))
go s.checkStatus()
}
}
}
}
This is the instantiated method that listens on each server for messages delivered on the availability of a server at any given time. While running a goroutine, we keep a perpetually listening channel open to listen to Boolean responses from checkStatus()
. If the server is available, the next delay is set to default; otherwise, TIMEOUT_INCREMENT
is added to the delay. If the server has failed too many times, it's taken out of rotation by setting its InService
property to false
and no longer invoking the checkStatus()
method. Let's next look at the method for checking the present status of Server
:
func (s *Server) checkStatus() {
previousStatus := "Unknown"
if s.Status == true {
previousStatus = "OK"
} else {
previousStatus = "down"
}
fmt.Println("Checking Server", s.Name)
fmt.Println("\tServer was", previousStatus, "on last check at", s.LastChecked)
response, err := http.Get(s.Addr)
if err != nil {
fmt.Println("\tError: ", err)
s.Status = false
s.StatusCode = 0
} else {
s.StatusCode = response.StatusCode
s.Status = true
}
s.LastChecked = time.Now()
s.Recheck <- s.Status
}
基于服务器 ping 示例,我们的checkStatus()
方法看起来应该很熟悉。我们寻找服务器;如果可用,我们将true
传递给我们的Recheck
频道;否则false
,如下代码所示:
func healthCheck(sc chan bool) {
fmt.Println("Running initial health check")
for i := range Servers {
Servers[i].Recheck = make(chan bool)
go Servers[i].serverListen(sc)
go Servers[i].checkStatus()
}
}
我们的healthCheck
功能只是启动每个服务器检查(并重新检查)其状态的循环。它只运行一次,并通过make
语句初始化Recheck
通道:
func roundRobin() Server {
var AvailableServer Server
if nextServerIndex > (len(Servers) - 1) {
nextServerIndex = 0
}
if Servers[nextServerIndex].InService == true {
AvailableServer = Servers[nextServerIndex]
} else {
serverReady := false
for serverReady == false {
for i := range Servers {
if Servers[i].InService == true {
AvailableServer = Servers[i]
serverReady = true
}
}
}
}
nextServerIndex++
return AvailableServer
}
roundRobin
函数首先检查队列中下一个可用的Server
,如果该服务器发生故障,则循环通过剩余的找到第一个可用的Server
。如果它通过所有循环,它将重置为0
。让我们看看全局配置变量:
var Servers []Server
var nextServerIndex int
var ServersAvailable bool
var ServerChan chan bool
var Proxy *httputil.ReverseProxy
var ResetProxy chan bool
这些是我们的全局变量,Server
结构的Servers
部分,nextServerIndex
变量,用于增加下一个要返回的Server
、ServersAvailable
和ServerChan
,只有在可行的服务器可用后才启动负载平衡器,然后是Proxy
变量,它们告诉我们的http
去哪里。这需要一个ReverseProxy
方法,我们现在将在下面的代码中查看该方法:
func handler(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
Proxy = setProxy()
return func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = "/"
p.ServeHTTP(w, r)
}
}
请注意,我们在这里操作的是一个ReverseProxy
结构,这与我们之前对网页服务的尝试不同。我们的下一个函数执行循环并获取下一个可用服务器:
func setProxy() *httputil.ReverseProxy {
nextServer := roundRobin()
nextURL, _ := url.Parse(nextServer.Addr)
log.Println("Next proxy source:", nextServer.Addr)
prox := httputil.NewSingleHostReverseProxy(nextURL)
return prox
}
每次请求后都会调用setProxy
函数,您可以将其视为处理程序中的第一行。接下来,我们有一个通用的监听功能,用于查找我们将反向代理的请求:
func startListening() {
http.HandleFunc("/index.html", handler(Proxy))
_ = http.ListenAndServe(":8080", nil)
}
func main() {
nextServerIndex = 0
ServersAvailable = false
ServerChan := make(chan bool)
done := make(chan bool)
fmt.Println("Starting load balancer")
Servers = []Server{{Name: "Web Server 01", Addr: "http://www.google.com", Status: false, InService: false}, {Name: "Web Server 02", Addr: "http://www.amazon.com", Status: false, InService: false}, {Name: "Web Server 03", Addr: "http://www.apple.zom", Status: false, InService: false}}
go healthCheck(ServerChan)
for {
select {
case <-ServerChan:
Proxy = setProxy()
startListening()
return
}
}
<-done
}
有了这个应用,我们就有了一个简单但可扩展的负载平衡器,它可以和 Go 中常见的核心组件一起工作。它的并发性特性使它保持精简和快速,我们使用标准 Go 编写了少量代码。
For the purpose of simplicity, we've designed most of our applications and sample code with bidirectional channels, but of course any channel can be set unidirectionally. This essentially turns a channel into a "read-only" or "write-only" channel.
如果您想知道,当一个通道不能节省任何资源或保证出现问题时,为什么要费心限制它的方向,原因可以归结为代码的简单性和限制恐慌的可能性。
By now we know that sending data on a closed channel results in a panic, so if we have a write-only channel, we'll never accidentally run into that problem in the wild. Much of this can also be mitigated with WaitGroups
, but in this case that's a sledgehammer being used on a nail. Consider the following loop:
const TOTAL_RANDOMS = 100
func concurrentNumbers(ch chan int) {
for i := 0; i < TOTAL_RANDOMS; i++ {
ch <- i
}
}
func main() {
ch := make(chan int)
go concurrentNumbers(ch)
for {
select {
case num := <- ch:
fmt.Println(num)
if num == 98 {
close(ch)
}
default:
}
}
}
由于我们在 goroutine 完成之前突然关闭ch
通道一位,因此对它的任何写入都会导致运行时错误。
在本例中,我们调用的是一个只读命令,但它位于select
循环中。通过只允许在单向通道上发送特定操作,我们可以进一步保护这一点。此应用将始终工作到通道中提前关闭的点,比TOTAL_RANDOMS
常数少一个。
当我们限制我们通道的方向或读/写能力时,如果我们的一个或多个进程无意中在这样一个通道上发送数据,我们还可以减少闭合通道死锁的可能性。
因此,“什么时候使用单向通道合适?”这个问题的简短答案是“只要可以”
不要强制解决问题,但如果您可以将通道设置为只读/写,它可能会抢先解决问题。
一个经常派上用场的技巧,我们还没有解决,就是拥有一个有效的无类型通道的能力。
如果您想知道为什么这可能有用,那么简单的答案是简洁的代码和应用设计。通常,这是一种令人沮丧的策略,但您可能会发现它有时很有用,尤其是当您需要通过单个渠道传达一个或多个不同的概念时。以下是不确定通道类型的示例:
package main
import (
"fmt"
"time"
)
func main() {
acceptingChannel := make(chan interface{})
go func() {
acceptingChannel <- "A text message"
time.Sleep(3 * time.Second)
acceptingChannel <- false
}()
for {
select {
case msg := <- acceptingChannel:
switch typ := msg.(type) {
case string:
fmt.Println("Got text message",typ)
case bool:
fmt.Println("Got boolean message",typ)
if typ == false {
return
}
default:
fmt.Println("Some other type of message")
}
default:
}
}
<- acceptingChannel
}
与许多基本和中间开发以及部署需求一样,Go 附带了一个用于处理单元测试的内置应用。
测试背后的基本前提是创建包,然后创建一个测试包以针对初始应用运行。以下是一个非常基本的示例:
mathematics.go
package mathematics
func Square(x int) int {
return x * 3
}
mathematics_test.go
package mathematics
import
(
"testing"
)
func Test_Square_1(t *testing.T) {
if Square(2) != 4 {
t.Error("Square function failed one test")
}
}
在该子目录中进行一次简单的 Go 测试将为您提供所需的响应。虽然这是公认的简单且有缺陷的,但您可能会看到分解代码并进行增量测试是多么容易。这就足够进行开箱即用的基本单元测试了。
纠正这一点将相当简单,同样的测试将通过以下代码:
func Square(x int) int {
return x * x
}
测试包有一定的局限性;但是,由于它提供了基本的通过/失败,因此无法进行断言。有两个第三方软件包可以介入并在这方面提供帮助,我们将在以下部分中探讨它们。
GoCheck主要通过断言和验证对基本测试包进行扩展。您还可以从中获得一些基本的基准测试工具,这些工具的工作原理比使用 Go 进行工程设计所需的任何工具都要简单一些。
有关 GoCheck 的更多详细信息请访问http://labix.org/gocheck 并使用go get gopkg.in/check.v1
进行安装。
与 GoCheck 不同,银杏(及其依赖性 Gomega)采用不同的测试方法,利用行为驱动开发(BDD模型)。行为驱动开发是一种通用模型,用于确保您的应用在每一步都能完成它应该做的事情,银杏将形式化为一些易于解析的属性。
BDD tends to complement test-driven development (for example, unit testing) rather than replacement. It seeks to answer a few critical questions about the way people (or other systems) will interact with your application. In that sense, we'll generally describe a process and what we expect from that process in fairly human-friendly terms. The following is a short snippet of such an example:
Describe("receive new remote TCP connection", func() {
Context("user enters a number", func() {
It("should be an integer", func() {
})
})
})
这使得测试与单元测试一样精细,但也扩展了我们在详细和明确的行为中处理应用使用的方式。
如果您或您的组织对 BDD 感兴趣,那么这是一个非常棒的、成熟的包,用于实现更深入的单元测试。
有关银杏的更多信息,请访问https://github.com/onsi/ginkgo 并使用go get github.com/onsi/ginkgo/ginkgo
进行安装。
有关依赖关系的更多信息,请参阅go get github.com/onsi/gomega
。
If you're unfamiliar with Google App Engine, the short version is it's a cloud environment that allows for simple building and deployment of Platform-As-A-Service (paas) solutions.
Compared to a lot of similar solutions, Google App Engine allows you to build and test your applications in a very simple and straightforward way. Google App Engine allows you to write and deploy in Python, Java, PHP, and of course, Go.
在大多数情况下,Google App Engine 提供了一个标准的 Go 安装,可以很容易地与http
包相吻合。但它也为您提供了一些值得注意的附加软件包,这些软件包是谷歌应用引擎本身所独有的:
Package
|
描述
|
| --- | --- |
| appengine/memcache
| This provides a distributed memcache installation unique to Google App Engine |
| appengine/mail
| 此允许您通过 SMTP 式平台发送电子邮件 |
| appengine/log
| 考虑到您的存储在这里可能更短暂,它将日志的云版本形式化 |
| appengine/user
| 此同时打开标识和 OAuth 功能 |
| appengine/search
| 这让您的应用通过数据存储对您自己的数据进行谷歌搜索 |
| appengine/xmpp
| This provides Google Chat-like capabilities |
| appengine/urlfetch
| 这是一个爬虫功能 |
| appengine/aetest
| 此扩展了 Google App Engine 的单元测试 |
虽然 Go 仍然被认为是 Google App Engine 的测试版,但你可以预期,如果有人能够胜任地在云环境中部署它,那就是 Google。
当谈到最佳实践时,Go的奇妙之处在于,即使你不一定把每件事都做好,Go也会对你大喊大叫,或者为你提供必要的工具来修复它。
如果您试图包含代码而不使用它,或者如果您试图初始化变量而不使用它,Go 将阻止您。如果要清除代码的格式,请使用go fmt
启用它。
从头开始构建包时,最简单的一件事就是以惯用的方式构造代码目录。新包的标准类似于以下代码:
/projects/
thisproject/
bin/
pkg/
src/
package/
mypackage.go
这样设置 Go 代码不仅对您自己的组织有帮助,而且可以让您更轻松地分发包。
对于任何在公司或协作编码环境中工作的人员来说,文档是神圣不可侵犯的。您可能还记得,使用godoc
命令可以在命令行或通过临时本地主机服务器快速获取有关包的信息。以下是您可以使用godoc
的两种基本方式:
使用 godoc
|
描述
|
| --- | --- |
| godoc fmt
| 这会将fmt
文档带到屏幕上 |
| godoc -http=:3000
| 它承载端口:3030
上的文档 |
Go让编写代码变得超级容易,你绝对应该这样做。只需在每个标识符(包、类型或函数)上方添加单行注释,即可将其附加到上下文文档中,如下代码所示:
// A demo documentation package
package documentation
// The documentation struct object
// Chapter int represents a document's chapter
// Content represents the text of the documentation
type Documentation struct {
Chapter int
Content string
}
// Display() outputs the content of any given Document by chapter
func (d Documentation) Display() {
}
安装后,此将允许任何人在您的软件包上运行godoc
文档,并获得您愿意提供的尽可能多的详细信息。
在 Go 核心代码本身中,您经常会看到更健壮的例子,值得回顾一下,将您的文档风格与 Google 和 Go 社区的文档风格进行比较。
假设您以与前面列出的组织技术一致的方式保存代码,那么通过代码存储库和主机提供代码应该是轻而易举的事。
使用 GitHub 作为标准,我们可以这样设计第三方应用:
- 确保您坚持以前的结构格式。
- 将源文件保存在它们将远程驻留的目录结构下。换句话说,本地结构将反映远程结构。
- 很明显,只提交您希望在远程存储库中共享的文件。
假设您的存储库是公共的,那么任何人都应该能够获取(go get
,然后安装(go install
您的软件包。
最后一点考虑到本书的上下文,如果您要构建将要导入的单独包,那么可能会显得有些不合适,请尽可能避免包含并发代码。
这不是一个硬性的规则,但是当你考虑潜在的用法时,让主应用处理并发性是有意义的,除非你的包绝对需要它。这样做将防止许多隐藏的、难以调试的行为,这些行为可能会降低您的库的吸引力。
我真诚地希望,通过这本书,您能够探索、理解和利用 Go 强大能力的深度。
我们已经讨论了很多,从最基本的、无通道的并发 goroutine 到复杂的通道类型、并行性和分布式计算,我们在每一步都带来了一些示例代码。
到目前为止,您应该已经完全准备好以一种高度并发、快速和无错误的方式在代码中构建您内心想要的任何东西。除此之外,您应该能够生成格式良好、结构正确、文档化的代码,供您、您的组织或其他人使用,以便在最佳利用的地方实现并发。
并发本身是一个模糊的概念;对于不同的人(以及跨多种语言的人)来说,这意味着略有不同,但核心目标始终是快速、高效和可靠的代码,可以为任何应用提供性能提升。
在充分了解 GO 的并发实现以及其内部工作的情况下,我希望随着语言的发展和发展,您将继续您的旅程,并同样恳请您考虑 GO 项目本身在开发过程中的贡献。