问题:

你被要求编写一个叫做 WebsiteRacer 的函数,用来对比请求两个 URL 来「比赛」,并返回先响应的 URL。如果两个 URL 在 10 秒内都未返回结果,那么应该返回一个 error

 
实现这个功能我们需要用到
  •  net/http 用来调用 HTTP 请求
  •  net/http/httptest 用来测试这些请求
  •  Go 程(goroutines)
  • select

先写测试

我们从最幼稚的做法开头把事情开展起来。
func TestRacer(t *testing.T) {
slowURL := "http://www.facebook.com"
fastURL := "http://www.quii.co.uk" want := fastURL
got := Racer(slowURL, fastURL) if got != want {
t.Errorf("got '%s', want '%s'", got, want)
}
}

我们知道这样不完美并且有问题,但这样可以把事情开展起来。重要的是,不要徘徊在第一次就想把事情做到完美。

尝试运行测试

./racer_test.go:14:9: undefined: Racer

为测试的运行编写最少量的代码,并检查失败测试的输出

func Racer(a, b string) (winner string) {
return
}

racer_test.go:25: got '', want 'http://www.quii.co.uk'

编写足够的代码使程序通过

func Racer(a, b string) (winner string) {
startA := time.Now()
http.Get(a)
aDuration := time.Since(startA) startB := time.Now()
http.Get(b)
bDuration := time.Since(startB) if aDuration < bDuration {
return a
} return b
}
对每个 URL:
  • 1.我们用 time.Now() 来记录请求 URL 前的时间。
  • 2.然后用 http.Get 来请求 URL 的内容。这个函数返回一个 http.Response 和一个 error,但目前我们不关心它们的值。
  • 3.time.Since 获取开始时间并返回一个 time.Duration 时间差。

我们完成这些后就可以通过对比请求耗时来找出最快的了。

问题

这可能会让你的测试通过,也可能不会。问题是我们通过访问真实网站来测试我们的逻辑。
使用 HTTP 测试代码非常常见,Go 标准库有这类工具可以帮助测试。
 在前两章模拟和依赖注入章节中,我们讲到了理想情况下如何不依赖外部服务来进行测试,因为它们可能
  • 速度慢
  •  不可靠
  • 无法进行边界条件测试
在标准库中有一个 net/http/httptest 包,它可以让你轻易建立一个 HTTP 模拟服务器(mock HTTP server)。
我们改为使用模拟测试,这样我们就可以控制可靠的服务器来测试了。
func TestRacer(t *testing.T) {

    slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Millisecond)
w.WriteHeader(http.StatusOK)
})) fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})) slowURL := slowServer.URL
fastURL := fastServer.URL want := fastURL
got := Racer(slowURL, fastURL) if got != want {
t.Errorf("got '%s', want '%s'", got, want)
} slowServer.Close()
fastServer.Close()
}

语法看着有点儿复杂,没关系,慢慢来。

httptest.NewServer 接受一个我们传入的 匿名函数 http.HandlerFunc
http.HandlerFunc 是一个看起来类似这样的类型:type HandlerFunc func(ResponseWriter, *Request)
这些只是说它是一个需要接受一个 ResponseWriterRequest参数的函数,这对于 HTTP 服务器来说并不奇怪。
结果呢,这里并没有什么彩蛋,这也是如何在 Go 语言写一个 真实的 HTTP 服务器的方法。唯一的区别就是我们把它封装成一个易于测试的 httptest.NewServer,它会找一个可监听的端口,然后测试完你就可以关闭它了。
我们让两个服务器中慢的那一个短暂地 time.Sleep 一段时间,当我们请求时让它比另一个慢一些。然后两个服务器都会通过 w.WriteHeader(http.StatusOK) 返回一个 OK 给调用者。
如果你重新运行测试,它现在肯定会通过并且会更快完成。你可以调整 sleep 时间故意破坏测试。
 

重构

我们在主程序代码和测试代码里都有一些重复。

func Racer(a, b string) (winner string) {
aDuration := measureResponseTime(a)
bDuration := measureResponseTime(b) if aDuration < bDuration {
return a
} return b
} func measureResponseTime(url string) time.Duration {
start := time.Now()
http.Get(url)
return time.Since(start)
}

这样简化代码后可以让 Racer 函数更加易读。

