[关闭]
@phper 2018-03-13T10:09:57.000000Z 字数 4522 阅读 650

19.接口(二)

Golang

原文:https://golangbot.com/interfaces-part-2/


欢迎来到 Golang 系列教程的第 19 个教程。接口共有两个教程,这是我们第二个教程。

实现接口:指针接受者与值接受者

在上一章的教程"接口(一)"中,所有示例里面,我们都是使用值接受者(Value Receiver)来实现接口的。我们同样可以使用指针接受者(Pointer Receiver)来实现接口。只不过在用指针接受者实现接口时,还有一些细节需要注意。我们通过下面的代码来理解吧。

  1. package main
  2. import "fmt"
  3. type Describer interface {
  4. Describe()
  5. }
  6. type Person struct {
  7. name string
  8. age int
  9. }
  10. func (p Person) Describe() { // 使用值接受者实现
  11. fmt.Printf("%s is %d years old\n", p.name, p.age)
  12. }
  13. type Address struct {
  14. state string
  15. country string
  16. }
  17. func (a *Address) Describe() { // 使用指针接受者实现
  18. fmt.Printf("State %s Country %s", a.state, a.country)
  19. }
  20. func main() {
  21. var d1 Describer
  22. p1 := Person{"Sam", 25}
  23. d1 = p1
  24. d1.Describe()
  25. p2 := Person{"James", 32}
  26. d1 = &p2
  27. d1.Describe()
  28. var d2 Describer
  29. a := Address{"Washington", "USA"}
  30. /* 如果下面一行取消注释会导致编译错误:
  31. cannot use a (type Address) as type Describer
  32. in assignment: Address does not implement
  33. Describer (Describe method has pointer
  34. receiver)
  35. */
  36. //d2 = a
  37. d2 = &a // 这是合法的
  38. // 因为在第 22 行,Address 类型的指针实现了 Describer 接口
  39. d2.Describe()
  40. }

在线运行程序

在上面程序中的第 13 行,结构体 Person 使用值接受者,实现了 Describer 接口。

我们在讨论方法的时候就已经提到过,使用值接受者声明的方法,既可以用值来调用,也能用指针调用。不管是一个值,还是一个可以解引用的指针,调用这样的方法都是合法的

p1 的类型是 Person,在第 29 行,p1 赋值给了 d1。由于 Person 实现了接口变量 d1,因此在第 30 行,会打印 Sam is 25 years old

接下来在第 32 行,d1 又赋值为 &p2,在第 33 行同样打印输出了 James is 32 years old。棒棒哒。:)

在 22 行,结构体 Address 使用指针接受者实现了 Describer 接口。

在上面程序里,如果去掉第 45 行的注释,我们会得到编译错误:main.go:42: cannot use a (type Address) as type Describer in assignment: Address does not implement Describer (Describe method has pointer receiver)。这是因为在第 22 行,我们使用 Address 类型的指针接受者实现了接口 Describer,而接下来我们试图用 a 来赋值 d2。然而 a 属于值类型,它并没有实现 Describer 接口。你应该会很惊讶,因为我们曾经学习过,使用指针接受者的方法,无论指针还是值都可以调用它。那么为什么第 45 行的代码就不管用呢?

其原因是:对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口中存储的具体值(Concrete Value)并不能取到地址,因此在第 45 行,对于编译器无法自动获取 a 的地址,于是程序报错

第 47 行就可以成功运行,因为我们将 a 的地址 &a 赋值给了 d2

程序的其他部分不言而喻。该程序会打印:

  1. Sam is 25 years old
  2. James is 32 years old
  3. State Washington Country USA

实现多个接口

类型可以实现多个接口。我们看看下面程序是如何做到的。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type SalaryCalculator interface {
  6. DisplaySalary()
  7. }
  8. type LeaveCalculator interface {
  9. CalculateLeavesLeft() int
  10. }
  11. type Employee struct {
  12. firstName string
  13. lastName string
  14. basicPay int
  15. pf int
  16. totalLeaves int
  17. leavesTaken int
  18. }
  19. func (e Employee) DisplaySalary() {
  20. fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
  21. }
  22. func (e Employee) CalculateLeavesLeft() int {
  23. return e.totalLeaves - e.leavesTaken
  24. }
  25. func main() {
  26. e := Employee {
  27. firstName: "Naveen",
  28. lastName: "Ramanathan",
  29. basicPay: 5000,
  30. pf: 200,
  31. totalLeaves: 30,
  32. leavesTaken: 5,
  33. }
  34. var s SalaryCalculator = e
  35. s.DisplaySalary()
  36. var l LeaveCalculator = e
  37. fmt.Println("\nLeaves left =", l.CalculateLeavesLeft())
  38. }

