Three-Day Thinking in Go (Day 2)

用户定义类型

类型组合

用户可以利用组合(composition)来从已有的类型中定义新的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type (
car struct {
year int
maker string
}

ev struct {
car car
chargeRate float32
}
)

model3 := ev {
chargeRate, 10.0
car: car{
year: 2017,
maker: "Tesla",
}
}
// can visit
model3.car.maker
model3.chargeRate

匿名类型组合

相当于把nested的类型展开了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type (
car struct {
year int
maker string
}

ev struct {
car
chargeRate float32
}
)

model3 := ev {
chargeRate, 10.0
car: car{ // 默认用type的名字
year: 2017,
maker: "Tesla",
}
}
// can visit
model3.maker
model3.chargeRat

并行类型

  • 用户可以把一个类型定义为一个已有的类型
  • 这个类型是全新的(并行的类型),并不是别名,
  • 和原始type使用的时候可能需要type promotion
  • 类型上的方法不会继承

匿名结构体

有时候,有一些用完即抛的操纵,比如un-marshall json,或者test case的输入,
可以考虑创建匿名结构体

1
2
3
4
5
6
7
8
9
func main() {
model3 := struct {
year int
maker string
} {
year: 2017,
maker: "Tesla",
}
}

方法

  • 类型方法是一种特殊的function,定义的是type的行为
  • 用来区分类型方法还是函数是, 函数接收器(function receiver), 有接收器的是方法, 没有接收器的为函数
  • 可以用在primitive类型也可以用户定义的类型
  • pass by value (这个value可以是指针)
  • 用 dot 来访问 type上的方法
  • 对于 mutable 和 immutable 的区分是在函数接收器的传递上
    • 指针方法,指针作为接收器,
    • 值方法,传值作为接收器,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 值方法 value semantic
package main

import "fmt"
import "time"

type car struct {
year int
maker string
}

func (c car) isNew() bool {
return c.year == time.Now().Year()
}

}
func (c car) setYear() {
c.year = time.Now().Year()
}

func main() {
myCar := car{
year: 2019,
maker: "Tesla",
}
fmt.Printf("%#v\n", myCar) // => main.car{year:2019, maker:"Tesla"}
myCar.setYear()
fmt.Printf("%#v\n", myCar) // => main.car{year:2019, maker:"Tesla"}
}
1
2
3
4
// mutation的方法需要使用 指针方法 Value Semantic
func (c *car) setYear() {
c.year = time.Now().Year()
}

接口

An interface type specifies a method set called its interface.
A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is nil.

  • 接口声明行为
  • 接口名字一般使用er/or结尾的表动作的名词
  • 多态
  • 接口不是类型的一部分
  • 接口一个双字数据结构
    • 一个指针指向type相关的信息结构体 iTable(type info + method set)
    • 一个指针指向数据
  • 不需要显式的声明某一个类型实现了某一个接口
  • 通过方法集弄清楚一个类型是否实现了某接口
  • 接口可以组合composite
  • 尽可能让接口小 (The bigger the interface, the weaker the abstraction - Rob Pike)
  • 小心接口污染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

type painter interface {
paint(color string)
}

type table struct {
brand, color string
}

func (t table) paint(color string) {
t.color = color
}

type chair struct {
brand, color string
}

func (c *chair) paint(color string) {
c.color = color
}

func paintIt(p painter, color string) {
p.paint(color)
}

func main() {
tasks := []painter{
table{brand: "s", color: "black"},
chair{brand: "c", color: "red"}, // => cannot use chair literal (type chair) as type painter in slice literal: chair does not implement painter (paint method has pointer receiver)
&chair{brand: "c", color: "red"}, // => 用空interface
}

for i := 0; i < len(tasks); i++ {
paintIt(tasks[i], "white")
fmt.Println(tasks[i])
}
}

接口方法集和(Method Set)规则

  • method set 记录了对于一个type支持的方法
  • 对于一个类型T,method set为其receiver为T类型的方法
  • 对于类型*T, method set则包括了receiver为T和*T类型的方法
接收器 caller T caller *T
included included
指针 no included

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"reflect"
)

