Golang学习笔记(05-3-接口)

102 阅读10分钟

1. 接口定义和使用

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

1.1. 定义接口

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

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

1.2. 接口的实现

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

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.3. 接口的使用方式

1.3.1. 使用方式一

  1. 定义一个接口变量,该变量为 nil 。line:21
  2. 使用满足接口的结构体初始化该变量。line:23  line:25
  3. 使用该变量执行接口中的方法。line24  line26
package main

import "fmt"

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, "备份完毕!")
}

func main()  {
	var db backuper
	fmt.Printf("%T %#v\n", db, db)
	db = redis{"Redis","/data/backup/redis"}
	db.backup()
	db = mysql{"MySQL"}
	db.backup()
}
[root@heyingsheng day05]# go run 02-interface/main.go
<nil> <nil>
Redis 备份完毕,备份存在路径: /data/backup/redis
MySQL 备份完毕!

1.3.2. 使用方法二

  1. 定义一个函数,接收的参数为接口类型。line:24
  2. 将实现接口的结构体实例作为参数传递给已经定义的函数。line:31  line:32
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.4. 接口的注意事项

  • 接口是个引用类型,使用var 声明一个变量为某个接口后,变量的值是 nil ,只有该变量指向了实现该接口的自定义类型变量才能完成实例化!
  • 接口中不支持属性的定义,只能定义需要实现的方法,实现这些方法的任务在于各种数据类型
  • 实现接口的数据类型不仅仅可以是结构体,还可以是其它数据类型,但用的最多的是结构体类型

2. 接口的功能

接口的目的是为了屏蔽不同数据类型带来的影响,比如屏蔽redis和mysql两个结构体实例的差异,让一个接口变量能初始化为redis和mysql实例。另外在调用结构体方法中,解决方法对参数的强约问题,让redis和mysql都可以调用同一个函数。

2.1. 接口的方法类型

自定义类型(包括结构体)的方法有两大类,分别是接收者为值和接收者为指针两种。在通常情况下都是建议使用指针类型接收者的方法。由指针接收者方法实现的接口 和 由值接收者方法实现的接口有一些区别。具体如下:

2.1.1. 值类型的方法

用值接收者的方法实现的接口中,可以使用值去调用方法,也可以使用指针去调用。即下面案例中,app类型的变量 ss 既可以初始化为结构体指针,也可以初始化为结构体。之所以可以初始化为指针,是因为接收者为值类型的方法存在语法糖,会自动对指针取值。

package main

import (
	"fmt"
	"strings"
)

type service struct {
	name string
	port uint16
}

// 基于值接收者的方法
func (s service) manager(operate string) {
	switch operate {
	case "start":
		fmt.Printf("%v start! Listen port 0.0.0.0:%d\n", s.name, s.port)
	case "stop":
		fmt.Printf("%s stop!Release port %d\n", s.name, s.port)
	}
}

type app interface {
	manager(string)  // manager() 是值类型接收者实现的方法
}

func main() {
	var ss app
	ss = service{
		name: "sshd",
		port: 22,
	}
	ss.manager("start")  // 结构体值 调用manager(),正常返回
	fmt.Println(strings.Repeat("-",30))
	ss = &service{
		name: "Apache",
		port: 80,
	}
	ss.manager("start")  // 结构体指针 调用manager(),正常返回。语法糖
	fmt.Println(strings.Repeat("-",30))
	nginx := &service{
		name: "Nginx",
		port: 80,
	}
	nginx.manager("stop")  // 结构体指针 调用自己的方法,不走接口,正常返回。语法糖
}

[root@heyingsheng day05]# go run 04-value/main.go
sshd start! Listen port 0.0.0.0:22
------------------------------
Apache start! Listen port 0.0.0.0:80
------------------------------
Nginx stop!Release port 80

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

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

package main

import (
	"fmt"
)

type service struct {
	name string
	port uint16
}

//基于指针接收者的方法
func (s *service)manager(operate string)  {
	switch operate {
	case "start":
		fmt.Printf("%v start! Listen port 0.0.0.0:%d\n", s.name, s.port)
	case "stop":
		fmt.Printf("%s stop!Release port %d\n", s.name, s.port)
	}
}

type app interface {
	manager(string)  // manager() 是值类型接收者实现的方法
}

func main() {
	var ss app
	//ss = service{
	//	name: "sshd",
	//	port: 22,
	//}
	//ss.manager("start")  // 结构体值 调用manager(),异常
	//fmt.Println(strings.Repeat("-",30))
	ss = &service{
		name: "Apache",
		port: 80,
	}
	ss.manager("start")  // 结构体指针 调用manager(),正常返回。
}

2.2. 空接口

2.2.1. 空接口的用途

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

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

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

2.2.2. 空接口的应用

package main