在线运行程序

上述程序在第 7 行和第 11 行分别声明了两个接口:SalaryCalculatorLeaveCalculator

第 15 行定义了结构体 Employee,它在第 24 行实现了 SalaryCalculator 接口的 DisplaySalary 方法,接着在第 28 行又实现了 LeaveCalculator 接口里的 CalculateLeavesLeft 方法。于是 Employee 就实现了 SalaryCalculatorLeaveCalculator 两个接口。

第 41 行,我们把 e 赋值给了 SalaryCalculator 类型的接口变量 ,而在 43 行,我们同样把 e 赋值给 LeaveCalculator 类型的接口变量 。由于 e 的类型 Employee 实现了 SalaryCalculatorLeaveCalculator 两个接口,因此这是合法的。

该程序会输出:

  1. Naveen Ramanathan has salary $5200
  2. Leaves left = 25

接口的嵌套

尽管 Go 语言没有提供继承机制,但可以通过嵌套其他的接口,创建一个新接口。

我们来看看这如何实现。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type SalaryCalculator interface {
  6. DisplaySalary()
  7. }
  8. type LeaveCalculator interface {
  9. CalculateLeavesLeft() int
  10. }
  11. type EmployeeOperations interface {
  12. SalaryCalculator
  13. LeaveCalculator
  14. }
  15. type Employee struct {
  16. firstName string
  17. lastName string
  18. basicPay int
  19. pf int
  20. totalLeaves int
  21. leavesTaken int
  22. }
  23. func (e Employee) DisplaySalary() {
  24. fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
  25. }
  26. func (e Employee) CalculateLeavesLeft() int {
  27. return e.totalLeaves - e.leavesTaken
  28. }
  29. func main() {
  30. e := Employee {
  31. firstName: "Naveen",
  32. lastName: "Ramanathan",
  33. basicPay: 5000,
  34. pf: 200,
  35. totalLeaves: 30,
  36. leavesTaken: 5,
  37. }
  38. var empOp EmployeeOperations = e
  39. empOp.DisplaySalary()
  40. fmt.Println("\nLeaves left =", empOp.CalculateLeavesLeft())
  41. }

在线运行程序

在上述程序的第 15 行,我们创建了一个新的接口 EmployeeOperations,它嵌套了两个接口:SalaryCalculatorLeaveCalculator

如果一个类型定义了 SalaryCalculatorLeaveCalculator 接口里包含的方法,我们就称该类型实现了 EmployeeOperations 接口。

在第 29 行和第 33 行,由于 Employee 结构体定义了 DisplaySalaryCalculateLeavesLeft 方法,因此它实现了接口 EmployeeOperations

在 46 行,empOp 的类型是 EmployeeOperationse 的类型是 Employee,我们把 empOp 赋值为 e。接下来的两行,empOp 调用了 DisplaySalary()CalculateLeavesLeft() 方法。

该程序输出:

  1. Naveen Ramanathan has salary $5200
  2. Leaves left = 25

接口的零值

接口的零值是 nil。对于值为 nil 的接口,其底层值(Underlying Value)和具体类型(Concrete Type)都为 nil

  1. package main
  2. import "fmt"
  3. type Describer interface {
  4. Describe()
  5. }
  6. func main() {
  7. var d1 Describer
  8. if d1 == nil {
  9. fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
  10. }
  11. }

在线运行程序

上面程序里的 d1 等于 nil,程序会输出:

  1. d1 is nil and has type <nil> value <nil>

对于值为 nil 的接口,由于没有底层值和具体类型,当我们试图调用它的方法时,程序会产生 panic 异常。

  1. package main
  2. type Describer interface {
  3. Describe()
  4. }
  5. func main() {
  6. var d1 Describer
  7. d1.Describe()
  8. }

在线运行程序

在上述程序中,d1 等于 nil,程序产生运行时错误 panicpanic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc8527]

接口的介绍到此结束。祝你愉快。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注