点击上方蓝色“Go语言中文网”关注我们,设个星标,每天学习 Go 语言

你可以在这里查看本章的所有代码[1]

按照传统,我们学习新语言编写的第一个程序都是 Hello,world。

在之前的章节[2]中,我们讨论了 Go 死板的文件存放位置。

按照以下路径创建目录 $GOPATH/src/github.com/{your-user-id}/hello

如果你使用的是基于 Unix 的系统,你的名字是「bob」并且很乐于遵循 Go 关于 $GOPATH 的约定(这是最简单的设置方式)。那么你可以执行以下命令 mkdir -p $GOPATH/src/github.com/bob/hello 快速创建目录。

在该目录下创建一个 hello.go 的文件并写入以下代码,键入 go run hello.go 来运行程序。

package main

import "fmt"

func main() {    fmt.Println("Hello, world")}

它是如何运行的

当你使用 Go 编写程序时,你将定义一个 main 包,并在其中定义一个 main 函数。包是一种将相关的 Go 代码组合到一起的方式。

func 关键字通过一个名称和函数体来定义函数。

通过 import "fmt" 导入一个包含 Println 函数的包,我们用它来打印输出。

如何测试

你会如何测试这个程序?将你「领域」内的代码和外部世界(会引起副作用)分离开会更好。fmt.Println 会产生副作用(打印到标准输出),我们发送的字符串在自己的领域内。[^注1]

[^注1]: 原文: How do you test this? It is good to separate your "domain" code from the outside world (side-effects). The fmt.Println is a side effect (printing to stdout) and the string we send in is our domain.

所以为了更容易测试,我们把这些问题拆分开。

package main

import "fmt"

func Hello() string {return "Hello, world"}

func main() {    fmt.Println(Hello())}

我们再次使用 func 创建了一个新函数,但是这次我们在定义中添加了另一个关键字 string。这意味着这个函数返回一个字符串。

现在创建一个名为 hello_test.go 的新文件,我们将在这里为 Hello 函数编写一个测试

package main

import "testing"

func TestHello(t *testing.T) {    got := Hello()    want := "Hello, world"

if got != want {        t.Errorf("got '%s' want '%s'", got, want)    }}

在解释之前,让我们先运行一下代码。在终端运行 go test,它应该已经通过了!为了检验测试,可以尝试通过改变 want 字符串来破坏测试。

注意,你不必在多个测试框架之间进行选择,然后理解如何安装它们。你需要的一切都内建在语言中,语法与你将要编写的其余代码相同。

编写测试

编写测试和写函数很类似,其中有一些规则

  • 程序需要在一个名为 xxx_test.go 的文件中编写

  • 测试函数的命名必须以单词 Test 开始

  • 测试函数只接受一个参数 t *testing.T

现在这些信息足以让我们明白,类型为 *testing.T 的变量 t 是你在测试框架中的 "hook"(钩子),所以当你想让测试失败时可以执行 t.Fail() 之类的操作。

我们再讨论一些新的话题:

if

Go 的 if 语句非常类似于其他编程语言。

声明变量

我们使用 varName := value 的语法声明变量,它允许我们在测试中重用一些值使代码更具可读性。

t.Errorf

我们调用 t 的 Errorf 方法打印一条消息并使测试失败。f 表示格式化,它允许我们构建一个字符串,并将值插入占位符值 %s 中。当你测试失败时,它能够让你清楚测试是如何运行的。

稍后我们将探讨方法和函数之间的区别。

Go 文档

Go 的另一个高质量特征是文档。通过运行 godoc -http :8000,可以在本地启动文档。如果你访问 localhost:8000/pkg[3],将看到系统上安装的所有包。

大多数标准库都有优秀的文档和示例。浏览 http://localhost:8000/pkg/testing/[4] 是非常值得的,去看一下你有什么可以用的。

Hello, YOU

现在有了测试,就可以安全地迭代我们的软件了。

在上一个示例中,我们在写好代码 之后 编写了测试,以便你学会如何编写测试和声明函数。从此刻起,我们将 首先编写测试

我们的下一个需求是让我们指定问候的接受者。

让我们从在测试中捕获这些需求开始。这是基本的测试驱动开发,可以确保我们的测试用例 确实 是测试我们想要的。当你回顾编写测试时,存在一个风险:即使代码没有按照预期工作,测试也可能继续通过。