func TestRacer(t *testing.T) {

    slowServer := makeDelayedServer(20 * time.Millisecond)
fastServer := makeDelayedServer(0 * time.Millisecond) defer slowServer.Close()
defer fastServer.Close() slowURL := slowServer.URL
fastURL := fastServer.URL want := fastURL
got := Racer(slowURL, fastURL) if got != want {
t.Errorf("got '%s', want '%s'", got, want)
}
} func makeDelayedServer(delay time.Duration) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(delay)
w.WriteHeader(http.StatusOK)
}))
}

我们通过一个名为 makeDelayedServer 的函数重构了模拟服务器,以将一些不感兴趣的代码移出测试并减少了重复代码。

defer

在某个函数调用前加上 defer 前缀会在 包含它的函数结束时 调用它。
有时你需要清理资源,例如关闭一个文件,在我们的案例中是关闭一个服务器,使它不再监听一个端口。
你想让它在函数结束时执行(关闭服务器),但要把它放在你创建服务器语句附近,以便函数内后面的代码仍可以使用这个服务器。
我们的重构是一次改进,并且目前是涵盖 Go 语言特性提供的合理解决方案,但我们可以让它更简单。

进程同步

  • Go 在并发方面很在行,为什么我们要一个接一个地测试哪个网站更快呢?我们应该能够同时测试两个。
  • 我们并不关心请求的 准确响应时间,我们只是需要知道哪个更快返回而已。
 
想实现这个,我们要介绍一个叫 select 的新构造(construct),它可以帮我们轻易清晰地实现进程同步。

func Racer(a, b string) (winner string) {
select {
case <-ping(a):
return a
case <-ping(b):
return b
}
} func ping(url string) chan bool {
ch := make(chan bool)
go func() {
http.Get(url)
ch <- true
}()
return ch
}

ping

我们定义了一个可以创建 chan bool 类型并返回它的 ping 函数。
在这个案例中,我们并不 关心 channel 中发送的类型, 我们只是想发送一个信号 来说明已经发送完了,所以返回 bool 就可以了。
同样在这个函数中,当我们完成 http.Get(url)时启动了一个用来给 channel 发送信号的 Go 程(goroutine)。

select

如果你记得并发那一章的内容,你可以通过 myVar := <-ch 来等待值发送给 channel。这是一个 阻塞 的调用,因为你需要等待值返回。
select 则允许你同时在 多个 channel 等待。第一个发送值的 channel「胜出」,case 中的代码会被执行。
我们在 select 中使用 ping 为两个 URL 设置两个 channel。无论哪个先写入其 channel 都会使 select 里的代码先被执行,这会导致那个 URL 先被返回(胜出)。
做了这些修改后,我们的代码背后的意图就很明确了,实现起来也更简单。
 

超时

 最后的需求是当 Racer 耗时超过 10 秒时返回一个 error。

先写测试

t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
serverA := makeDelayedServer(11 * time.Second)
serverB := makeDelayedServer(12 * time.Second)

defer serverA.Close()
defer serverB.Close()

_, err := Racer(serverA.URL, serverB.URL)

if err == nil {
t.Error("expected an error but didn't get one")
}
})

为了练习这个场景,现在我们要使模拟服务器超过 10 秒后返回两个值,胜出的 URL(这个测试中我们用 _ 忽略掉了)和一个 error

尝试运行测试

./racer_test.go:37:10: assignment mismatch: 2 variables but 1 values

为测试的运行编写最少量的代码,并检查失败测试的输出

func Racer(a, b string) (winner string, error error) {
select {
case <-ping(a):
return a, nil
case <-ping(b):
return b, nil
}
}
修改 Racer 的函数签名来返回胜出者和一个 error。返回 nil 仅用于模拟顺利的场景(happy cases)。
编译器会报怨你的 第一个测试 只期望一个值,所以把这行改为 got, _ := Racer(slowURL, fastURL),要知道顺利的场景中我们不应得到一个 error
 现在运行测试会在超过 11 秒后失败。

--- FAIL: TestRacer (12.00s)
--- FAIL: TestRacer/returns_an_error_if_a_server_doesn't_respond_within_10s (12.00s)
racer_test.go:40: expected an error but didn't get one

编写足够的代码使程序通过

func Racer(a, b string) (winner string, error error) {
select {
case <-ping(a):
return a, nil
case <-ping(b):
return b, nil
case <-time.After(10 * time.Second):
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
}
}
使用 select 时,time.After 是一个很好用的函数。当你监听的 channel 永远不会返回一个值时你可以潜在地编写永远阻塞的代码,尽管在我们的案例中它没有发生。time.After 会在你定义的时间过后发送一个信号给 channel 并返回一个 chan 类型(就像 ping 那样)。
对我们来说这完美了;如果 ab 谁胜出就返回谁,但如果测试达到 10 秒,那么 time.After 会发送一个信号并返回一个 error
 

