learn Go with tests学习笔记(5)——Structs, methods & interfaces

本文探讨了Go语言中如何利用结构体、方法和接口增强代码的可读性和灵活性。通过实例展示了如何通过TDD(测试驱动开发)创建长方形和圆的计算功能,并通过接口实现不同形状的通用测试。同时,介绍了如何通过接口实现解耦,以及使用表格驱动测试提高测试效率。最后,提到了错误消息的格式化和数学常量的使用。
摘要由CSDN通过智能技术生成

Structs, methods & interfaces

知识点概括

  • 声明结构体struct来创建自定义数据类型 which lets you bundle related data together and make the intent of your code clearer
  • 声明接口interface,从而定义能够被不同数据类型使用的function(参数多态parametric polymorphism)
  • 为自定义数据类型创建方法method从而add functionality to your data types并且实现接口
  • 列表驱动测试table driven test让断言更清晰,这样可以使测试文件更易于扩展和维护
if got != want {
        t.Errorf("got %.2f want %.2f", got, want)
    }

Structs

问题引入

这一章开头要求使用TDD方法建立两个函数分别计算长方形的周长和面积,实现起来都非常简单。但是这时候发现了一个问题,我们的代码能正常工作,但是其中不包含任何显式的信息表示计算的是长方形。粗心的开发者可能会错误的调用这些函数来计算三角形的周长和面积而没有意识到错误的结果。

解决方案
  1. 具体化函数名,如RectangleArea(w, h float64)

  2. 自定义类型Rectangle,封装长方形的信息

    声明一个结构体struct:

    type Rectangle struct {
        Width float64
        Height float64
    }
    

    通过这样做使得我们在调用计算周长perimeter和面积area函数的时候输入的不再是没有其他线索的高Height和宽Width,而是一个长方形Rectangle类型,这样更加有利于我们去更加清晰地表达convey我们的意图intent。

Methods

第二个问题

现在我们想要设计一个函数来计算圆的面积。但是Go不允许对一个函数名进行两遍不同的定义:

func Area(circle Circle) float64       {}
func Area(rectangle Rectangle) float64 {}

一旦对同一个函数名定义两遍,就会发生报错:./shapes.go:20:32: Area redeclared in this block

解决方案
  1. 重新再创建一个package。So we could create our Area(Circle) in a new package, but that feels overkill here.

  2. Circle类型定义一个名为Area的method。

    A method is a function with a receiver. A method declaration binds an identifier, the method name, to a method, and associates the method with the receiver’s base type.

    Methods和functions很像,但是它是通过being invoked on一个特定的类型的一个实例来实现调用,不像一个function哪里都可以调用。

    func (receiverName ReceiverType) MethodName(args)声明方法和函数的区别就是在方法名前面加上receiver。

    When your method is called on a variable of that type, you get your reference to its data via the receiverName variable. In many other programming languages this is done implicitly and you access the receiver via this.但是Go中习惯上在声明方法的时候使用type的首字母作为receiverName。例如(r Rectangle)(c Circle)

Interfaces

第三个问题

我们的测试中有两个subtests,代码显得有些冗余。想做的事情是写一个checkArea函数,既可以传进去Rectangle类型的变量也可以传进去Circle类型,如果传进去的不是一个形状类型就会fail。All we want to do is take a collection of shapes, call the Area() method on them and then check the result.

interface的强大之处:

  • allow you to make functions that can be used with different types
  • create highly-decoupled code
  • whilst still maintaining type-safety
func TestArea(t *testing.T) {
	//这里在helper function中传进去一个Shape,We just tell Go what a Shape is using an interface declaration,然后Go就知道一个类型属不属于Sahpe
	checkArea := func(t testing.TB, shape Shape, want float64) {
		t.Helper()
		got := shape.Area()
		if got != want {
			t.Errorf("got %g want %g", got, want)
		}
	}

	t.Run("rectangles", func(t *testing.T) {
		rectangle := Rectangle{12, 6}
		checkArea(t, rectangle, 72.0)
	})

	t.Run("circles", func(t *testing.T) {
		circle := Circle{10}
		checkArea(t, circle, 314.1592653589793)
	})

}
//declare Shape interface
type Shape interface {
	Area() float64
}
  • Rectangle has a method called Area that returns a float64 so it satisfies the Shape interface

  • Circle has a method called Area that returns a float64 so it satisfies the Shape interface

  • string does not have such a method, so it doesn’t satisfy the interface

    在Go中接口的实现时隐式的,如果你传进去的数据类型满足了接口的要求,那编译就会通过

    In Go interface resolution is implicit. If the type you pass in matches what the interface is asking for, it will compile.

