Array VS Slice

数组允许你以特定的顺序在变量中存储相同类型的多个元素。
对于数组来说,最常见的就是迭代数组中的元素。
我们创建一个 Sum 函数,它使用 for 来循环获取数组中的元素并返回所有元素的总和。
 

先写测试(array)

利用 TDD 思想我们需要先写测试函数
先创建一个 sum_test.go 文件
sum_test.go 中:

package main

import "testing"

func TestSum(t *testing.T) {

    numbers := [5]int{1, 2, 3, 4, 5}

    got := Sum(numbers)
want := 15 if want != got {
t.Errorf("got %d want %d given, %v", got, want, numbers)
}
}

数组的容量是我们在声明它时指定的固定值。我们可以通过两种方式初始化数组:

关于其他引用类型的介绍参考

在错误信息中打印函数的输入有时很有用。

我们使用 %v(默认输出格式)占位符来打印输入,它非常适用于展示数组。

运行测试

我们创建一个Sum.go文件,不着急补全,我们先写个框架,让它编译通过

package main

func Sum(numbers [5]int) (sum int) {
return 0
}

这时测试还会失败,不过会返回明确的错误信息:

sum_test.go:13: got 0 want 15 given, [1 2 3 4 5]

这个时候把代码补充完整,使得它能够通过测试:

func Sum(numbers [5]int) int {
sum := 0
for i := 0; i < 5; i++ {
sum += numbers[i]
}
return sum
}

可以使用 取下标 也就是 array[index] 语法来获取数组中指定索引对应的值。

在本例中我们使用 for 循环分 5 次取出数组中的元素并与 sum 变量求和。

重构一(改进代码)

我们可以使用 range 语法来让函数变得更加整洁。

func Sum(numbers [5]int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}

range 会迭代数组,每次迭代都会返回数组元素的索引和值。我们选择使用 '_' 空白标志符 来忽略索引。

数组和它的类型

数组有一个有趣的属性,它的大小也属于类型的一部分,如果你尝试将 [4]int 作为 [5]int 类型的参数传入函数,是不能通过编译的。
它们是不同的类型,就像尝试将 string 当做 int 类型的参数传入函数一样。
因为这个原因,所以数组比较笨重,大多数情况下我们都不会使用它。
Go 的切片(slice)类型不会将集合的长度保存在类型中,因此它的尺寸可以是不固定的。
下面我们会完成一个动态长度的 Sum
 

先写测试(slice)

我们会使用 切片类型,它可以接收不同大小的切片集合。语法上和数组非常相似,只是在声明的时候不指定长度:

func TestSum(t *testing.T) {

    t.Run("collection of 5 numbers", func(t *testing.T) {
numbers := [5]int{1, 2, 3, 4, 5} got := Sum(numbers)
want := 15 if got != want {
t.Errorf("got %d want %d given, %v", got, want, numbers)
}
}) t.Run("collection of any size", func(t *testing.T) {
numbers := []int{1, 2, 3} got := Sum(numbers)
want := 6 if got != want {
t.Errorf("got %d want %d given, %v", got, want, numbers)
}
}) }

运行测试

当然会编译出错:

同样先使用最少的代码来让失败的测试先跑起来

这时我们可以选择一种解决方案:
 
  • 修改现有的 API,将 Sum 函数的参数从数组改为切片。如果这么做我们就有可能会影响使用这个 API 的人,因为我们的 其他 测试不能编译通过。
  • 创建一个新函数。

根据目前的情况,并没有人使用我们的函数,所以选择修改原来的函数。

func Sum(numbers []int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}

如果你运行测试,它们还是不能编译通过,你必须把之前测试代码中的数组换成切片

再把 Sum 补充完整,使得它能够通过测试:

事实证明,这里需要我们做的只是修复编译器错误,然后测试就通过了。

重构二

我们已经重构了 Sum 函数把参数从数组改为切片。注意不要在重构以后忘记维护你的测试代码。

