1. 接口定义和使用

Golang在函数和方法的定义中要严格规定参数和返回值的数据类型,但是在部分函数中,比如 fmt.Println() 却可以传入任意类型的变量,要实现这样的功能就必须要通过接口(interface)。

1.1. 定义接口

接口是一种抽象的数据类型接口(interface)是实现一组方法(method)的特殊数据类型,接口只关心方法,而不关心属性

  • 接口名:Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包之外的代码访问
  • 参数:可以和结构体方法一致的格式 name string ,也可以仅使用类型 string
  • 返回值:与函数和方法一致
    1. type interfaceName interface {
    2. methodName1(args)(res)
    3. methodName2(args)(res)
    4. ...
    5. }

    1.2. 接口的实现

    当一个类型的方法满足了接口中定义的所有方法时,称之为这个类型实现了这个接口。如以下的代码中,redis和mysql类型就实现了backuper的接口!Golang中接口不需要显示的声明,即不需要显示的指明某个类型实现了某某接口! ```go type backuper interface { backup() }

type redis struct {name string; dstPath string} type mysql struct {name string}

func (r redis)backup() { fmt.Printf(“%s 备份完毕,备份存在路径: %s\n”, r.name,r.dstPath) }

func (m mysql)backup() { fmt.Println(m.name, “备份完毕!”) }

  1. <a name="1092o"></a>
  2. ### 1.3. 接口的使用方式
  3. <a name="mPM2L"></a>
  4. #### 1.3.1. 使用方式一
  5. 1. 定义一个接口变量,该变量为 nil 。line:21
  6. 1. 使用满足接口的结构体初始化该变量。line:23 line:25
  7. 1. 使用该变量执行接口中的方法。line24 line26
  8. ```go
  9. package main
  10. import "fmt"
  11. type backuper interface {
  12. backup()
  13. }
  14. type redis struct {name string; dstPath string}
  15. type mysql struct {name string}
  16. func (r redis)backup() {
  17. fmt.Printf("%s 备份完毕,备份存在路径: %s\n", r.name,r.dstPath)
  18. }
  19. func (m mysql)backup() {
  20. fmt.Println(m.name, "备份完毕!")
  21. }
  22. func main() {
  23. var db backuper
  24. fmt.Printf("%T %#v\n", db, db)
  25. db = redis{"Redis","/data/backup/redis"}
  26. db.backup()
  27. db = mysql{"MySQL"}
  28. db.backup()
  29. }
  1. [root@heyingsheng day05]# go run 02-interface/main.go
  2. <nil> <nil>
  3. Redis 备份完毕,备份存在路径: /data/backup/redis
  4. MySQL 备份完毕!