package main

import "testing"

func TestHello(t *testing.T) {    got := Hello("Chris")    want := "Hello, Chris"

if got != want {        t.Errorf("got '%s' want '%s'", got, want)    }}

这时运行 go test,你应该会获得一个编译错误

./hello_test.go:6:18: too many arguments in call to Hello    have (string)    want ()

当使用像 Go 这样的静态类型语言时,聆听编译器 是很重要的。编译器理解你的代码应该如何拼接到一起工作,所以你就不必关心这些了。

在这种情况下,编译器告诉你需要怎么做才能继续。我们必须修改函数 Hello 来接受一个参数。

修改 Hello 函数以接受字符串类型的参数

func Hello(name string) string {return "Hello, world"}

如果你尝试再次运行测试,main.go 将无法编译,因为你没有传递参数。传递参数 "world" 让它通过。

func main() {    fmt.Println(Hello("world"))}

现在,当你运行测试时,你应该看到类似的内容

hello_test.go:10: got 'Hello, world' want 'Hello, Chris''

我们最终得到了一个可编译的程序,但是根据测试它并没有达到我们的要求。

为了使测试通过,我们使用 name 参数并用 Hello, 字符串连接它,

func Hello(name string) string {return "Hello, " + name}

现在再运行测试就应该通过了。通常作为 TDD 周期的一部分,我们该着手 重构 了。

关于版本控制的一点说明

此时,如果你正在使用版本控制(你应该这样做!)我将按原样 提交 代码。因为我们拥有一个测试支持的可用软件。

不过我不会推送到主分支上,因为我下一步计划重构。现在提交很合适,当重构中陷入混乱时你总是可以回到可用版本。

这里没有太多可重构的,但我们可以介绍一下另一种语言特性 常量

常量

通常我们这样定义一个常量

const helloPrefix = "Hello, "

现在我们可以重构代码

const helloPrefix = "Hello, "

func Hello(name string) string {return helloPrefix + name}

重构之后,重新测试,以确保没有破坏任何东西。

常量应该可以提高应用程序的性能,它避免了每次调用 Hello 时创建 "Hello, " 字符串实例。

显然,对于这个例子来说,性能提升是微不足道的!但是创建常量的价值是可以快速理解值的含义,有时还可以帮助提高性能。

再次回到 Hello, world

下一个需求是当我们的函数用空字符串调用时,它默认为打印 "Hello, World" 而不是 "Hello, "

首先编写一个新的失败测试

func TestHello(t *testing.T) {

    t.Run("saying hello to people", func(t *testing.T) {        got := Hello("Chris")        want := "Hello, Chris"

if got != want {            t.Errorf("got '%s' want '%s'", got, want)        }    })

    t.Run("say hello world when an empty string is supplied", func(t *testing.T) {        got := Hello("")        want := "Hello, World"

if got != want {            t.Errorf("got '%s' want '%s'", got, want)        }    })

}

这里我们将介绍测试库中的另一个工具 -- 子测试。有时,对一个「事情」进行分组测试,然后再对不同场景进行子测试非常有效。

这种方法的好处是,你可以建立在其他测试中也能够使用的共享代码。

当我们检查信息是否符合预期时,会有重复的代码。

重构不 仅仅 是针对程序的代码!

重要的是,你的测试 清楚地说明 了代码需要做什么。

我们可以并且应该重构我们的测试。

func TestHello(t *testing.T) {

    assertCorrectMessage := func(t *testing.T, got, want string) {        t.Helper()if got != want {            t.Errorf("got '%s' want '%s'", got, want)        }    }

    t.Run("saying hello to people", func(t *testing.T) {        got := Hello("Chris")        want := "Hello, Chris"        assertCorrectMessage(t, got, want)    })

    t.Run("empty string defaults to 'world'", func(t *testing.T) {        got := Hello("")        want := "Hello, World"        assertCorrectMessage(t, got, want)    })

}

我们在这里做了什么?

我们将断言重构为函数。这减少了重复并且提高了测试的可读性。在 Go 中,你可以在其他函数中声明函数并将它们分配给变量。你可以像调用普通函数一样调用它们。我们需要传入 t *testing.T,这样我们就可以在需要的时候令测试代码失败。