func TestSum(t *testing.T) {

    t.Run("collection of 5 numbers", func(t *testing.T) {
numbers := []int{1, 2, 3, 4, 5} got := Sum(numbers)
want := 15 if got != want {
t.Errorf("got %d want %d given, %v", got, want, numbers)
}
}) t.Run("collection of any size", func(t *testing.T) {
numbers := []int{1, 2, 3} got := Sum(numbers)
want := 6 if got != want {
t.Errorf("got %d want %d given, %v", got, want, numbers)
}
}) }
质疑测试的价值是非常重要的。测试并不是越多越好,而是尽可能的使你的代码更加健壮。
太多的测试会增加维护成本,因为 维护每个测试都是需要成本的
在本例中,针对该函数写两个测试其实是多余的,因为切片尺寸并不影响函数的运行。
 Go 有内置的计算测试 覆盖率的工具,它能帮助你发现没有被测试过的区域。
我们不需要追求 100% 的测试覆盖率,它只是一个供你获取测试覆盖率的方式。
只要你严格遵循 TDD 规范,那你的测试覆盖率就会很接近 100%。
运行:

go test -cover

你会看到:

现在删除一个测试,然后再次运行。

新的想法

这回我们需要一个 SumAll 函数,它接受多个切片,并返回由每个切片元素的总和组成的新切片。

先写测试

func TestSumAll(t *testing.T) {

    got := SumAll([]int{1,2}, []int{0,9})
want := []int{3, 9} if got != want {
t.Errorf("got %v want %v", got, want)
}
}

运行测试

先使用最少的代码来让失败的测试先跑起来

我们需要定义满足测试要求的 SumAll
我们可以写一个 可变参数的函数:
func SumAll(numbersToSum ...[]int) (sums []int) {
return
}

这时运行测试会报编译时错误:

./sum_test.go:26:9: invalid operation: got != want (slice can only be compared to nil)

在 Go 中不能对切片使用等号运算符。你可以写一个函数迭代每个元素来检查它们的值。

但是一种比较简单的办法是使用 reflect.DeepEqual,它在判断两个变量是否相等时十分有用。

func TestSumAll(t *testing.T) {

    got := SumAll([]int{1,2}, []int{0,9})
want := []int{3, 9} if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}

确保你已经在文件头部 import reflect,这样你才能使用 DeepEqual 方法。

需要注意的是 reflect.DeepEqual 不是「类型安全」的,所以有时候会发生比较怪异的行为。

为了看到这种行为,暂时将测试修改为:

func TestSumAll(t *testing.T) {

    got := SumAll([]int{1,2}, []int{0,9})
want := "bob" if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
这里我们尝试比较 slicestring。这显然是不合理的,但是却通过了编译!所以使用 reflect.DeepEqual 比较简洁但是在使用时需多加小心。
 sum_test.go:30: got [] want [3 9]
将代码补充完整使函数能够测试通过
我们需要做的就是迭代可变参数,使用 Sum 计算每个参数的总和并把结果放入函数返回的切片中。

func SumAll(numbersToSum ...[]int) (sums []int) {
lengthOfNumbers := len(numbersToSum)
sums = make([]int, lengthOfNumbers) for i, numbers := range numbersToSum {
sums[i] = Sum(numbers)
} return
}
我们学到了很多新东西。
这里有一种创建切片的新方式。make 可以在创建切片的时候指定我们需要的长度和容量。
我们可以使用切片的索引访问切片内的元素,使用 = 对切片元素进行赋值。
 现在应该可以测试通过。

重构三

顺便说一下,切片有容量的概念。如果你有一个容量为 2 的切片,但使用 mySlice[10]=1 进行赋值,会报运行时错误。
不过你可以使用 append 函数,它能为切片追加一个新值。

func SumAll(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
sums = append(sums, Sum(numbers))
} return sums
}
在这个实现中,我们不用担心切片元素会超过容量。我们开始使用空切片(在函数签名中定义),在每次计算完切片的总和后将结果添加到切片中。
接下来的工作是把 SumAll 变成 SumAllTails
 

还有一个新想法

先写测试

func TestSumAllTails(t *testing.T) {
got := SumAllTails([]int{1,2}, []int{0,9})
want := []int{2, 9} if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}