import "fmt"
// 在函数传参中使用
func viewEle(a interface{})  {
	fmt.Printf("%T %#v\n", a, a)
}

func main()  {
	viewEle([]string{"a","b","c"})
	viewEle(false)
	viewEle(100)
}
package main

import "fmt"

var m0 map[uint64]interface{} // 空接口作为map数据类型的value

func main() {
	m0 = make(map[uint64]interface{}, 10)
	m0[0] = "abc"
	m0[1] = [...]string{"张三", "李四"}
	m0[2] = []int{1, 2, 3, 4}
	m0[3] = false

	fmt.Printf("%T %#v\n", m0, m0)
}

2.2.3. 接口的断言

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

  • 通过if条件判断, v, ok := x.(Type) 来判断是否是Type类型,如果是则ok为true,否则为false
  • 通过switch判断, x.(type) 专门用在switch语句种判断数据类型
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] 是其它数据类型

2.4. 接口的嵌套

子接口可以调用父接口的方法,如 line:25 的statusManager实例化的对象可以调用接口 starter 和 stoper 的方法。要想实现子接口,则必须要实现父接口和子接口中所有的方法!
需要注意的是,在嵌套的接口中,两个父类接口中不能有重名的方法,比如这个案例中如果 starter 和 stoper 两个都要实现相同方法 query(),那么 startusManager 接口就会报错!

package main

import "fmt"

type service struct {
	name string
}

func (s *service)start()  {
	fmt.Printf("%s start!\n", s.name)
}

func (s *service)stop()  {
	fmt.Printf("%s stop!\n", s.name)
}

type starter interface {
	start()
}

type stoper interface {
	stop()
}

type statusManager interface{  // 嵌套interface,包含了 starter 和 stoper 两个接口
	starter
	stoper
}

func main()  {
	var app statusManager = &service{"httpd"}
	app.start()  // statusManager 可以调用子接口 starer 的方法
	app.stop()   // statusManager 可以调用子接口 stoper 的方法
}
[root@heyingsheng day05]# go run 06-interface/main.go
httpd start!
httpd stop!

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

3.1. 列表排序

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

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

构建Student结构体切片,并使用 Sort() 进行排序

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())
}
[root@heyingsheng studygo]# go run day06/03-interface/main.go
排序前:[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]
排序后:[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. 对类型进行抽象

package task

import "fmt"

type Cluster struct {
	ID   int
	Name string
	Info string
}

// 构造函数
func NewCluster(name, info string, id int) *Cluster {
	return &Cluster{
		ID:   id,
		Name: name,
		Info: info,
	}
}

// Create
func (c *Cluster) Create() {
	fmt.Printf("create cluster name:%s; id:%d\n", c.Name, c.ID)
}

// Delete
func (c *Cluster) Delete() {
	fmt.Printf("delete cluster name:%s; id:%d\n", c.Name, c.ID)
}

// Get
func (c *Cluster) GetSelf() interface{} {
	fmt.Printf("get cluster name:%s; id:%d\n", c.Name, c.ID)
	return c
}
package task

import "fmt"

type Service struct {
	ID   int
	Name string
	Info string
}

// 构造函数
func NewService(name, info string, id int) *Service {
	return &Service{
		ID:   id,
		Name: name,
		Info: info,
	}
}

// Create
func (s *Service) Create() {
	fmt.Printf("create service name:%s; id:%d\n", s.Name, s.ID)
}

// Delete
func (s *Service) Delete() {
	fmt.Printf("delete service name:%s; id:%d\n", s.Name, s.ID)
}
package task

// 定义接口
type Handler interface {
	Create()
	Delete()
	GetSelf() interface{}
}
package deploy

import "go_learn/day20/task"

type Deployment struct {
	App         task.Handler  // 使用接口抽象 cluster 和 service
	State       string
	DesireState string
}

func NewDeployment(app task.Handler, state, desireState string) *Deployment {
	return &Deployment{
		App: app,
		State: state,
		DesireState: desireState,
	}
}

func (d *Deployment) Run()  {
	d.App.Create()
}

func (d *Deployment) Delete()  {
	d.App.Delete()
}
package main

import (
	"fmt"
	"go_learn/day20/deploy"
	"go_learn/day20/task"
)

func main()  {
	cluster := task.NewCluster("k3s","k3s info", 10001)
	service := task.NewService("nginx","nginx info", 10002)
	deploy.NewDeployment(cluster, "pending", "running").Run()
	deploy.NewDeployment(service,"pending","deleted").Delete()
	fmt.Println()
	fmt.Println(deploy.NewDeployment(cluster, "pending", "running").App.GetSelf().(*task.Cluster).Name)
}
[root@duduniao go_learn]# go run day20/cmd/main.go
create cluster name:k3s; id:10001
delete service name:nginx; id:10002

get cluster name:k3s; id:10001
k3s