go-接口

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。

接口的零值是nil

接口

一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现该接口类型中的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"
type Phone interface {
call(name string)
}
type Person struct {
Name string
}
// 方法
func (p *Persion) Test2(newName string) string {
p.Name = newName
return p.Name
}
// 接口, 把call方法绑到了Person结构上
func (p Person) call(name string) string {
return p.Name + "打电话给" + name
}
func main() {
p1 := Person("Layne")
str := p1.call("Don")
fmt.Println(str) // Layne 大电话给Don
}

结构体赋值给接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
type Person struct {
Name string
}
type notifier interface{
notice()
}
func (p Person) notice(){
fmt.Println(p.Name, "noticing")
}
func main(){
var p notifier // 定义了接口类型变量p
p = Person{"Layne"} // 把Person结构赋值给变量P
p.notice() // Layne noticing
}

如果一个struct实现了interface中的所有方法,那么可以将一个struct的变量赋值给一个interface,只实现一部分方法,那么就不能赋值操作。
这个例子中,如果notifier中还有一个demo()方法,但是person没有实现这个demo(),那么在主函数中执行赋值就会出错,提示没有实现notifier的所有接口

方法集

方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联:

Values Methods Receivers
T (t T)
*T (t T) and (t *T)

上述表格展示了规范里对方法集的描述。描述中说道,T类型的值的方法集值包含值接收者声明的方法。而指向T类型的指针的方法集即可包含值接收者声明的方法,也包含指针接收者声明的方法。

换个角度从接收者的角度来看一下这些规则:

Methods Receivers Values
(t T) t and *T
(t *T) *T

上述表格说明,如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口

代码说明:

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
type notifier interface {
notify()
}
type user struct{
name string
email string
}
// 接收值为user类型的指针
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email)
}
func main() {
u := user{"Bill", "bill@email.com"}
// sendNotification(u) 会编译出错,因为这里传入的是user类型的值接收者
sendNotification(&u) // 正确传入指针接收者
}
// 接收一个实现了notifier接口的值并发送通知
func sendNotification(n notifier) {
n.notify()
}

多态

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
package main
import 'fmt"
type Teacher struct {
Name string
}
type Student struct {
Name string
}
type notifier interface {
notice()
}
func (t Teacher) notice() {
fmt.Println(t.Name, "noticing")
}
func (s Student) notice() {
fmt.Println(t.name, "informing")
}
func sendMsg(n notifier) {
n.notice()
}
func main() {
t := Teacher("Layne")
s := Student("Don")
sendMsg(t) // Layne noticing
sendMsg(s) // Don informing
}

类型断言

类型断言是一个作用在接口值上的操作,写出来类似于x.(T), 其中x是一个接口类型的表达式,而T是一个类型(称为断言类型).类型断言会检查作为操作数的动态类型是否满足指定的断言类型。
这里有两个可能:
首先检查成功,类型断言的结果就是x的动态之,类型当然就是T。换句话说,类型断言就是用来从他的操作数中把具体类型的值提取出来的操作。如果检查失败,那么操作错误:

1
2
3
4
var w io.writer
w = os.Stdout
f := w.(*os.File) // 成功: f == os.Stdout
c := w.(*bytes.Buffer) // 异常: 接口持有的是 *os.File, 不是 *bytes.Buffer

其次,如果断言类型T是一个接口类型,那么类型断言检查x的动态类型是否满足T。如果检查成功,动态值并没有提取出来,结果仍然是一个接口值,接口值的类型和值部门也没有变更,只是结果的类型为接口类型T。
换句话说,类型断言是一个接口值表达式,从一个接口类型变为拥有另外一套方法的接口类型(通常方法数量是增多), 但保留了接口之中的动态类型和动态值部分:

var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter)  // 成功: *os.File 有 Read 和 Writer 方法

w = new(ByteCounter)
rw = w.(io.ReadWriter)   // 异常: *ByteCounter 没有Read方法

无论哪种类型作为断言类型,如果操作数是一个空接口值,断言类型都失败。

小结

  • 多个类型可以实现同一个接口
  • 实现某个接口的类型可以有其他的方法(除了实现接口方法外)
  • 一个类型可以实现多个接口
  • 方法的接收者不区分值还是指针,但实现接口方法的类型区分值和指针