1.3.2. 使用方法二

  1. 定义一个函数,接收的参数为接口类型。line:24
  2. 将实现接口的结构体实例作为参数传递给已经定义的函数。line:31 line:32 ```go package main

import “fmt”

type redis struct { name string dstPath string }

func (r redis) backup() { fmt.Printf(“%s 备份完毕,备份存在路径: %s\n”, r.name, r.dstPath) }

type mysql struct{ name string }

func (m mysql) backup() { fmt.Println(m.name, “备份完毕!”) }

type backuper interface { backup() }

func backup(b backuper) { b.backup() }

func main() { redis := redis{“Redis”, “/opt/backup/redis”} mysql := mysql{“MySQL”} backup(redis) backup(mysql) }

  1. <a name="CKcEW"></a>
  2. ### 1.4. 接口的注意事项
  3. - 接口是个引用类型,使用var 声明一个变量为某个接口后,变量的值是 nil ,只有该变量指向了实现该接口的自定义类型变量才能完成实例化!
  4. - 接口中不支持属性的定义,只能定义需要实现的方法,实现这些方法的任务在于各种数据类型
  5. - 实现接口的数据类型不仅仅可以是结构体,还可以是其它数据类型,但用的最多的是结构体类型
  6. ---
  7. <a name="tqYFN"></a>
  8. ## 2. 接口的功能
  9. 接口的目的是为了屏蔽不同数据类型带来的影响,比如屏蔽redis和mysql两个结构体实例的差异,让一个接口变量能初始化为redis和mysql实例。另外在调用结构体方法中,解决方法对参数的强约问题,让redis和mysql都可以调用同一个函数。
  10. <a name="iRLYc"></a>
  11. ### 2.1. 接口的方法类型
  12. 自定义类型(包括结构体)的方法有两大类,分别是接收者为值和接收者为指针两种。在通常情况下都是建议使用指针类型接收者的方法。由指针接收者方法实现的接口 和 由值接收者方法实现的接口有一些区别。具体如下:
  13. <a name="Mx6tO"></a>
  14. #### 2.1.1. 值类型的方法
  15. 用值接收者的方法实现的接口中,可以使用值去调用方法,也可以使用指针去调用。即下面案例中,app类型的变量 ss 既可以初始化为结构体指针,也可以初始化为结构体。之所以可以初始化为指针,是因为接收者为值类型的方法存在语法糖,会自动对指针取值。
  16. ```go
  17. package main
  18. import (
  19. "fmt"
  20. "strings"
  21. )
  22. type service struct {
  23. name string
  24. port uint16
  25. }
  26. // 基于值接收者的方法
  27. func (s service) manager(operate string) {
  28. switch operate {
  29. case "start":
  30. fmt.Printf("%v start! Listen port 0.0.0.0:%d\n", s.name, s.port)
  31. case "stop":
  32. fmt.Printf("%s stop!Release port %d\n", s.name, s.port)
  33. }
  34. }
  35. type app interface {
  36. manager(string) // manager() 是值类型接收者实现的方法
  37. }
  38. func main() {
  39. var ss app
  40. ss = service{
  41. name: "sshd",
  42. port: 22,
  43. }
  44. ss.manager("start") // 结构体值 调用manager(),正常返回
  45. fmt.Println(strings.Repeat("-",30))
  46. ss = &service{
  47. name: "Apache",
  48. port: 80,
  49. }
  50. ss.manager("start") // 结构体指针 调用manager(),正常返回。语法糖
  51. fmt.Println(strings.Repeat("-",30))
  52. nginx := &service{
  53. name: "Nginx",
  54. port: 80,
  55. }
  56. nginx.manager("stop") // 结构体指针 调用自己的方法,不走接口,正常返回。语法糖
  57. }
  1. [root@heyingsheng day05]# go run 04-value/main.go
  2. sshd start! Listen port 0.0.0.0:22
  3. ------------------------------
  4. Apache start! Listen port 0.0.0.0:80
  5. ------------------------------
  6. Nginx stop!Release port 80

2.1.2. 指针类型的方法(常用)

因为指针接收者的方法较为常用,因此使用指针接收者的方法实现的接口也比较常用。指针类型方法实现的接口,不能兼容值类型结构体对象!

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type service struct {
  6. name string
  7. port uint16
  8. }
  9. //基于指针接收者的方法
  10. func (s *service)manager(operate string) {
  11. switch operate {
  12. case "start":
  13. fmt.Printf("%v start! Listen port 0.0.0.0:%d\n", s.name, s.port)
  14. case "stop":
  15. fmt.Printf("%s stop!Release port %d\n", s.name, s.port)
  16. }
  17. }
  18. type app interface {
  19. manager(string) // manager() 是值类型接收者实现的方法
  20. }
  21. func main() {
  22. var ss app
  23. //ss = service{
  24. // name: "sshd",
  25. // port: 22,
  26. //}
  27. //ss.manager("start") // 结构体值 调用manager(),异常
  28. //fmt.Println(strings.Repeat("-",30))
  29. ss = &service{
  30. name: "Apache",
  31. port: 80,
  32. }
  33. ss.manager("start") // 结构体指针 调用manager(),正常返回。
  34. }

2.2. 空接口

2.2.1. 空接口的用途

对于 fmt.Println() 函数能接收任意类型的值作为参数,原因是源码中接收的参数为 a ...inteface{} ,表示支持不定长传参,参数类型为 interface{}interface{} 表示空接口,即没有方法约束的接口,因此所有数据类型都实现了空接口,进而空接口类型的变量可以初始化为任意数据类型。这样就类似于Python一样,解决了参数类型的限制。空接口本身也是一种数据类型!

  1. func Println(a ...interface{}) (n int, err error) {
  2. return Fprintln(os.Stdout, a...)
  3. }

再比如,在定义映射的时候,由于数据类型的强制约束导致能定义的字段非常少,往往不能满足开发需求,采用空接口屏蔽数据类型的影响是一个比较简单的做法。

2.2.2. 空接口的应用

  1. package main
  2. import "fmt"
  3. // 在函数传参中使用
  4. func viewEle(a interface{}) {
  5. fmt.Printf("%T %#v\n", a, a)
  6. }
  7. func main() {
  8. viewEle([]string{"a","b","c"})
  9. viewEle(false)
  10. viewEle(100)
  11. }
  1. package main
  2. import "fmt"
  3. var m0 map[uint64]interface{} // 空接口作为map数据类型的value
  4. func main() {
  5. m0 = make(map[uint64]interface{}, 10)
  6. m0[0] = "abc"
  7. m0[1] = [...]string{"张三", "李四"}
  8. m0[2] = []int{1, 2, 3, 4}
  9. m0[3] = false
  10. fmt.Printf("%T %#v\n", m0, m0)
  11. }

