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

Posted slowlydance2me

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言:编写一个 WebsiteRacer 的函数,用来对比请求两个 URL 来「比赛」,并返回先响应的 URL。如果两个 URL 在 10 秒内都未返回结果,返回一个 error。相关的知识,希望对你有一定的参考价值。

问题:

你被要求编写一个叫做 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 语言开发环境搭建及编写第一个Go程序

[译] 使用 Go 语言编写一个简单的 SHELL

Go 语言入门基础语法

每天一点GO语言——Linux环境下安装Go语言环境以及编写Go语言程序初体验

Spark支持通过GO语言编写程序吗

编程实践用 go 语言实现一个SQL DSL