t.Helper() 需要告诉测试套件这个方法是辅助函数(helper)。通过这样做,当测试失败时所报告的行号将在函数调用中而不是在辅助函数内部。这将帮助其他开发人员更容易地跟踪问题。如果你仍然不理解,请注释掉它,使测试失败并观察测试输出。

现在我们有了一个写得很好的失败测试,让我们使用 if 修复代码。

const helloPrefix = "Hello, "

func Hello(name string) string {if name == "" {        name = "World"    }return helloPrefix + name}

如果我们运行测试,应该看到它满足了新的要求,并且我们没有意外地破坏其他功能。

回到版本控制

现在我们对代码很满意,我将修改之前的提交,所以我们只提交认为好的版本及其测试。

规律

让我们再次回顾一下这个周期

  • 编写一个测试

  • 让编译通过

  • 运行测试,查看失败原因并检查错误消息是很有意义的

  • 编写足够的代码以使测试通过

  • 重构

从表面上看,这可能看起来很乏味,但坚持反馈循环非常重要。

它不仅确保你有 相关的测试,还可以确保你通过重构测试的安全性来 设计优秀的软件

查看测试失败是一个重要的检查手段,因为它还可以让你看到错误信息。作为一名开发人员,如果测试失败时不能清楚地说明问题所在,那么使用这个代码库可能会非常困难。

通过确保你的测试 快速 并建立你的工具,以便运行测试足够简单,你在编写代码时就可以进入流畅的状态。

如果不写测试,你提交的时候通过运行软件来手动检查你的代码,这会打破你的流畅状态,而且你任何时候都无法将自己从这种状态中拯救出来,尤其是从长远来看。

继续前进!更多需求

天呐,我们有更多的需求了。我们现在需要支持第二个参数,指定问候的语言。如果一种我们不能识别的语言被传进来,就默认为英语。

通过 TDD 轻松实现这一功能,我们是有信心的!

为使用西班牙语的用户编写测试,将其添加到现有的测试用例中。

    t.Run("in Spanish", func(t *testing.T) {        got := Hello("Elodie", "Spanish")        want := "Hola, Elodie"        assertCorrectMessage(t, got, want)    })

记住不要欺骗自己!先编写测试。当你尝试运行测试时,编译器 应该 会出错,因为你用两个参数而不是一个来调用 Hello

./hello_test.go:27:19: too many arguments in call to Hello    have (string, string)    want (string)

通过向 Hello 添加另一个字符串参数来修复编译问题

func Hello(name string, language string) string {if name == "" {        name = "World"    }return helloPrefix + name}

当你尝试再次运行测试时,它会抱怨在其他测试和 main.go 中没有传递足够的参数给 Hello

./hello.go:15:19: not enough arguments in call to Hello    have (string)    want (string, string)

通过传递空字符串来修复它们。现在,除了我们的新场景外,你的所有测试都应该编译并通过

hello_test.go:29: got 'Hola, Elodie' want 'Hello, Elodie'

这里我们可以使用 if 检查语言是否是「西班牙语」,如果是就修改信息

func Hello(name string, language string) string {if name == "" {        name = "World"    }

if language == "Spanish" {return "Hola, " + name    }

return helloPrefix + name}

测试现在应该通过了。

现在是 重构 的时候了。你应该在代码中看出了一些问题,其中有一些重复的「魔术」字符串。自己尝试重构它,每次更改都要重新运行测试,以确保重构不会破坏任何内容。

const spanish = "Spanish"const helloPrefix = "Hello, "const spanishHelloPrefix = "Hola, "

func Hello(name string, language string) string {if name == "" {        name = "World"    }

if language == spanish {return spanishHelloPrefix + name    }

return helloPrefix + name}

法语

  • 编写一个测试,断言如果你传递 "French" 你会得到 "Bonjour, "

  • 看到它失败,检查易读的错误消息

  • 在代码中进行最小的合理更改

你可能写了一些看起来大致如此的东西

func Hello(name string, language string) string {if name == "" {        name = "World"    }

if language == spanish {return spanishHelloPrefix + name    }

if language == french {return frenchHelloPrefix + name    }

return helloPrefix + name}

switch