2.2.3. 接口的断言

在开发中普遍存在多种数据类型都是实现了相同的接口,那么多种数据类型的实例都可以赋值给这个接口的变量,当我们需要将接口变量还原为原本的数据数据类型时,必须要通过断言判断后才能实现!如以下场景:
在空接口中可以传递任意数据类型的数据,当我们需要判断当前传进来的是哪种数据类型,则需要通过断言来实现,即判断传进来的参数属于哪种类型。断言有两种方式:

  • 通过if条件判断, v, ok := x.(Type) 来判断是否是Type类型,如果是则ok为true,否则为false
  • 通过switch判断, x.(type) 专门用在switch语句种判断数据类型 ```go package main

import “fmt”

func f0(a interface{}) { //, ok := a.(string) //if ok { // fmt.Println(a, “是一个字符串”) //} else { // fmt.Println(a, “不是一个字符串”) //} // 上述方法可以简写为如下形式: if , ok := a.(string); ok { fmt.Println(a, “是一个字符串”) } else { fmt.Println(a, “不是一个字符串”) } }

func f1(a interface{}) { switch v := a.(type) { case string: fmt.Println(v, “是一个字符串”) case int8, uint8, int16, uint16, int32, uint32, int64, uint64, int: fmt.Println(v, “是一个整数”) case float32, float64: fmt.Println(v, “是一个浮点数”) case bool: fmt.Println(v, “是一个布尔值”) default: fmt.Println(v, “是其它数据类型”) } }

func main() { f0(“abc”) f0(false) f1(123456789) f1(uint64(123456789)) f1(“武汉加油!”) f1([]int{1, 2, 3, 4}) }

[root@heyingsheng studygo]# go run day06/03-interface/main.go abc 是一个字符串 false 不是一个字符串 123456789 是一个整数 123456789 是一个整数 武汉加油! 是一个字符串 [1 2 3 4] 是其它数据类型

  1. <a name="cbuu8"></a>
  2. ### 2.4. 接口的嵌套
  3. 子接口可以调用父接口的方法,如 line:25 的statusManager实例化的对象可以调用接口 starter 和 stoper 的方法。要想实现子接口,则必须要实现父接口和子接口中所有的方法!<br />需要注意的是,在嵌套的接口中,两个父类接口中不能有重名的方法,比如这个案例中如果 starter 和 stoper 两个都要实现相同方法 query(),那么 startusManager 接口就会报错!
  4. ```go
  5. package main
  6. import "fmt"
  7. type service struct {
  8. name string
  9. }
  10. func (s *service)start() {
  11. fmt.Printf("%s start!\n", s.name)
  12. }
  13. func (s *service)stop() {
  14. fmt.Printf("%s stop!\n", s.name)
  15. }
  16. type starter interface {
  17. start()
  18. }
  19. type stoper interface {
  20. stop()
  21. }
  22. type statusManager interface{ // 嵌套interface,包含了 starter 和 stoper 两个接口
  23. starter
  24. stoper
  25. }
  26. func main() {
  27. var app statusManager = &service{"httpd"}
  28. app.start() // statusManager 可以调用子接口 starer 的方法
  29. app.stop() // statusManager 可以调用子接口 stoper 的方法
  30. }
  1. [root@heyingsheng day05]# go run 06-interface/main.go
  2. httpd start!
  3. httpd stop!

3. 接口的使用场景和案例

3.1. 列表排序