慢速测试

现在的问题是这个测试要耗时 10 秒以上。对这么简单的逻辑来说可不好。
 我们可以做的就是让超时时间(timeout)可配置,这样测试就可以设置一个非常短的时间,并且代码在真实环境中可以被设置成 10 秒。

func Racer(a, b string, timeout time.Duration) (winner string, error error) {
select {
case <-ping(a):
return a, nil
case <-ping(b):
return b, nil
case <-time.After(timeout):
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
}
}
现在代码不能编译了,因为我们没提供超时时间。
在急于将这个默认值添加到测试前,先让我们 聆听他们
  • 在顺利的情况「happy test」下我们是否关心超时时间?
  • 需求对超时时间很明确

鉴于以上信息,我们再做一次小的重构来让我们的测试和代码的用户合意

var tenSecondTimeout = 10 * time.Second

func Racer(a, b string) (winner string, error error) {
return ConfigurableRacer(a, b, tenSecondTimeout)
} func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) {
select {
case <-ping(a):
return a, nil
case <-ping(b):
return b, nil
case <-time.After(timeout):
return "", fmt.Errorf("timed out waiting for %s and %s", a, b)
}
}

我们的用户和第一个测试可以使用 Racer(使用 ConfigurableRacer),不顺利的场景测试可以使用 ConfigurableRacer

func TestRacer(t *testing.T) {

    t.Run("compares speeds of servers, returning the url of the fastest one", func(t *testing.T) {
slowServer := makeDelayedServer(20 * time.Millisecond)
fastServer := makeDelayedServer(0 * time.Millisecond) defer slowServer.Close()
defer fastServer.Close() slowURL := slowServer.URL
fastURL := fastServer.URL want := fastURL
got, err := Racer(slowURL, fastURL) if err != nil {
t.Fatalf("did not expect an error but got one %v", err)
} if got != want {
t.Errorf("got '%s', want '%s'", got, want)
}
}) t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
server := makeDelayedServer(25 * time.Millisecond) defer server.Close() _, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond) if err == nil {
t.Error("expected an error but didn't get one")
}
})
}

在第一个测试最后加了一个检查来验证我们没得到一个 error

总结

我们学到了什么?

select
  •  可帮助你同时在多个 channel 上等待。
  • 有时你想在你的某个「案例」中使用 time.After
httptest
  •  一种方便地创建测试服务器的方法,这样你就可以进行可靠且可控的测试。
  • 使用和 net/http
 

Go语言:编写一个 WebsiteRacer 的函数,用来对比请求两个 URL 来「比赛」,并返回先响应的 URL。如果两个 URL 在 10 秒内都未返回结果,返回一个 error。的更多相关文章

  1. 转载自 BotVS 「 珍藏版 」如何搭建一个完整的交易框架

    [img]http://dn-filebox.qbox.me/8c218c119046b2a25df2d9c7b00c1e0fa6899bdd.png[/img]NO:01 交易策略 ≠ 交易系统. ...

  2. 微信自动关闭内置浏览器页面,返回公众号窗口 WeixinJSBridge.call('closeWindow')

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  3. 解决WCF“接收对 http://xxx.svc 的 HTTP 响应时发生错误。这可能是由于服务终结点绑定未使用 HTTP 协议造成的。这还可能是由于服务器中止了 HTTP 请求上下文(可能由于服务关闭)所致"

    最近在工作中新加了一个接口,本地调试的时候,直接抛出“接收对 http://xxx.svc 的 HTTP 响应时发生错误.这可能是由于服务终结点绑定未使用 HTTP 协议造成的.这还可能是由于服务器中 ...

  4. Entity Framework 6 Recipes 2nd Edition(11-2)译 -> 为一个”模型定义”函数返回一个计算列

    11-3. 为一个”模型定义”函数返回一个计算列 问题 想从”模型定义”函数里返回一个计算列 解决方案 假设我们有一个员工(Employee)实体,属性有: FirstName, LastName,和 ...

  5. Entity Framework 6 Recipes 2nd Edition(11-6)译 -> 从一个”模型定义”函数里返回一个复杂类型

    11-6.从一个”模型定义”函数里返回一个复杂类型 问题 想要从一个”模型定义”函数返回一个复杂类型 解决方案 假设我们有一个病人(patient)和他们访客(visit)的模型,如 Figure 1 ...

  6. Leaf - 一个由 Go 语言编写的开发效率和执行效率并重的开源游戏服务器框架

    转自:https://toutiao.io/posts/0l7l7n/preview Leaf 游戏服务器框架简介 Leaf 是一个由 Go 语言(golang)编写的开发效率和执行效率并重的开源游戏 ...

  7. 用 C 语言编写一个简单的垃圾回收器

    人们似乎觉得编写垃圾回收机制是非常难的,是一种仅仅有少数智者和Hans Boehm(et al)才干理解的高深魔法.我觉得编写垃圾回收最难的地方就是内存分配,这和阅读K&R所写的malloc例 ...

  8. 用c++语言编写函数 int index(char *s,char * t),返回字符串t在字符串s中出现的最左边的位置,如果s中没有与t匹配的子串,则返回-1。类似于索引的功能。

    首先,分析一下程序的思路: 1:从s的第i个元素开始,与t中的第1个元素匹配,如果相等,则将s的第i+1元素与t中的第2个元素匹配,以此类推,如果t所有元素都匹配,则返回位置i;否则,执行2; 2: ...

  9. 用C语言编写一个简单的词法分析程序

    问题描述: 用C或C++语言编写一个简单的词法分析程序,扫描C语言小子集的源程序,根据给定的词法规则,识别单词,填写相应的表.如果产生词法错误,则显示错误信息.位置,并试图从错误中恢复.简单的恢复方法 ...

  10. linux c语言编写一个shell壳

    目的:我们要用c语言编写一个shell可以运行在linux机器上的. 介绍:shell所在的层次 我们要做的是操作系统,用于用户与操作系统进行交互的myhsell 思路:用户输入  一行字符串,我们先 ...