解耦Decoupling

注意我们定义的helper函数根本不需要传进来的是长方形还是三角形还是圆形,通过声明Shape接口,helper与具体形状这个信息得以解耦,只需要获得它们的method来完成要干的事情就行。这种使用接口来声明你only what you need的方法在软件设计中非常重要。

Table driven tests

Table driven tests are useful when you want to build a list of test cases that can be tested in the same manner.

func TestArea(t *testing.T) {
	//这里创建了一个“匿名struct”,使用带有两个属性的[]struct声明了struct切片
	areaTests := []struct {
		shape Shape
		want  float64
	}{
		{Rectangle{12, 6}, 72.0},
		{Circle{10}, 314.1592653589793},
	}
	//We then iterate over them just like we do any other slice, using the struct fields to run our tests.
	for _, tt := range areaTests {
		got := tt.shape.Area()
		if got != tt.want {
			t.Errorf("got %g want %g", got, tt.want)
		}
	}

}

这样做的好处是,如果你新定义了一个实现Shape接口的形状(实现Area方法并且返回float64),你会发现你可以很方便地把它加入到测试当中。

Table driven tests can be a great item in your toolbox, but be sure that you have a need for the extra noise in the tests. They are a great fit when you

  1. wish to test various implementations of an interface,
  2. or if the data being passed in to a function has lots of different requirements that need testing.

我们还可以对测试进行一些改进,比如可以看到原先有一些magic值

{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
{Triangle{12, 6}, 36.0},

我们没法第一眼就知道这些值的含义,可以改成如下表示方法

        {shape: Rectangle{Width: 12, Height: 6}, want: 72.0},
        {shape: Circle{Radius: 10}, want: 314.1592653589793},
        {shape: Triangle{Base: 12, Height: 6}, want: 36.0},

The test speaks to us more clearly, as if it were an assertion of truth, not a sequence of operations

func TestArea(t *testing.T) {

	areaTests := []struct {
		name    string
		shape   Shape
		hasArea float64
	}{
		{name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0},
		{name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793},
		{name: "Triangle", shape: Triangle{Base: 12, Height: 6}, hasArea: 36.0},
	}

	for _, tt := range areaTests {
		// using tt.name from the case to use it as the `t.Run` test name
		t.Run(tt.name, func(t *testing.T) {
			got := tt.shape.Area()
			if got != tt.hasArea {
				t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea)
			}
		})

	}

}

By wrapping each case in a t.Run you will have clearer test output on failures as it will print the name of the case

--- FAIL: TestArea (0.00s) --- FAIL: TestArea/Rectangle (0.00s) shapes_test.go:33: main.Rectangle{Width:12, Height:6} got 72.00 want 72.10

And you can run specific tests within your table with go test -run TestArea/Rectangle.

碎片知识

格式化字符串%.2ff 对应 float64.2 表示输出 2 位小数。 Use of %g will print a more precise decimal number in the error message (fmt options).For example, using a radius of 1.5 in a circle area calculation, %f would show 7.068583 whereas %g would show 7.0685834705770345.

在table driven tests中我们会遇到一个问题那就是,无法得知error是在具体哪一个测试例子中fail的。We can change our error message into %#v got %g want %g. The %#v format string will print out our struct with the values in its field, so the developer can see at a glance the properties that are being tested.

To make circle’s Area function pass we will borrow the Pi constant from the math package (remember to import it).

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

sted.

To make circle’s Area function pass we will borrow the Pi constant from the math package (remember to import it).

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洞爷湖dyh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值