运行测试

./sum_test.go:26:9: undefined: SumAllTails

先使用最少的代码来让失败的测试先跑起来

把函数名称改为 SumAllTails 并重新运行测试

sum_test.go:30: got [3 9] want [2 9]

将代码补充完整使函数能够测试通过

func SumAllTails(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
tail := numbers[1:]
sums = append(sums, Sum(tail))
} return sums
}

我们可以使用语法 slice[low:high] 获取部分切片。如果在冒号的一侧没有数字就会一直取到最边缘的元素。

在我们的函数中,我们使用 numbers[1:] 取到从索引 1 到最后一个元素。

重构四

这次没有太多需要重构的地方。
我们只需要想到:
如果传入一个空切片会怎样?空切片的「尾部」是什么呢,如果我们在空数组上使用 myEmptySlice[1:]会发生什么?

先写测试

func TestSumAllTails(t *testing.T) {

    t.Run("make the sums of some slices", func(t *testing.T) {
got := SumAllTails([]int{1,2}, []int{0,9})
want := []int{2, 9} if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}) t.Run("safely sum empty slices", func(t *testing.T) {
got := SumAllTails([]int{}, []int{3, 4, 5})
want :=[]int{0, 9} if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
}) }

运行测试

会报 panic

panic: runtime error: slice bounds out of range [recovered]
 
panic: runtime error: slice bounds out of range
 
值得注意的是,该函数 编译通过 了,但是在运行时出现错误。
编译时错误是我们的朋友,因为它帮助我们让程序可以工作。
运行时错误是我们的敌人,因为它影响我们的用户。
 
将代码补充完整使函数能够测试通过

func SumAllTails(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
if len(numbers) == 0 {
sums = append(sums, 0)
} else {
tail := numbers[1:]
sums = append(sums, Sum(tail))
}
} return sums
}

重构五

我们的测试代码有一部分是重复的,我们可以把它放到另一个函数中复用。

func TestSumAllTails(t *testing.T) {

    checkSums := func(t *testing.T, got, want []int) {
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v want %v", got, want)
}
} t.Run("make the sums of tails of", func(t *testing.T) {
got := SumAllTails([]int{1, 2}, []int{0, 9})
want := []int{2, 9}
checkSums(t, got, want)
}) t.Run("safely sum empty slices", func(t *testing.T) {
got := SumAllTails([]int{}, []int{3, 4, 5})
want := []int{0, 9}
checkSums(t, got, want)
}) }

这样使用起来更加方便,而且还能增加代码的类型安全性。如果一个粗心的开发者使用 checkSums(t, got, "dave") 是不能通过编译的

总结

我们学习了:
  •  
    数组
  •  
    切片
     
  •  
    多种方式的切片初始化
  •  
    切片的容量是 固定 的,但是你可以使用 append 从原来的切片中创建一个新切片
  •  
    如何获取部分切片
  •  
    使用 len 获取数组和切片的长度
  •  
    使用测试代码覆盖率的工具
  •  
    reflect.DeepEqual 的妙用和对代码类型安全性的影响
数组和切片的元素可以是任何类型,包括数组和切片自己。如果需要你可以定义 [][]string