随机推荐

  1. vue后台管理系统——主页布局

    电商后台管理系统的功能--页面的整体布局 1. 整体布局 整体布局:先上下划分,再左右划分. 需要使用到ElementUI中提供的Container组件 <el-container> &l ...

  2. grafana配置邮箱告警

    [root@localhost grafana]# cd /etc/grafana/ [root@localhost grafana]# vim grafana.ini 注意:发件邮件要开启smtp服 ...

  3. IDEA 2018.3.*本地启动tomcat项目无法设置Application context localhost 404

    记录一个开发中遇到的坑,网上找了好久才找到一个能解决的办法,特此转载一下. 旧版的idea启动web项目,在tomcat配置环节,有设置Application content的功能.我们可以设置成&q ...

  4. Element UI 父组件el-tabel选择某行跳转子组件,在子组件的el-table中选择数组,添加到父组件操作行中

    解决思路: 1.在父组件选择操作某行数据时,将父组件的行号暂存(index). 2.跳转子组件页面,选择某行数据,点击提交将该行数据传递个父组件 3.父组件取到第一步暂存行号(index),将子组件传 ...

  5. 动手搭建ssm框架

    现在很多公司用的开源框架很多都是ssm框架的一个结构,这里我自己试着自己搭一个简单的框架,大家共同学习.下面一起跟着我搭建吧,本人菜鸟,有任何不对的地方有望指出. 框架结构:spring(4.3.9. ...

  6. idea使用mapstruct报错,Internal error in the mapping processor

    错误信息如下: java: Internal error in the mapping processor: java.lang.NullPointerException at org.ma... 修 ...

  7. printf( )和scanf( )

    printf()的转换说明 转换说明 输出 %a,%A 浮点数,十六进制数和p记数法 %c 单个字符 %d.%i 有符号的十进制整数 %e,%E 浮点数,e记数法 %f 浮点数,十进制计数法 %g/% ...

  8. CatDCGAN项目复现与对抗网络初识

    CatDCGAN项目复现与对抗网络初识 作者 CarpVexing 日期 100521 禁止转载 目录 CatDCGAN项目复现与对抗网络初识 引言 CatDCGAN项目基本信息 复现项目的准备工作 ...

  9. java比较器:Comparable和Comparator

    java比较器 Comparable 一.java中对象可以通过==或!=比较地址值是否相同,在开发场景中还需要对对象做出大小比较以排序 需要利用接口Comparable或Comparator Com ...

  10. FIR滤波器的设计

    FIR数字滤波器的设计 线性相位FIR滤波器的特点 单位冲激响应:\(h(n),0\leq n\leq N-1\) 系统函数:\(H(z)=\sum_{n=0}^{N-1}h(n)z^{-n}\) 零 ...