当你有很多 if 语句检查一个特定的值时,通常使用 switch 语句来代替。如果我们希望稍后添加更多的语言支持,我们可以使用 switch 来重构代码,使代码更易于阅读和扩展。

func Hello(name string, language string) string {if name == "" {        name = "World"    }

    prefix := helloPrefix

switch language {case french:        prefix = frenchHelloPrefixcase spanish:        prefix = spanishHelloPrefix    }

return prefix + name}

编写一个测试,添加用你选择的语言写的问候,你应该可以看到扩展这个 神奇 的函数是多么简单。

最后一次重构?

你可能会抱怨说也许我们的函数正在变得很臃肿。对此最简单的重构是将一些功能提取到另一个函数中。

func Hello(name string, language string) string {if name == "" {        name = "World"    }

return greetingPrefix(language) + name}

func greetingPrefix(language string) (prefix string) {switch language {case french:        prefix = frenchHelloPrefixcase spanish:        prefix = spanishHelloPrefixdefault:        prefix = englishPrefix    }return}

一些新的概念:

  • 在我们的函数签名中,我们使用了 命名返回值(prefix string)。

  • 这将在你的函数中创建一个名为 prefix 的变量。

    • 你只需调用 return 而不是 return prefix 即可返回所设置的值。

    • 它将被分配「零」值。这取决于类型,例如 int 是 0,对于字符串它是 ""

    • 这将显示在 Go Doc 中,所以它使你的代码更加清晰。

  • 如果没有其他 case 语句匹配,将会执行 default 分支。

  • 函数名称以小写字母开头。在 Go 中,公共函数以大写字母开始,私有函数以小写字母开头。我们不希望我们算法的内部结构暴露给外部,所以我们将这个功能私有化。

总结

谁会知道你可以从 Hello, world 中学到这么多东西呢?

现在你应该以下内容有了一定的理解:

Go 的一些语法

  • 编写测试

  • 用参数和返回类型声明函数

  • ifelseswitch

  • 声明变量和常量

TDD 过程以及步骤的重要性

  • 编写一个失败的测试,并查看失败信息,可以看到我们已经为需求写了一个 相关 的测试,并且看到它产生了一个 易于理解的失败描述

  • 编写最少量的代码以使其通过,因此我们知道我们有可工作软件

  • 然后 重构,支持我们测试的安全性,以确保我们拥有易于使用的精心制作的代码

在我们的例子中,我们通过小巧易懂的步骤从 Hello() 到 Hello("name"),到 Hello("name", "french")

与「现实世界」的软件相比,这当然是微不足道的,但原则依然通用。TDD 是一门需要通过开发去实践的技能,通过将问题分解成更小的可测试的组件,你编写软件将会更加轻松。


作者:Chris James[5]译者:Donng[6]校对:polaris1119[7],pityonline[8]

本文由 GCTT[9] 原创编译,Go 中文网[10] 荣誉推出

推荐阅读:

  • 通过测试学习Go:安装 Go,搭建开发环境

  • 通过测试学习 Go 语言

喜欢该系列的朋友,欢迎关注“Go语言中文网”:

参考资料

[1]

你可以在这里查看本章的所有代码: https://github.com/quii/learn-go-with-tests/tree/master/hello-world

[2]

章节: https://github.com/studygolang/learn-go-with-tests/blob/master/zh-CN/install-go.md#Go-环境

[3]

localhost:8000/pkg: localhost:8000/pkg

[4]

http://localhost:8000/pkg/testing/: http://localhost:8000/pkg/testing/

[5]

Chris James: https://dev.to/quii

[6]

Donng: https://github.com/Donng

[7]

polaris1119: https://github.com/polaris1119

[8]

pityonline: https://github.com/pityonline

[9]

GCTT: https://github.com/studygolang/GCTT

[10]

Go 中文网: https://studygolang.com/

