第十一部分 接口详解
什么是接口
官方解释为:接口类型是对其它类型行为的抽象和概括
一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口
通俗理解为:当一个小鸡实现了鸭子的接口,那么小鸡也是鸭子
就像这样
假设定义一个鸭子的接口。如下:
gotype Duck interface { Quack() // 鸭子叫 DuckGo() // 鸭子走 }
假设现在有另一个鸡类型,结构如下:
gotype Chicken struct { } func (c Chicken) IsChicken() bool { fmt.Println("我是小鸡") }
这只鸡很不一般,也可以做鸭子能做的事
gofunc (c Chicken) Quack() { fmt.Println("嘎嘎") } func (c Chicken) DuckGo() { fmt.Println("大摇大摆的走") }
我们定义一个函数,负责执行鸭子能做的事情。
gofunc DoDuck(d Duck) { d.Quack() d.DuckGo() }
因为小鸡实现了鸭子的所有方法,所以小鸡也是鸭。那么在 main 函数中就可以这么写了。
gofunc main() { c := Chicken{} DoDuck(c) }
markdown
接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口
sort.Interface接☐
官方解释为:sort包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。相比之下,Go语言的sort.Sort函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片
简单来说:当我们提供了
序列的长度,表示两个元素比较的结果,一种交换两个元素的方式
时,我们可以直接使用内置函数sort.Interface,而不需要自己编写一个复杂的排序函数,其他函数也是类似,当我们满足内置函数的条件时,可以直接调用内置函数
一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是sort.Interface的三个方法:
go
package sort
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
type StringSlice []string
func (p StringSlice) Len() int {
return len(p)
}
func (p StringSlice) Less(i, j int) bool {
return p[i] < p[j]
}
func (p StringSlice) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
sort.Sort(StringSlice(names))
类型断言
- 如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。换句话说,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出panic。例如:
govar w io.Writer w = os.Stdout f := w.(*os.File) c := w.(*bytes.Buffer)
- 如果相反地断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。如果这个检查成功了,动态值没有获取到;这个结果仍然是一个有相同动态类型和值部分的接口值,但是结果为类型T。换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保留了接口值内部的动态类型和值的部分
- 如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型(更少的方法集合)做断言,因为它表现的就像是赋值操作一样,除了对于nil接口值的情况
iniw = rw w = rw.(io.Writer)
- 一个接口值的动态类型是不确定的。如果类型断言出现在一个预期有两个结果的赋值操作中,例如如下的定义,这个操作不会在失败的时候发生panic,但是替代地返回一个额外的第二个结果,这个结果是一个标识成功与否的布尔值:
govar w io.Writer = os.Stdout f, ok := w.(*os.File) b, ok := w.(*bytes.Buffer)
- 第二个结果通常赋值给一个命名为ok的变量。如果这个操作失败了,那么ok就是false值,第一个结果等于被断言类型的零值,在这个例子中就是一个nil的
*bytes.Buffer
类型。- 这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁:
arduinoif f, ok := w.(*os.File); ok { // ...use f... }
当类型断言的操作对象是一个变量,有时会看见原来的变量名重用而不是声明一个新的本地变量名,这个重用的变量原来的值会被覆盖,如下:
arduinoif w, ok := w.(*os.File); ok { // ...use w... }
接口学习总结
- 当设计一个新包时,我们总是先创建一套接口,然后再定义一些满足它们的具体类型。这种方式的结果就是有很多的接口,它们中的每一个仅只有一个实现。这种接口是不必要的抽象。可以使用导出机制来限制一个类型的方法或一个结构体的字段是否在包外可见。接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要
- 当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好方式。
- 因为在Go语言中只有当两个或更多的类型实现一个接口时才使用接口,它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口。当新的类型出现时,小的接口更容易满足。对于接口设计的一个好的标准就是
Ask only for what you need(只考虑你需要的东西)
第十二部分 goroutine和channel
在Go语言中,每一个并发的执行单元叫作一个goroutine。设想这里的一个程序有两个函数,一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。
gof() go f()
-
如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。例如: 一个可以发送int类型数据的channel一般写为chan int
-
和map类似,channel也对应一个make创建的底层数据结构的引用。当复制一个channel或用于函数参数传递时,只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。channel的零值是nil。
-
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较
-
一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用
<-
运算符。在发送语句中,<-
运算符分割channel和要发送的值
注意:go语言中不接收<- channel
的值也是允许的
-
协程
单cpu同时运行若干程序,称为
并发
多cpu同时运行若干程序,称为
并行
-
管道
channel:本质就是一个数据结构-队列 数据是**
先进先出
**【FIFo:first in first out】 线程安全,多goroutine访问时,不需要加锁,就是说channelz本身就是线程安全 的 channel有类型的,一个string的channel只能存放string类型数据。 -
管道的定义
var 变量名 chan 数据类型 举例: var intChan chan int (intChan用于存放int数据) var mapChan chan map[int]string (mapChan.用于存放map[int]string类型) var perChan1 chan Person var perChan2 chan *Person 说明
0. channel是引用类型- channel必须初始化才能写入数据,即make后才能使用
- 管道是有类型的,intChan只能写入整数int
-
管道的容量不能增长
-
管道的初始化与存放取出
channel初始化 说明:使用make进行初始化 var intChan chan int intChan = make(chan int,10) 向channel中写入(存放)数据
govar intChan chan int intChan = make(chan int, 10) num := 999 intChan <- 10 //数据存入管道 len :1 cap :10 intChan <- num //len :2 cap :10 num01 := <-intChan //数据从管道取出 len :1 cap :10 num02 := <-intChan //len :0 cap :10 fmt.Println(num01, "\t", num02)
-
从管道取出时可以不接收文件
channel中只能存放指定的数据类型 channle的数据放满后,就不能再放入了 如果从channel取出数据后,可以继续放入 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
-
存放任意10个类型变量
gotype Cat struct { Name string Age int } func main() { var allChan chan interface{} //空接口可以接受任意变量 allChan = make(chan interface{}, 10) cat1 := Cat{Name: "tom", Age: 18} cat2 := Cat{Name: "tom~", Age: 180} allChan <- cat1 allChan <- cat2 allChan <- 10 allChan <- "jack" cat11 := <-allChan cat22 := <-allChan vi := <-allChan v2 := <-allChan fmt.Println(cat11, cat22, vi, v2) }
-
从管道中取出结构体的某一字段
go
type Cat struct {
Name string
Age int
}
func main() {
allChan := make(chan interface{}, 3)
allChan <- 10
allChan <- "tom jack"
cat := Cat{"小花猫", 4}
allChan <- cat
<-allChan //抛出前两个
<-allChan
newCat := <-allChan
fmt.Printf("newCat=%T newCat=%v\n", newCat, newCat)
newCat02 := newCat.(Cat) // 类型断言
fmt.Println(newCat02.Name)
}
-
channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
gofunc main() { intChan := make(chan int, 3) intChan <- 100 intChan <- 200 close(intChan) // close() 关闭管道 //这时不能够再写入数到channel fmt.Println("ok~") //当管道关闭后,读取数据是可以的 n := <-intChan fmt.Println("n=", n) }
-
channel的遍历(每次遍历会将读取到的元素取出,不能用for-i,只能用for-range)
gofunc main() { intChan2 := make(chan int, 100) for i := 0; i < 100; i++ { intChan2 <- i * 2 //放入100个数据到管道 } //在遍历时,如果channel没有关闭,则会出现dead1ock的错误 //在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历 close(intChan2) for v := range intChan2 { //inChan没有下标,不需要index fmt.Println("v=", v) } }
-
如果,编译器(运行时),发现一个管道只有写,而设有读,则该管道,会阻塞
-
写管道和读管道的频率可以不一致
-
管道可以声明为双向、只读或者只写
go//在默认情况下下,管道是双向 var chan1 chan int //可读可写 // 声明为只写 var chan2 chan<-int // 只写箭头在后 chan2 make(chan int,3) chan2<-20 //num :=<-chan2 //error fmt.Println("chan2=",chan2) // 声明为只读 var chan3 <-chan int // 只读箭头在前 num3 :=<-chan3 // chan3<-3 fmt.Println("num3",num3)
-
使用select可以解决从管道取数据的阻塞问题
gointChan := make(chan int, 10) for i := 0; i < 10; i++ { intChan <- i } stringChan := make(chan string, 5) for i := 0; i < 5; i++ { stringChan <- "hello" + fmt.Sprintf("%d", i) } for { //注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock //会自动到下一个case匹配 select { case v := <-intChan: fmt.Printf("从intchan读取的数据%d\n", v) case v := <-stringChan: fmt.Printf("从stringchani读取的数据%s\n", v) default: fmt.Printf("都取不到了,不玩了\n") return } }
-
goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic,进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续执行
第十三部分 测试
go test
go test命令是一个按照一定的约定和组织来测试代码的程序。在包目录内,所有以_test.go
为后缀名的源文件在执行go build时不会被构建成包的一部分,它们是go test测试的一部分。
在*_test.go
文件中,有三种类型的函数:测试函数、基准测试(benchmark)函数、示例函数。一个测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准测试函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档
go test命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件
测试用例文件名必须以
_test.go
结尾。比如cal_test.go
,cal不是固定的。测试用例函数必须以Test开头,一般来说就是Test+被测试的函数名,比如TestAddUpper
TestAddUpper(t tesing.T)的形参类型必须是 testing.T
一个测试用例文件中,可以有多个测试用例函数,比如TestAddUpper、TestSub
运行测试用例指令
(1)cmd>go test如果运行正确,无日志,错误时,会输出日志]
(2)cmd>go test -v[运行正确或是错误,都输出日志]
当出现错误时,可以使用t.Fatalf来格式化输出错误信息,并退出程序 t.Logf方法可以输出相应的日志
测试用例函数,并没有放在main函数中,也执行了,这就是测试用例的方便之处
PASS表示测试用例运行成功,FAIL表示测试用例运行失败 测试单个文件,一定要带上被测试的原文件
> go test -v cal_test.go cal.go
测试单个方法
> go test -v -test.run TestAddUpper
单元测试的意义:
- 提高代码质量代码测试都是为了帮助开发人员发现问题从而解决问题,提高代码质量。
- 尽早发现问题问题越早发现,解决的难度和成本就越低。
- 保证重构正确性随着功能的增加,重构(修改老代码)几乎是无法避免的。很多时候我们不敢重构的原因,就是担心其它模块因为依赖它而不工作。有了单元测试,只要在改完代码后运行一下单测就知道改动对整个系统的影响了,从而可以让我们放心的重构代码。
- 简化调试过程单元测试让我们可以轻松地知道是哪一部分代码出了问题。
- 简化集成过程由于各个单元已经被测试,在集成过程中进行的后续测试会更加容易。
- 优化代码设计编写测试用例会迫使开发人员仔细思考代码的设计和必须完成的工作,有利于开发人员加深对代码功能的理解,从而形成更合理的设计和结构。
- 单元测试是最好的文档单元测试覆盖了接口的所有使用方法,是最好的示例代码。而真正的文档包括注释很有可能和代码不同步,并且看不懂
单元测试的原则:
- 快
- 一致性代码没有改变的情况下,每次运行得结果应该保持确定且一致
- 原子性 结果只有两种情况
Pass
或者Fail
- 用例独立执行顺序不影响,执行前后环境状态一致
- 单一职责一个用例只负责一个场景
- 隔离
- 可读性用例的名称、变量名等直接表现出该测试的目标
- 自动化
学习总结
- 通过对接口的学习,了解掌握如何使用接口,提升代码可读性,减少冗余。学习了如何通过提供条件,使用内置方法来降低代码量。空接口的使用与类型断言的应用场景。
- 协程与通道的使用,极大效率提高了程序的运行速度,使用cpu的最大运行效率,将单次长时间操作以并行方式同步解决,通过cpu更高的功耗换来程序的高频运行
- 单元测试部分,学习如何利用单元测试,多次运行程序,对结果进行和判断,通过接收不同的返回值来寻找代码运行问题所在,代码测试的目的在于尽量的列举代码出错的情况,来针对问题进行解决,不能盲目的追求单元测试的范围和解决所有问题