文章目录
接口(interface)
接口是一种类型。接口定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
为什么要引入接口:
- 比如三角形、四边形、圆形都能计算周长和面积,如何把它们当成“图形”来处理?
- 比如学生、老师都会吃饭睡觉学习,如何把他们当成“人”来处理?
Go语言为了解决类似上面的问题,就设计了接口的概念。接口区别于之前学到所有的具体的类型,接口是一种抽象的类型。当看到一个接口类型的值时,唯一知道的是通过它的方法能做什么。
接口定义
Go语言提倡面向接口编程。每个接口由数个方法组成,接口的定义为:
type 接口类型名 interface{
方法名1(参数列表1) 返回值列表1
方法名2(参数列表2) 返回值列表2
}
其中:
- 接口名:使用
type
关键字将接口定义为自定义的类型名,Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
等。接口名最好要能突出该接口的类型含义; - 方法名:当方法名首字母是大写且接口类型名首字母是大写时,该方法可以被接口所在包之外的代码访问;
- 参数列表、返回值列表:列表中的变量名可以省略。
接口实例
一个对象只要实现全部实现了接口中的方法,即实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
接口类型变量能够存储所有实现了该接口中的方法的实例。像下面的例子中,person
、dog
、cat
对象都实现了speaker
接口中的方法speak()
,因此speaker
接口类型定义的变量能存储person
,dog
, cat
结构体对象。
这里和Java中的接口相比较:
- 不同之处在于:Java中的接口定义的方法可以有自己的实现(即函数体),而Go接口中的方法不包含具体的实现;
- 相同之处在于:可以通过接口类型的变量来存储引用实现了接口中方法的对象,这类似于Java中的多态。
栗子:
package main
import "fmt"
type speaker interface {
speak()
}
type person struct{}
type dog struct{}
type cat struct{}
func (p person) speak() {
fmt.Println("嘤嘤嘤~")
}
func (d dog) speak() {
fmt.Println("汪汪汪~")
}
func (c cat) speak() {
fmt.Println("喵喵喵~")
}
func _genericFunc(s speaker) {
s.speak()
}
func main() {
var (
p person
d dog
c cat
)
_genericFunc(p)
_genericFunc(d)
_genericFunc(c)
}
代码输出:
值接收者和指针接收者实现接口
值接收者实现接口
使用值接收者实现接口之后,不管是dog
结构体还是结构体指针*dog
类型的变量都可以赋值给该接口变量。Go语言中有对指针类型变量求值的语法糖,dog
指针fugui
内部会自动求值*fugui
。
package main
import "fmt"
// Mover 实现了包含移动动作的接口
type Mover interface {
move()
}
type dog struct{}
// 使用值接收者实现接口
func (d dog) move() {
fmt.Println("狗会动!")
}
func main() {
var x Mover
var wangcai = dog{} // wangcai是dog类型结构体变量
x = wangcai // x可以接受dog类型
x.move()
var fugui = &dog{} // fugui是*dog类型
x = fugui // x可以接受*dog类型
x.move()
}
代码输出:
指针接收者实现接口
相同的代码:
package main
import "fmt"
// Mover 实现了包含移动动作的接口
type Mover interface {
move()
}
type dog struct{}
// 使用指针接收者实现接口
func (d *dog) move() {
fmt.Println("狗会动!")
}
func main() {
var x Mover
// var wangcai = dog{} // wangcai是dog类型结构体变量
// x = wangcai // x不可以接受dog类型
// x.move()
var fugui = &dog{} // fugui是*dog类型
x = fugui // x可以接受*dog类型
x.move()
}
此时实现Mover
接口的是*dog
类型,所以不能给x
传入dog
类型的变量,只能存储*dog
类型的变量。
接口值
接口值由两部分组成,一个具体的类型和该类型的值,其称为接口的动态类型和动态值。
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
在Go语言中,变量总是被一个明确的值初始化,即使接口类型也不例外,对于一个接口的零值就是它的类型和值部分都是nil
。第一条语句定义了变量w
。
可通过使用w==nil
或者w!=nil
来判断接口值是否为空,调用一个空接口值上的任意方法都会产生panic
:
w.Write([]byte("hello")) // panic: nil pointer dereference
第二个语句将一个*os.File
类型的值赋给变量w
,这个赋值过程调用了一个具体类型到接口类型的隐式转换,这和显式的使用io.Writer(os.Stdout)
是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为*os.File
指针的类型描述符,它的动态值持有os.Stdout的拷贝;这是一个代表处理标准输出的os.File类型变量的指针。
调用一个包含*os.File
类型指针的接口值的Write方法,使得(*os.File).Write
方法被调用。这个调用输出“hello”。
w.Write([]byte("hello")) // "hello"
后面的语句效果类似,可参考Go语言圣经。
接口值可以使用 == 和!=来进行比较。两个接口值相等仅当它们都是nil值,或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。
然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic。
接口与类型的关系
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如,狗可以叫,也可以动。我们就分别定义Sayer
接口和Mover
接口。
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
dog
既可以实现Sayer
接口,也可以实现Mover
接口。
type dog struct {
name string
}
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}
多个类型可以实现同一个接口。
接口嵌套
接口还可以嵌套,从而构成一个新的接口类型,效果如下面的例子所示:
package main
import "fmt"
// Sayer 接口
type Sayer interface {
say(string)
}
// Mover 接口
type Mover interface {
move(string)
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
type cat struct {
name string
}
func (c *cat) say(name string) {
fmt.Printf("%s:喵喵喵~\n", name)
}
func (c *cat) move(name string) {
fmt.Printf("%s:我可是跑的比你快多了哟~\n", name)
}
func main() {
c1 := &cat{
name: "招财猫",
}
var x Sayer = c1
x.say(c1.name)
c2 := &cat{
name: "汤姆猫",
}
var y Mover = c2
y.move(c2.name)
c3 := &cat{
name: "加菲猫",
}
var z animal = c3
z.move(c3.name)
z.say(c3.name)
}
运行结果:
空接口
空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口。因此,空接口类型的变量可以存储任意类型的变量。
package main
import "fmt"
func main() {
// 定义空接口类型
// 可直接将空接口类型简写成interface{}
// type x interface {
// }
// 定义空接口类型变量
var x interface{}
s := "Hello World"
x = s
fmt.Printf("type:%T, value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T, value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T, value:%v\n", x, x)
}
运行结果为:
空接口主要可以用来:作为函数的参数;作为map
的值。
空接口作为函数的参数
使用空接口实现可以接受任意类型的函数参数。
func show(a interface{}){
fmt.Printf("Type:%T Value:%v\n", a, a)
}
空接口作为map
的值
使用空接口可以保存任意值的字典。
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "Tom"
studentInfo["age"] = 23
studentInfo["married"] = false
fmt.Println(studentInfo)
类型断言
要想知道接口所存储的变量的类型,可以使用类型断言,其语法格式为x.(T)
,这里的x
是一个空接口类型的变量,T
表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型相匹配。
在下面的例子中,该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。在这种情况下,不使用ok
的话,断言失败的话会触发panic
,而这里只是将false
写入到ok
,且v
是T
类型的零值。
func main() {
var x interface{}
x = "Hello World"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}
上面的示例中如果要断言多次就需要写多个if
判断,这个时候我们可以使用switch
语句来实现。
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
可参考这篇文章。
并发
在Go语言中,每一个并发的执行单元叫做goroutine
。目前可以简单地把goroutine
类比成一个线程,这样就可以先写出一些正确的程序了,goroutine
和线程的本质区别之后研究。
当一个程序启动时,其主函数即在一个单独的goroutine
中运行,称为main goroutine
。新的goroutine
会用go
语句创建,go
语句是一个普通的函数或方法调用前加上关键字go
,go
语句会使其语句中的函数在一个新创建的goroutine
中运行,而go
语句本身会迅速地完成。
f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
小tips: golang ide里面的几个快捷键,
alt+鼠标左键
可同时选中多行进行编辑;ctrl+D
可以复制当前行;ctrl+x
可以删除当前行;shift+enter
可以快速到下一行进行编辑,即使光标在当前行正中间;alt+shift+up/down
可以将光标所在行的代码上下移动
第一个goroutine
的例子如下代码所示:
package main
import (
"fmt"
"time"
)
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(45)
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
// 递归的方式计算斐波那契数列
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
// time包中对Duration的定义
//type Duration int64
//const (
// Nanosecond Duration = 1
// Microsecond = 1000 * Nanosecond
// Millisecond = 1000 * Microsecond
// Second = 1000 * Millisecond
// Minute = 60 * Second
// Hour = 60 * Minute
//)
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
在这个例子中,动画显示了几秒之后,fib(45)
的调用成功返回,并打印结果。然后主函数返回,主函数返回之后,所有的goroutine
都会被直接打断,程序退出。除了从主函数退出或者直接终止程序之外,没有其他的编程方法能够让一个goroutine
来打断另一个的执行,但是之后可以看到一种方式实现这个目的,通过goroutine
之间的通信来让一个goroutine
请求其他的goroutine
,并让被请求的goroutine
自动结束执行。