以上描述的是接口的定义和使用语法,但是对初学者而言,最不好掌握的是接口的应用场景。通过案例来熟悉接口应用!

  • 问题:定义结构体Student,其包含字段: Sid, Name, Age, Score;要求对 []Student{} 按照考试成绩 Score倒叙排列!
  • 思路:除了使用排序算法之外,Golang 的 sort 包中提供了 func Sort(data Interface) 函数,可以对实现了 Interface 接口的data变量进行排序! Interface 接口的定义如下:
    1. type Interface interface {
    2. // Len方法返回集合中的元素个数
    3. Len() int
    4. // Less方法报告索引i的元素是否比索引j的元素小
    5. Less(i, j int) bool
    6. // Swap方法交换索引i和j的两个元素
    7. Swap(i, j int)
    8. }
    构建Student结构体切片,并使用 Sort() 进行排序 ```go package main

import ( “fmt” “math/rand” “sort” )

type Student struct { Name string Score int }

type SliceStudent []*Student

func (s SliceStudent)Print() string { var res string for _, v := range s { res += fmt.Sprintf(“[name=%v, score=%v] “, v.Name, v.Score) } return res }

// 以下三个方法是提供给 sort.Sort() 函数使用 func (s SliceStudent)Len() int{ return len(s) }

func (s SliceStudent)Less(i, j int) bool { if s[i].Score > s[j].Score { return true } return false }

func (s SliceStudent)Swap(i, j int) { s[i], s[j] = s[j], s[i] }

func main() { var s0 = make(SliceStudent, 0, 5) for i:=0; i<5; i++ { s0 = append(s0, &Student{fmt.Sprintf(“stud-%d”, i+1), rand.Intn(100)}) } fmt.Printf(“排序前:%v\n”, s0.Print()) sort.Sort(s0) fmt.Printf(“排序后:%v\n”, s0.Print()) }

  1. ```
  2. [root@heyingsheng studygo]# go run day06/03-interface/main.go
  3. 排序前:[name=stud-1, score=81] [name=stud-2, score=87] [name=stud-3, score=47] [name=stud-4, score=59] [name=stud-5, score=81]
  4. 排序后:[name=stud-2, score=87] [name=stud-1, score=81] [name=stud-5, score=81] [name=stud-4, score=59] [name=stud-3, score=47]

3.2. 对类型进行抽象

  1. package task
  2. import "fmt"
  3. type Cluster struct {
  4. ID int
  5. Name string
  6. Info string
  7. }
  8. // 构造函数
  9. func NewCluster(name, info string, id int) *Cluster {
  10. return &Cluster{
  11. ID: id,
  12. Name: name,
  13. Info: info,
  14. }
  15. }
  16. // Create
  17. func (c *Cluster) Create() {
  18. fmt.Printf("create cluster name:%s; id:%d\n", c.Name, c.ID)
  19. }
  20. // Delete
  21. func (c *Cluster) Delete() {
  22. fmt.Printf("delete cluster name:%s; id:%d\n", c.Name, c.ID)
  23. }
  24. // Get
  25. func (c *Cluster) GetSelf() interface{} {
  26. fmt.Printf("get cluster name:%s; id:%d\n", c.Name, c.ID)
  27. return c
  28. }
  1. package task
  2. import "fmt"
  3. type Service struct {
  4. ID int
  5. Name string
  6. Info string
  7. }
  8. // 构造函数
  9. func NewService(name, info string, id int) *Service {
  10. return &Service{
  11. ID: id,
  12. Name: name,
  13. Info: info,
  14. }
  15. }
  16. // Create
  17. func (s *Service) Create() {
  18. fmt.Printf("create service name:%s; id:%d\n", s.Name, s.ID)
  19. }
  20. // Delete
  21. func (s *Service) Delete() {
  22. fmt.Printf("delete service name:%s; id:%d\n", s.Name, s.ID)
  23. }
  1. package task
  2. // 定义接口
  3. type Handler interface {
  4. Create()
  5. Delete()
  6. GetSelf() interface{}
  7. }
  1. package deploy
  2. import "go_learn/day20/task"
  3. type Deployment struct {
  4. App task.Handler // 使用接口抽象 cluster 和 service
  5. State string
  6. DesireState string
  7. }
  8. func NewDeployment(app task.Handler, state, desireState string) *Deployment {
  9. return &Deployment{
  10. App: app,
  11. State: state,
  12. DesireState: desireState,
  13. }
  14. }
  15. func (d *Deployment) Run() {
  16. d.App.Create()
  17. }
  18. func (d *Deployment) Delete() {
  19. d.App.Delete()
  20. }
  1. package main
  2. import (
  3. "fmt"
  4. "go_learn/day20/deploy"
  5. "go_learn/day20/task"
  6. )
  7. func main() {
  8. cluster := task.NewCluster("k3s","k3s info", 10001)
  9. service := task.NewService("nginx","nginx info", 10002)
  10. deploy.NewDeployment(cluster, "pending", "running").Run()
  11. deploy.NewDeployment(service,"pending","deleted").Delete()
  12. fmt.Println()
  13. fmt.Println(deploy.NewDeployment(cluster, "pending", "running").App.GetSelf().(*task.Cluster).Name)
  14. }
  1. [root@duduniao go_learn]# go run day20/cmd/main.go
  2. create cluster name:k3s; id:10001
  3. delete service name:nginx; id:10002
  4. get cluster name:k3s; id:10001
  5. k3s