使用指针接收器和值接收器实现接口


我们在第 1 部分中讨论的所有示例接口都是使用值接收器实现的。也可以使用指针接收器实现接口。在使用指针接收器实现接口时需要注意一些细微之处。

  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() { //implemented using value receiver
  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() { //implemented using pointer receiver
  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. /* compilation error if the following line is
  31. uncommented
  32. cannot use a (type Address) as type Describer
  33. in assignment: Address does not implement
  34. Describer (Describe method has pointer
  35. receiver)
  36. */
  37. //d2 = a
  38. d2 = &a //This works since Describer interface
  39. //is implemented by Address pointer in line 22
  40. d2.Describe()
  41. }

Run in playground

在上面的程序中,PersonPerson 结构使用第 13 行中的值接收器实现 Describer 接口。

正如我们在讨论方法时已经学到的那样,带有值接收器的方法同时接受指针和值接收器。对任何值或者可以取消引用的值,调用值方法都是合法的。

_p1
是 Person 类型的值,它在第 29 行中分配给 d1。Person 实现了 d1 接口,因此第30 行输出 Sam is 25 years old。

类似地,在 32 行将 d1 分配给 &p2。第 33 行将输出 James is 32 years old。太棒了:)。

Address 结构在第 22 行中使用的指针接收器实现 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)。这是因为,Describer 接口是使用第 22 行中的地址指针接收器实现的,我们正在尝试分配一个值类型 a,但它没有实现 Describer 接口。这肯定会让你感到惊讶,因为我们之前已经知道带有指针接收器的方法将同时接受指针和值接收器。那么为什么第 45 行代码不能工作。

原因是在任何对已经是指针或可以获取其地址的任何对象调用指针值方法都是合法的。存储在接口中的具体值不可寻址,因此编译器无法自动获取 a 的地址。 因此第 45 行这段代码不能工作。
**
第 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. }

Run in playground

上面的程序第 7 和 11 行声明了两个接口 SalaryCalculatorLeaveCalculator

第 15 行 Employee 结构定义了在第 24 行 SalaryCalculator 接口的 DisplaySalary 方法和第 28 行 LeaveCalculator 接口的 CalculateLeavesLeft 方法的实现。 现在,Employee 实现了 SalaryCalculatorLeaveCalculator 接口。

第 41 行我们将 e 分配给 SalaryCalculator 类型的变量,在第 43 行并将相同的变量 e 分配给 LeaveCalculator 类型的变量。 这是允许的,因为 Employee 类型 e 实现了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. }

Run in playground

上面程序的第 15 行中的 EmployeeOperations 接口是通过嵌入 SalaryCalculatorLeaveCalculator 接口创建的。

如果它为 SalaryCalculatorLeaveCalculator 接口中提供的方法提供方法定义,则称任何类型都实现 EmployeeOperations 接口。

Employee 结构实现了 EmployeeOperations 接口,因为它分别为第 29 行和第 33 行中的 DisplaySalaryCalculateLeavesLeft 方法提供了定义。

在第 46 行,类型为 Employeee 被分配给 EmployeeOperations 类型的 empOp 。在接下来的两行中,在 empOp 上调用 DisplaySalary()CalculateLeavesLeft()方法。

该程序将输出

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

接口零值


接口的零值为零。 nil 接口既有底层值,也有具体类型为 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. }

Run in playground

上述程序中的 d1nil,此程序将输出

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

如果我们尝试在 nil 接口上调用方法,程序将会发生panic,因为 nil 接口既没有底层值也没有具体类型。

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

Run in playground

由于上面程序中的 d1nil,因此程序会因运行时报错 panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc8527]”

原文链接

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