type Painter interface {
Paint()
RemovePaint()
Color()
}

type Tom struct{}

func (t Tom) Color() {}
func (t *Tom) Paint() {}
func (t *Tom) RemovePaint() {}

func main() {
var t Tom
elemType := reflect.TypeOf(&t).Elem() // Elem returns the value that the interface v contains or that the pointer v points to
n := elemType.NumMethod()
for i := 0; i < n; i++ {
fmt.Println(elemType.Method(i).Name)
} // => Color

var pt *Tom
elemType = reflect.TypeOf(&pt).Elem()
n = elemType.NumMethod()
for i := 0; i < n; i++ {
fmt.Println(elemType.Method(i).Name)
} // => Color, Paint, RemovePaint
}

利用reflection可以根据call实现了同一个interface方法的不同类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import "fmt"

type painter interface {
paint(color string)
}

type table struct {
brand, color string
}

func (t table) paint(color string) {
t.color = color
}

type chair struct {
brand, color string
}

type tv struct {
brand, color string
}

func (c *chair) paint(color string) {
c.color = color
}

func paintIt(o interface{}, color string) {
if p, ok := o.(painter); ok {
p.paint(color)
return
}
fmt.Println("no way to paint this one")
}

func main() {
tasks := []interface{}{
&table{brand: "table", color: "black"},
&chair{brand: "chair", color: "red"},
&tv{brand: "tv", color: "while"},
}

for i := 0; i < len(tasks); i++ {
fmt.Println("paint", tasks[i], "to white color")
paintIt(tasks[i], "white")
fmt.Println("after paint", tasks[i])
}
}

// paint &{table black} to white color
// after paint &{table black}
// paint &{chair red} to white color
// after paint &{chair white}
// paint &{tv while} to white color
// no way to paint this one
// after paint &{tv while}

Testing

  • 测试是go的一等公民,所以自带了一个testing的框架
  • test file 必须以 _test.go 结尾
  • test 方法命名必须以 Test 开头
  • test code 和 code 在同一个文件夹
  • table testing 是 idiomatic, 标准库很多test是这么写的
  • 自带test coverage tool
  • 可以方便的测试http
1
2
3
4
5
6
go test    # 在当前folder跑test
go test -v # 在当前folder跑test 并打印测试信息
go test .../. # 在当前文件夹以及子文件夹中跑test
go test --run hello # 跑含有hello的tests
go test -coverprofile=cov.out # 生成测试覆盖结果
go tool cover -html=cov.out # 在网页中测试覆盖结果

Assert/BDD/TDD/Mocking

1
go get github.com/stretchr/testify

BDD: Ginkgo/goblin
Mock: moq

Table-driven Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func sayNumber(i int) int {
return i*2
}

func TestSayNumber(t *testing.T) {
testcases := []map[string]struct{
value, expect int
} {
"test_one": {1, 10}
"test_two": {2, 20}
}

for _, u := range testcases {
actual, expected := sayNumber(u.value), u.expected
if actual != expected {
t.Fatalf("Use case %v expected %v but got %v", u.value, expected, actual)
}
} // => Failed Use case 1 exptected 10 but got 2
}

MicroBenchmarks

  • go自带有性能测试的支持
  • Benchmark tests 也放在 _test.go 的文件中
  • Benchmark 为test的前缀
  • Test用的是 testing.T 而Benchmark用的是testing.B
  • 测试跑的次数b.N 会自动被调整 确保测量是可靠的
  • 默认的测试时间是1s 但是可以通过-benchtime调整
  • 可以用-benchmeme来检测内存的分配
  • 跑test并不会跑Benchmark
  • assign 返回值 避免 inlining
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Output string

func BenchmarkGreet(b *testing.B) {
// test setup

// reset timer if needed
b.ResetTimer()

var output string
for i := 0; i < b.N; i++ {
// assign return avoid inlining
output = greeter.Greet("hi")
}
Output = output
}
// go test -bench Greet
// go test -bench Greet --benchmem
// go test -bench Greet --benchtime 5s
希望快速得到新文章的通知?请关注作者的微信公众号

wechat-qrcode