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方法建立两个函数分别计算长方形的周长和面积,实现起来都非常简单。但是这时候发现了一个问题,我们的代码能正常工作,但是其中不包含任何显式的信息表示计算的是长方形。粗心的开发者可能会错误的调用这些函数来计算三角形的周长和面积而没有意识到错误的结果。
解决方案
-
具体化函数名,如RectangleArea(w, h float64)
-
自定义类型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
解决方案
-
重新再创建一个package。So we could create our
Area(Circle)
in a new package, but that feels overkill here. -
为
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 viathis
.但是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 calledArea
that returns afloat64
so it satisfies theShape
interface -
Circle
has a method calledArea
that returns afloat64
so it satisfies theShape
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
- wish to test various implementations of an interface,
- 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
.
碎片知识
格式化字符串%.2f
, f
对应 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
}