go定时器 每天重复_通过测试学习Go:Hello, World相关推荐

  1. python训练营微信公众号真实性_用python进行微信公众号开发(仅测试学习)

    今天看到篇教程,是用python开发微信公众号的,觉得有意思,就敲代码实现了一下,成功后更觉得好玩,故记录,方便开发深入时使用. 基础背景介绍: 首先得有个人微信号(没有自行注册),为方便测试学习: ...

  2. 电脑硬件检测_【学无止境】电脑硬件维修测试学习资料(附送各类PC检修资源)...

    大橙子资源驿站 『 电脑硬件维修测试学习资料』 多年的PC硬件检修工具和资料,都是本人一年年亲测积累起来的,安全无毒.打包上传分享给大家. -- 资源分享者语 关于『 电脑硬件维修学习资料 』 这是甛 ...

  3. oracle 测试sql执行时间_从 TPCH 测试学习性能优化技巧

    一. 目标 TPCH是由TPC(Transaction Processing Performance Council)事务处理性能委员会公布的一套针对数据库决策支持能力的测试基准,通过模拟数据库中与业 ...

  4. java如何测试定时器_如何测试定时任务 - 邓竣的个人页面 - OSCHINA - 中文开源技术交流社区...

    系统一般会有一些后台定时任务,假设我们使用quartz实现定时任务,那么有两个测试功能点: 定时任务逻辑是否正确 定时器cron表达式编写是否正确,触发时间点是否正确 定时任务逻辑测试 如果定时任务在 ...

  5. 深度学习:在图像上找到手势_使用深度学习的人类情绪和手势检测器:第1部分

    深度学习:在图像上找到手势 情感手势检测 (Emotion Gesture Detection) Has anyone ever wondered looking at someone and tri ...

  6. 测试私有方法 重构_通过测试学Go:指针和错误

    点击上方蓝色"Go语言中文网"关注我们,领全套Go资料,每天学习 Go 语言 你可以在这里找到本章的所有代码[1] 我们在上一节中学习了结构体(structs),它可以组合与一个概 ...

  7. 深度学习将灰度图着色_通过深度学习为视频着色

    深度学习将灰度图着色 零本地设置/ DeOldify / Colab笔记本 (Zero Local Setup / DeOldify / Colab Notebook) "Haal Kais ...

  8. 深度学习模型建立过程_所有深度学习都是统计模型的建立

    深度学习模型建立过程 Deep learning is often used to make predictions for data driven analysis. But what are th ...

  9. 乐器演奏_深度强化学习代理演奏的蛇

    乐器演奏 Ever since I watched the Netflix documentary AlphaGo, I have been fascinated by Reinforcement L ...

最新文章

  1. CPU 深夜狂飙,一帮大佬都傻眼了......
  2. 梯度消失问题学习资料整理
  3. MyEclipse 打包和发布的个人总结 .
  4. python requests session刷新_Python Requests Session set-cookie不生效的坑
  5. layer.open 强化1
  6. 【Fiddler 实战操作】如何使用 Fiddler 对苹果手机进行抓包
  7. linux gret 文件内容,DataX插件开发指南.docx
  8. Github 下载单个文件
  9. ADAMS2016启动证书错误解决
  10. 使用超大电路集成的计算机,使用超大规模集成电路制造的计算机应该归属于()...
  11. 编程路上,对于迷失者的一些小小建议
  12. 什么是列联表分析(Contingency table analysis)?
  13. outlook客户端怎么看html,谷歌浏览器根据html网页启动邮件客户端Outlook(示例代码)...
  14. SylixOS中断延迟队列
  15. 笔记本电脑运行特别慢怎么解决
  16. VMware Workstation导出的ovf格式虚拟机 不能用VirtualBox导入
  17. android ftp 链接不上去,安卓手机无法连接电脑上的ftp(vsftpd)服务器
  18. 51单片机学习2——DS1302制作简易数码管电子时钟
  19. android动态壁纸的制作
  20. 关于虚拟机中linux如何ping通百度

热门文章

  1. MLPerf Inference 0.7应用
  2. 24GHz和77GHz毫米波雷达技术细节
  3. 半导体群聚、虚拟垂直、整合
  4. Java IDEA Debug进制二维数组
  5. android setAlpha 与 getBackground().setAlpha
  6. C# 视频多人脸识别的实现过程
  7. 织梦手机站下一篇变上一篇而且还出错Request Error!
  8. IntelliJ IDEA 自动编译功能无法使用,On 'update' action:选项里面没有update classes and resources这项...
  9. 2022-2028年中国2,3,6-三甲基苯酚行业市场研究及前瞻分析报告
  10. golang etcd 报错 undefined: resolver.BuildOption 解决方案