Go语言:利用 TDD 测试驱动开发帮助理解数组与动态数组(切片)的区别的更多相关文章

  1. TDD(测试驱动开发)学习一:初识TDD

    首先说一下名词解释,TDD,英文名称Test-Driven Development,中文名称测试驱动开发,简单的断下句“测试/驱动/开发”,简单的理解一下,就是测试驱动着开发,大白话就是说用一边测试一 ...

  2. TDD(测试驱动开发)培训录

    2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都是复杂问题,改变人,改变一个组 ...

  3. TDD(测试驱动开发)培训录(转)

    本文转载自:http://www.cnblogs.com/whitewolf/p/4205761.html 最近也在了解TDD,发现这篇文章不错,特此转载一下. TDD(测试驱动开发)培训录 2015 ...

  4. TDD(测试驱动开发)

    TDD(测试驱动开发)培训录 2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都 ...

  5. TDD(测试驱动开发)学习二:创建第一个TDD程序

    本节我们将学习一些测试驱动开发环境的搭建,测试驱动开发概念和流程.所涉及的内容全部会以截图的形式贴出来,如果你也感兴趣,可以一步一步的跟着来做,如果你有任何问题,可以进行留言,我也会很高兴的为你答疑. ...

  6. (译)TDD(测试驱动开发)的5个步骤

    原文:5 steps of test-driven development https://developer.ibm.com/articles/5-steps-of-test-driven-deve ...

  7. C语言数组:C语言数组定义、二维数组、动态数组、字符串数组

    1.C语言数组的概念 在<更加优美的C语言输出>一节中我们举了一个例子,是输出一个 4×4 的整数矩阵,代码如下: #include <stdio.h> #include &l ...

  8. C语言柔性数组和动态数组

    [前言]经常看到C语言里的两个数组,总结一下. 一.柔性数组 参考:https://www.cnblogs.com/veis/p/7073076.html #include<stdio.h> ...

  9. 基于SOA架构的TDD测试驱动开发模式

    以需求用例为基,Case&Coding两条线并行,服务(M)&消费(VC)分离,单元.接口.功能.集成四层质量管理,自动化集成.测试.交付全程支持. 3个大阶段(需求分析阶段.研发准备 ...

  10. TDD(测试驱动开发)的推广方法论

随机推荐

  1. dota中的哲理

    战术和战略: 6k分和3k分玩家的最重要的区别不是英雄玩的不好,而是整体战略不明确. dota玩家游戏时长超过1000h的比比皆是,这些玩家里面分数差距相当大.高的7k往上,低的2k深坑爬不出来. 这 ...

  2. HDFS 内部工作机制

    HDFS 内部工作机制 HDFS集群分为两大角色:NameNode.DataNode (Secondary Namenode) NameNode 负责管理整个文件系统的元数据 DataNode 负责管 ...

  3. iOS包大小计算

    一.LinkMap文件分析 说明:LinkMap数据是根据文章<LinkMap文件分析>中方法实验实测数据. 如何获得LinkMap文件 1.在XCode中开启编译选项Write Link ...

  4. mysqld 打开连接卡

    把client的ip写在mysql服务器的/etc/hosts文件里 echo [ip] [hostname] >>/etc/hosts

  5. PHP Redis - List (列表)

    Redis列表是简单的字符串列表,按照插入顺序排序. 一个列表最多可以包含  232-1 个元素 (4294967295, 每个列表超过40亿个元素) 插入元素在列表头部(lPush,Lpushx) ...

  6. fpga 通过vga模块使电脑显示图片

    对vga模块搞了好几天,vga接口时序不难,就是行计数,列计数,对应到每一个像素上去.这里我先把vga控制模块的程序付上.采用的是小梅哥的程序.屏幕是800X600,配置的时钟是40M.注意改变屏幕尺 ...

  7. 【loguru】Python简单的日志管理模块

    简介 在部署一些定时运行或者长期运行的任务时,为了留存一些导致程序出现异常或错误的信息,通常会才用日志的方式来进行记录这些信息. 在 Python 中用到日志记录,那就不可避免地会用到内置的 logg ...

  8. MySQL8.0使用GROUP BY的问题

    当使用group by的语句中,select后面跟的列,在group by后面没有时,会报以下错误: Expression #2 of SELECT list is not in GROUP BY c ...

  9. verilog 硬件描述语言

    第一章 绪论 verilog--数字电路设计技术--ASIC/SOC芯片设计--协议pcie SATA USB--系统知识(个人计算机,芯片组,网络连接,嵌入式系统,硬件和软件的互操作) 第二章 寄存 ...

  10. 面向对象3(Java)

    多态 基本介绍 即同一方法可以根据发送对象的不同而采用多种不同的行为方式 一个对象的实际类型是确定的,但是可以指向对象的引用类型可以很多 多态存在的条件:a.有继承关系:b.子类重写父类方法:c.父类 ...