Go语言:利用 TDD 逐步为一个字典应用创建完整的 CRUD API
前言
键存储值,并快速查找它们。键视为单词,将值视为定义。所以,难道还有比构建我们自己的字典更好的学习 map 的方式吗?正文
首先编写测试
在 dictionary_test.go 中编写代码:
package main
import "testing"
func TestSearch(t *testing.T) {
dictionary := map[string]string{"test": "this is just a test"}
got := Search(dictionary, "test")
want := "this is just a test"
if got != want {
t.Errorf("got '%s' want '%s' given, '%s'", got, want, "test")
}
}
map 关键字开头,需要两种类型。第一个是键的类型,写在 [] 中。第二个是值的类型,跟在 [] 之后。尝试运行测试
运行 go test 后编译器会提示失败信息 ./dictionary_test.go:8:9: undefined: Search。
编写最少量的代码让测试运行并检查输出
在 dictionary.go 中:
package main
func Search(dictionary map[string]string, word string) string {
return ""
}
dictionary_test.go:12: got '' want 'this is just a test' given, 'test'编写足够的代码使测试通过
func Search(dictionary map[string]string, word string) string {
return dictionary[word]
}
从 map 中获取值和数组相同,都是通过 map[key] 的方式。
重构
func TestSearch(t *testing.T) {
dictionary := map[string]string{"test": "this is just a test"}
got := Search(dictionary, "test")
want := "this is just a test"
assertStrings(t, got, want)
}
func assertStrings(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
创建一个 assertStrings 辅助函数并删除 given 的部分让实现更通用。
使用自定义的类型
我们可以通过为 map 创建新的类型并使用 Search 方法改进字典的使用。
在 dictionary_test.go 中:
func TestSearch(t *testing.T) {
dictionary := Dictionary{"test": "this is just a test"}
got := dictionary.Search("test")
want := "this is just a test"
assertStrings(t, got, want)
}
Dictionary 类型了,但是我们还没有定义它。然后要在 Dictionary 实例上调用 Search 方法。assertStrings。dictionary.go 中:
type Dictionary map[string]string
func (d Dictionary) Search(word string) string {
return d[word]
}
在这里,我们创建了一个 Dictionary 类型,它是对 map 的简单封装。定义了自定义类型后,我们可以创建 Search 方法。
首先编写测试
func TestSearch(t *testing.T) {
dictionary := Dictionary{"test": "this is just a test"}
t.Run("known word", func(t *testing.T) {
got, _ := dictionary.Search("test")
want := "this is just a test"
assertStrings(t, got, want)
})
t.Run("unknown word", func(t *testing.T) {
_, err := dictionary.Search("unknown")
want := "could not find the word you were looking for"
if err == nil {
t.Fatal("expected to get an error.")
}
assertStrings(t, err.Error(), want)
})
}
在 Go 中处理这种情况的方法是返回第二个参数,它是一个 Error 类型。
Error 类型可以使用 .Error() 方法转换为字符串,我们将其传递给断言时会执行此操作。
我们也用 if 来保护 assertStrings,以确保我们不在 nil 上调用 .Error()。
尝试运行测试
这不会通过编译
./dictionary_test.go:18:10: assignment mismatch: 2 variables but 1 values
编写最少量的代码让测试运行并检查输出
func (d Dictionary) Search(word string) (string, error) {
return d[word], nil
}
现在你的测试将会失败,并显示更加清晰的错误信息
dictionary_test.go:22: expected to get an error.
编写足够的代码使测试通过
func (d Dictionary) Search(word string) (string, error) {
definition, ok := d[word]
if !ok {
return "", errors.New("could not find the word you were looking for")
}
return definition, nil
}
为了使测试通过,我们使用了一个 map 查找的有趣特性。
它可以返回两个值。第二个值是一个布尔值,表示是否成功找到 key。
此特性允许我们区分单词不存在还是未定义。
重构
var ErrNotFound = errors.New("could not find the word you were looking for")
func (d Dictionary) Search(word string) (string, error) {
definition, ok := d[word]
if !ok {
return "", ErrNotFound
}
return definition, nil
}
我们通过将错误提取为变量的方式,摆脱 Search 中魔术错误(magic error)。这也会使我们获得更好的测试。
t.Run("unknown word", func(t *testing.T) {
_, got := dictionary.Search("unknown")
assertError(t, got, ErrNotFound)
})
func assertError(t *testing.T, got, want error) {
t.Helper()
if got != want {
t.Errorf("got error '%s' want '%s'", got, want)
}
}
通过创建一个新的辅助函数,我们能够简化测试,并使用 ErrNotFound 变量,如果我们将来更改显示错误的文字,测试也不会失败。
首先编写测试
我们现在有很好的方法来搜索字典。但是,我们无法在字典中添加新单词。
func TestAdd(t *testing.T) {
dictionary := Dictionary{}
dictionary.Add("test", "this is just a test")
want := "this is just a test"
got, err := dictionary.Search("test")
if err != nil {
t.Fatal("should find added word:", err)
}
if want != got {
t.Errorf("got '%s' want '%s'", got, want)
}
}
在这个测试中,我们利用 Search 方法使字典的验证更加容易。
编写最少量的代码让测试运行并检查输出
在 dictionary.go 中:
func (d Dictionary) Add(word, definition string) {
}
测试现在应该会失败:
dictionary_test.go:31: should find added word: could not find the word you were looking for
编写足够的代码使测试通过
func (d Dictionary) Add(word, definition string) {
d[word] = definition
}
向 map 添加元素也类似于数组。你只需指定键并给它赋一个值。
引用类型
Map 有一个有趣的特性,不使用指针传递你就可以修改它们。
这是因为 map 是引用类型。这意味着它拥有对底层数据结构的引用,就像指针一样。
它底层的数据结构是 hash table 或 hash map
Map 作为引用类型是非常好的,因为无论 map 有多大,都只会有一个副本
引用类型引入了 maps 可以是 nil 值。如果你尝试使用一个 nil 的 map,你会得到一个 nil 指针异常,这将导致程序终止运行。
由于 nil 指针异常,你永远不应该初始化一个空的 map 变量:
var m map[string]string
相反,你可以像我们上面那样初始化空 map,或使用 make 关键字创建 map
dictionary = map[string]string{}
// OR
dictionary = make(map[string]string)
这两种方法都可以创建一个空的 hash map 并指向 dictionary。这确保永远不会获得 nil 指针异常
重构
在我们的实现中没有太多可以重构的地方,但测试可以简化一点。
func TestAdd(t *testing.T) {
dictionary := Dictionary{}
word := "test"
definition := "this is just a test"
dictionary.Add(word, definition)
assertDefinition(t, dictionary, word, definition)
}
func assertDefinition(t *testing.T, dictionary Dictionary, word, definition string) {
t.Helper()
got, err := dictionary.Search(word)
if err != nil {
t.Fatal("should find added word:", err)
}
if definition != got {
t.Errorf("got '%s' want '%s'", got, definition)
}
}
我们为单词和定义创建了变量,并将定义断言移到了自己的辅助函数中。
我们的 Add 看起来不错。除此之外,我们没有考虑当我们尝试添加的值已经存在时会发生什么!
如果值已存在,map 不会抛出错误。相反,它们将继续并使用新提供的值覆盖该值。
这在实践中很方便,但会导致我们的函数名称不准确。Add 不应修改现有值。它应该只在我们的字典中添加新单词。
首先编写测试
func TestAdd(t *testing.T) {
t.Run("new word", func(t *testing.T) {
dictionary := Dictionary{}
word := "test"
definition := "this is just a test"
err := dictionary.Add(word, definition)
assertError(t, err, nil)
assertDefinition(t, dictionary, word, definition)
})
t.Run("existing word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
dictionary := Dictionary{word: definition}
err := dictionary.Add(word, "new test")
assertError(t, err, ErrWordExists)
assertDefinition(t, dictionary, word, definition)
})
}
对于此测试,我们修改了 Add 以返回错误,我们将针对新的错误变量 ErrWordExists 进行验证。我们还修改了之前的测试以检查是否为 nil 错误。
尝试运行测试
编译将失败,因为我们没有为 Add 返回值。
./dictionary_test.go:30:13: dictionary.Add(word, definition) used as value
./dictionary_test.go:41:13: dictionary.Add(word, "new test") used as value
编写最少量的代码让测试运行并检查输出
在 dictionary.go 中:
var (
ErrNotFound = errors.New("could not find the word you were looking for")
ErrWordExists = errors.New("cannot add word because it already exists")
) func (d Dictionary) Add(word, definition string) error {
d[word] = definition
return nil
}
现在我们又得到两个错误。我们仍在修改值,并返回 nil 错误。
dictionary_test.go:43: got error '%!s(<nil>)' want 'cannot add word because it already exists'
dictionary_test.go:44: got 'new test' want 'this is just a test'
编写足够的代码使测试通过
func (d Dictionary) Add(word, definition string) error {
_, err := d.Search(word)
switch err {
case ErrNotFound:
d[word] = definition
case nil:
return ErrWordExists
default:
return err
}
return nil
}
这里我们使用 switch 语句来匹配错误。如上使用 switch 提供了额外的安全,以防 Search 返回错误而不是 ErrNotFound。
重构
我们没有太多需要重构的地方,但随着对错误使用的增多,我们还可以做一些修改。
const (
ErrNotFound = DictionaryErr("could not find the word you were looking for")
ErrWordExists = DictionaryErr("cannot add word because it already exists")
) type DictionaryErr string func (e DictionaryErr) Error() string {
return string(e)
}
我们将错误声明为常量,这需要我们创建自己的 DictionaryErr 类型来实现 error 接口。
你可以在 Dave Cheney 的这篇优秀文章中了解更多相关的细节。
简而言之,它使错误更具可重用性和不可变性
首先编写测试
func TestUpdate(t *testing.T) {
word := "test"
definition := "this is just a test"
dictionary := Dictionary{word: definition}
newDefinition := "new definition"
dictionary.Update(word, newDefinition)
assertDefinition(t, dictionary, word, newDefinition)
}
Update 与 Create 密切相关,这是下一个需要我们实现的方法
尝试运行测试
./dictionary_test.go:53:2: dictionary.Update undefined (type Dictionary has no field or method Update)
编写最少量的代码让测试运行并检查输出
我们已经知道如何处理这样的错误。我们需要定义我们的函数。
func (d Dictionary) Update(word, definition string) {}
从这里可以看出我们需要改变这个词的定义。
dictionary_test.go:55: got 'this is just a test' want 'new definition'
编写足够的代码使测试通过
当我们用 Create 解决问题时就明白了如何处理这个问题。所以让我们实现一个与 Create 非常相似的方法。
func (d Dictionary) Update(word, definition string) {
d[word] = definition
}
我们不需要对此进行重构,因为更改很简单。但是,我们现在遇到与 Create 相同的问题。
如果我们传入一个新单词,Update 会将它添加到字典中。
首先编写测试
t.Run("existing word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
newDefinition := "new definition"
dictionary := Dictionary{word: definition}
err := dictionary.Update(word, newDefinition)
assertError(t, err, nil)
assertDefinition(t, dictionary, word, newDefinition)
})
t.Run("new word", func(t *testing.T) {
word := "test"
definition := "this is just a test"
dictionary := Dictionary{}
err := dictionary.Update(word, definition)
assertError(t, err, ErrWordDoesNotExist)
})
我们在单词不存在时添加了另一种错误类型。我们还修改了 Update 以返回 error 值。
尝试运行测试
./dictionary_test.go:53:16: dictionary.Update(word, "new test") used as value
./dictionary_test.go:64:16: dictionary.Update(word, definition) used as value
./dictionary_test.go:66:23: undefined: ErrWordDoesNotExists
这次我们得到 3 个错误,但我们知道如何处理这些错误。
编写最少量的代码让测试运行并检查输出
const (
ErrNotFound = DictionaryErr("could not find the word you were looking for")
ErrWordExists = DictionaryErr("cannot add word because it already exists")
ErrWordDoesNotExist = DictionaryErr("cannot update word because it does not exist")
) func (d Dictionary) Update(word, definition string) error {
d[word] = definition
return nil
}
nil 错误。编写足够的代码使测试通过
func (d Dictionary) Update(word, definition string) error {
_, err := d.Search(word)
switch err {
case ErrNotFound:
return ErrWordDoesNotExist
case nil:
d[word] = definition
default:
return err
}
return nil
}
除了在更新 dictionary 和返回错误时切换之外,这个函数看起来几乎与 Add 完全相同。
关于声明 Update 的新错误的注意事项
ErrNotFound 而不添加新错误。但是,更新失败时有更精确的错误通常更好。遇到ErrNotFound时可以重定向用户,但遇到ErrWordDoesNotExist时会显示错误消息。
首先编写测试
func TestDelete(t *testing.T) {
word := "test"
dictionary := Dictionary{word: "test definition"}
dictionary.Delete(word)
_, err := dictionary.Search(word)
if err != ErrNotFound {
t.Errorf("Expected '%s' to be deleted", word)
}
}
我们的测试创建一个带有单词的 Dictionary,然后检查该单词是否已被删除。
尝试运行测试
通过运行 go test 我们得到
./dictionary_test.go:74:6: dictionary.Delete undefined (type Dictionary has no field or method Delete)
编写最少量的代码让测试运行并检查输出
func (d Dictionary) Delete(word string) {
}
添加这个之后,测试告诉我们没有删除这个单词
dictionary_test.go:78: Expected 'test' to be deleted
编写足够的代码使测试通过
func (d Dictionary) Delete(word string) {
delete(d, word)
}
delete。它需要两个参数。第一个是这个 map,第二个是要删除的键。delete 函数不返回任何内容,我们基于相同的概念构建 Delete 方法。Update 和 Create总结
在本节中,我们介绍了很多内容。我们为一个字典应用创建了完整的 CRUD API。在整个过程中,我们学会了如何:
- 创建 map
- 在 map 中搜索值
- 向 map 添加新值
- 更新 map 中的值
- 从 map 中删除值
- 了解更多错误相关的知识
- 如何创建常量类型的错误
- 对错误进行封装
Go语言:利用 TDD 逐步为一个字典应用创建完整的 CRUD API的更多相关文章
- 利用Runtime实现简单的字典转模型
前言 我们都知道,开发中会有这么一个过程,就是将服务器返回的数据转换成我们自己定义的模型对象.当然服务器返回的数据结构有xml类型的,也有json类型的.本文只讨论json格式的. 大家在项目中一般是 ...
- Python3基础 json.loads 解析json格式的数据,得到一个字典
Python : 3.7.0 OS : Ubuntu 18.04.1 LTS IDE : PyCharm 2018.2.4 Conda ...
- C语言利用 void 类型指针实现面向对象类概念与抽象。
不使用C++时,很多C语言新手可能认为C语言缺乏了面向对象和抽象性,事实上,C语言通过某种组合方式,可以间接性的实现面对对象和抽象. 不过多态和继承这种实现,就有点小麻烦,但是依然可以实现. 核心: ...
- C语言利用 void 类型指针实现面向对象类概念与抽象
不使用C++时,很多C语言新手可能认为C语言缺乏了面向对象和抽象性,事实上,C语言通过某种组合方式,可以间接性的实现面对对象和抽象. 不过多态和继承这种实现,就有点小麻烦,但是依然可以实现. 核心: ...
- R语言利用ROCR评测模型的预测能力
R语言利用ROCR评测模型的预测能力 说明 受试者工作特征曲线(ROC),这是一种常用的二元分类系统性能展示图形,在曲线上分别标注了不同切点的真正率与假正率.我们通常会基于ROC曲线计算处于曲线下方的 ...
- 如何利用开源思想开发一个SEO友好型网
如果你有一个网站需要去做SEO优化的时候,不要期望你的努力能立即得到回报.耐心等待并更正内容营销策略,最终会发现你的网站很受用户欢迎.下面就教你如何利用开源思维开发一个SEO友好型网站! 首先,你应该 ...
- C语言利用for循环打印菱形
C语言利用for循环打印菱形(高度为奇数) 这次用的方法是上下部分分开打印,先打印上部分,再打印下部分. 先举个简单的例子打印,再改进代码,登堂入室从而理解. 例:打印一个高度(高度必须为奇数)为 5 ...
- 《C语言入门1.2.3—一个老鸟的C语言学习心得》—清华大学出版社炮制的又一本劣书及伪书
<C语言入门1.2.3—一个老鸟的C语言学习心得>—清华大学出版社炮制的又一本劣书及伪书 [薛非评] 区区15页,有80多个错误. 最严重的有: 通篇完全是C++代码,根本不是C语言代码. ...
- 一个字典通过dictionaryWithDictionary 他们的内存指针是不同的
一个字典通过dictionaryWithDictionary 他们的内存指针是不同的 来自为知笔记(Wiz)
- 利用SCI做的一个足球答题系统
SCI,异步串行通信接口,内置独立的波特率产生电路和SCI收发器,可以选择发送8或9个数据位(其中一位可以指定为奇或偶校验位). SCI是全双工异步串行通信接口,主要用于MCU与其他计算机或设备之间的 ...
随机推荐
- RabbitMQ宕机了怎么办?
RabbiMQ宕机会导致消息丢失! 解决办法:可以做消息持久化. 非持久化消息:只有非持久化消息在RabbitMQ宕机时会发生消息丢失. 持久化消息:持久化的消息会在接收后被保存到磁盘中,所以Rabb ...
- C# 调用https接口 安全证书问题 解决方法
原文链接: https://blog.csdn.net/lizaijinsheng/article/details/127321758 说明: 如果是用https的话,由于没有证书,会报错:基础连接已 ...
- Linux python后台任务
Ubuntu 后台持续运行python服务 一般使用 nohup python -u app.py>t.log 2>&1 & nohup python3 -u app.py ...
- 1903021126 申文骏 Java 第三周作业 编写代码及运行
项目 内容 课程班级博客链接 19级信计班(本) 作业要求链接 第三周作业要求 博客名称 1903021126 申文骏 Java 第三周作业 编写代码及运行 要求 每道题要有题目,代码(使用插入代码, ...
- OpenCV实战之文档扫描判卷
import cv2 import numpy as np #图像显示 def cv_show(imgname,img): cv2.imshow(imgname,img) cv2.waitKey(0) ...
- python使用openpyxl读取合并单元格的值(转)
目录问题:解决思路:问题:假设在test.xlsx的"Sheet1"工作表中,A1:D3区域的值如下:要求给定指定的行.列以及对应的工作表作为参数,能够正确解析合并单元格,获取指定 ...
- COLMAP论文阅读笔记——sfm算法、不定期更新
文章目录 structure from motion revisited 第一步 对应点搜索 特征提取feature extraction 匹配matching 几何验证geometric verif ...
- ES6知识点总结
声明变量 let 不能重复声明 块级作用域 可修改let变量的值 , const 不能重复声明 块级作用域 不能修改const 变量的值 2. 箭头函数 而箭头函数的this指向函数定义时所 ...
- css3各种度量单位 px、em、%、rem、vh/vw、vmin/vmax
一 px 相对长度单位,浏览器的度量单位,相对于物理像素(显示器屏幕分辨率),1px在高清屏幕下可能占用2个物理像素.甚至3个物理像素,有关物理像素和px之间转换比,可以查看这篇文章. 二 em 相对 ...
- 用shell开火车哈哈
用shell开火车!(σ゚∀゚)σ⁶⁶⁶⁶⁶⁶⁶⁶⁶⁶ while true; do sl -aFile; done 这个效果更佳