go语言接口详解

go语言实现接口的条件

如果一个任意类型 T 的方法集为一个接口类型的方法集的超集,则我们说类型 T 实现了此接口类型。T 可以是一个非接口类型,也可以是一个接口类型。

实现关系在Go语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go语言中没有类似于 implements 的关键字。 Go编译器将自动在需要的时候检查两个类型之间的实现关系。

接口定义后,需要实现接口,调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用。

接口被实现的条件一:接口的方法与实现接口的类型方法格式一致

在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。

为了抽象数据写入的过程,定义 DataWriter 接口来描述数据写入需要实现的方法,接口中的 WriteData() 方法表示将数据写入,写入方无须关心写入到哪里。实现接口的类型实现 WriteData 方法时,会具体编写将数据写入到什么结构中。这里使用file结构体实现 DataWriter 接口的 WriteData 方法,方法内部只是打印一个日志,表示有数据写入,详细实现过程请参考下面的代码。

数据写入器的抽象:

// DataWriter 定义一个数据写入器接口
type DataWriter interface {
	WriteData(data any) error
}
// File 定义文件结构,用于实现DataWriter接口
type File struct {}
// WriteData 实现DateWriter接口的WriteData方法
func (f *File) WriteData(data any) error {
	// 模拟写入数据
	fmt.Println(data)
	return nil
}
func main() {
	// 实例化file
	f := new(File)
	// 声明DataWriter的接口
	var writer DataWriter
	// 将接口赋值f,也就是*File类型
	// 将*File类型的f赋值给DataWriter类型的writer,虽然两个变量类型不一致,但是writer是一个接口
	// 且f已经完全实现了DataWriter接口的所有方法,因此赋值是成功的
	writer = f
	// 使用DataWriter接口进行数据写入
	_ = writer.WriteData("哈哈哈")
}

当类型无法实现接口时,编译器会报错。

接口被实现的条件二:接口中所有方法均被实现

当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
在本节开头的代码中,为 DataWriter中 添加一个方法,代码如下:

  • go语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口,这个设计被称为非侵入式设计。

go语言类型与接口的关系

在Go语言中类型和接口之间有一对多和多对一的关系,下面将列举出这些常见的概念,以方便读者理解接口与类型在复杂环境下的实现关系。

一个类型可以实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。

网络上的两个程序通过一个双向的通信连接实现数据的交换,连接的一端称为一个 Socket。Socket 能够同时读取和写入数据,这个特性与文件类似。因此,开发中把文件和 Socket 都具备的读写特性抽象为独立的读写器概念。

Socket 和文件一样,在使用完毕后,也需要对资源进行释放。

把 Socket 能够写入数据和需要关闭的特性使用接口来描述,请参考下面的代码:

type Socket struct {}
func (s *Socket) Write(p []byte) (int, error) {
	return 0, nil
}
func (s *Socket) Close() error {
	return nil
}

Socket的Write方法实现了io.Writer接口。

type Writer interface {
	Write(p []byte) (n int, err error)
}

同时Socket也实现了io.Closer接口

type Closer interface {
	Close() error
}

使用 Socket 实现的 Writer 接口的代码,无须了解 Writer 接口的实现者是否具备 Closer 接口的特性。同样,使用 Closer 接口的代码也并不知道 Socket 已经实现了 Writer 接口,如下图所示。

在代码中使用 Socket 结构实现的 Writer 接口和 Closer 接口代码如下:

// usingWriter 使用io.Writer的代码,并不知道Socket和io.Closer的存在
func usingWriter(writer io.Writer) {
	_, _ = writer.Write(nil)
}
// usingCloser 使用io.Closer的代码,并不知道Socket和io.Writer的存在
func usingCloser(closer io.Closer) {
	_ = closer.Close()
}
func main() {
	s := new(Socket)
	usingWriter(s)
	usingCloser(s)
}

usingWriter和usingCloser完全独立,互相不知道对方的存在,也不知道自己使用的接口是Socket实现的。

多个类型可以实现相同的接口

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以在类型中嵌入其它类型或者结构体来实现。也就是说使用者,并不关心某个接口的方法是通过一个类型实现的,还是通过多个结构体嵌入到一个结构体中拼凑起来共同实现的。
Service接口定义了两个方法:

  • 一个是开启服务的方法(Start()),
  • 一个是输出日志的方法(Log())。

使用GameService结构体来实现Service,GameService自己的结构只能实现Start()方法,而Service接口中的Log方法已经被一个能输出日志的日志器Logger实现了,无需在进行GameService封装,或者重新实现一遍。所以将Logger嵌入到GameService中能最大程度的减少代码冗余,简化代码结构:

// Service 一个服务需要满足能够开启和写日志的功能
type Service interface {
	Start()  // 开启服务
	Log(string)  // 日志输出
}
// Logger 日志器
type Logger struct {}
// Log 实现Service的Log方法
func (l *Logger) Log(s string) {
	fmt.Println("log", s)
}
// GameService 游戏服务
type GameService struct {
	// 嵌入日志器
	Logger
}
// Start 实现Service的Start方法
func (g *GameService) Start() {
	fmt.Println("game start")
}

此时实例化GameService,并将实例赋值给Service:

func main() {
	var s Service = new(GameService)
	s.Start()
	s.Log("https://mayanan.cn")
}

s就可以使用Start()方法和Log()方法,Start()有GameService实现,Log()方法由Logger实现。

go语言类型断言

类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。
在Go语言中类型断言的语法格式如下:
value, ok := a.(T)
其中x表示一个接口的类型,T表示一个具体的类型(也可以为接口类型)
该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:

  • 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
func main() {
	var x any
	x = 3.14
	value, ok := x.(int)
	fmt.Println(value, ok)
	switch x.(type){
	case int:
		fmt.Println("int类型")
	case string:
		fmt.Println("string类型")
	case bool:
		fmt.Println("bool类型")
	default:
		fmt.Println("未知类型")
	}
}

go语言排序

排序操作和字符串格式化一样是很多程序经常使用的操作。尽管一个最短的快排程序只要 15 行就可以搞定,但是一个健壮的实现需要更多的代码,并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。

幸运的是,sort 包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。

相比之下,Go语言的 sort.Sort 函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型 sort.Interface 来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。

一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是 sort.Interface 的三个方法:

package sort
type Interface interface {
    Len() int            // 获取元素数量
    Less(i, j int) bool // i,j是序列元素的指数。
    Swap(i, j int)        // 交换元素
}

为了对序列进行排序,我们需要定义一个实现了这三个方法的类型,然后对这个类型的一个实例应用 sort.Sort 函数。思考对一个字符串切片进行排序,这可能是最简单的例子了。下面是这个新的类型 MyStringList 和它的 Len,Less 和 Swap 方法

type MyStringList []string
func (m MyStringList) Len() int { return len(m) }
func (m MyStringList) Less(i, j int) bool { return m[i] > m[j] }
func (m MyStringList) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
使用sort.Interface接口进行排序

对一系列字符串进行排序时,使用字符串切片([]string)承载多个字符串。使用 type 关键字,将字符串切片([]string)定义为自定义类型 MyStringList。为了让 sort 包能识别 MyStringList,能够对 MyStringList 进行排序,就必须让 MyStringList 实现 sort.Interface 接口。

下面是对字符串排序的详细代码(代码1):

func main() {
	// 准备一个内容被打乱顺序的字符串切片
	names := MyStringList{
		"3. Triple Kill",
		"5. Penta Kill",
		"2. Double Kill",
		"4. Quadra Kill",
		"1. First Blood",
	}
	sort.Sort(names)
	for _, v := range names {
		fmt.Println(v)
	}
}
常见类型的便捷排序

通过实现 sort.Interface 接口的排序过程具有很强的可定制性,可以根据被排序对象比较复杂的特性进行定制。例如,需要多种排序逻辑的需求就适合使用 sort.Interface 接口进行排序。但大部分情况中,只需要对字符串、整型等进行快速排序。Go语言中提供了一些固定模式的封装以方便开发者迅速对内容进行排序。

  1. 字符串切片的便捷排序:
func main() {
	// 准备一个内容被打乱顺序的字符串切片
	names := []string{
		"3. Triple Kill",
		"5. Penta Kill",
		"2. Double Kill",
		"4. Quadra Kill",
		"1. First Blood",
	}
	sort.Strings(names)
	for _, v := range names {
		fmt.Println(v)
	}
}
sort包中内建的类型排序接口

类 型 实现 sort.lnterface 的类型 直接排序方法 说 明 字符串(String) StringSlice sort.Strings(a [] string) 字符 ASCII 值升序 整型(int) IntSlice sort.Ints(a []int) 数值升序 双精度浮点(float64) Float64Slice sort.Float64s(a []float64) 数值升序 编程中经常用到的 int32、int64、float32、bool 类型并没有由 sort 包实现,使用时依然需要开发者自己编写。

对结构体数据进行排序

除了基本类型的排序,也可以对结构体进行排序。结构体比基本类型更为复杂,排序时不能像数值和字符串一样拥有一些固定的单一原则。结构体的多个字段在排序中可能会存在多种排序的规则,例如,结构体中的名字按字母升序排列,数值按从小到大的顺序排序。一般在多种规则同时存在时,需要确定规则的优先度,如先按名字排序,再按年龄排序等。

  1. 完整实现sort.Interface进行结构体排序
    将一批英雄名单使用结构体定义,英雄名单的结构体中定义了英雄的名字和分类。排序时要求按照英雄的分类进行排序,相同分类的情况下按名字进行排序,详细代码实现过程如下。

结构体排序代码(代码2):

type HeroKind int
const (
	None HeroKind = iota+1
	Tank
	Assassin
	Mage
)
type Hero struct {
	Name string
	Kind HeroKind
}
type Heros []*Hero
func (h Heros) Len() int {
	return len(h)
}
func (h Heros) Less(i, j int) bool {
	if h[i].Kind != h[j].Kind {
		return h[i].Kind < h[j].Kind
	}
	return h[i].Name < h[j].Name
}
func (h Heros) Swap(i, j int) {
	h[i], h[j] = h[j], h[i]
}

func main() {
	// 准备英雄列表
	heros := Heros{
		&Hero{"吕布", Tank},
		&Hero{"李白", Assassin},
		&Hero{"妲己", Mage},
		&Hero{"貂蝉", Assassin},
		&Hero{"关羽", Tank},
		&Hero{"诸葛亮", Mage},
	}
	sort.Sort(heros)
	for _, v := range heros {
		fmt.Println(v)
	}
}

输出结果:

&{关羽 2}
&{吕布 2}  
&{李白 3}  
&{貂蝉 3}  
&{妲己 4}  
&{诸葛亮 4}
  1. 使用sort.Slice进行切片元素的排序
    从 Go 1.8 开始,Go语言在 sort 包中提供了 sort.Slice() 函数进行更为简便的排序方法。sort.Slice() 函数只要求传入需要排序的数据,以及一个排序时对元素的回调函数,类型为 func(i,j int)bool,sort.Slice() 函数的定义如下:
type HeroKind int
const (
	None HeroKind = iota+1
	Tank
	Assassin
	Mage
)
type Hero struct {
	Name string
	Kind HeroKind
}
func main() {
	// 准备英雄列表
	heros := []*Hero{
		&Hero{"吕布", Tank},
		&Hero{"李白", Assassin},
		&Hero{"妲己", Mage},
		&Hero{"貂蝉", Assassin},
		&Hero{"关羽", Tank},
		&Hero{"诸葛亮", Mage},
	}
	sort.Slice(heros, func(i, j int) bool {
		if heros[i].Kind != heros[j].Kind {
			return heros[i].Kind < heros[j].Kind
		}
		return heros[i].Name < heros[j].Name
	})
	for _, hero := range heros {
		fmt.Println(hero)
	}
}
  • 使用sort.Slice不仅可以完成结构体切片排序,还可以对各种切片类型进行自定义排序。

go语言接口的嵌套组合

在Go语言中,不仅结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口。

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。

系统包中的接口嵌套组合

Go语言的 io 包中定义了写入器(Writer)、关闭器(Closer)和写入关闭器(WriteCloser)3 个接口,代码如下:

type Writer interface {
    Write(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}
type WriteCloser interface {
    Writer
    Closer
}
在代码中使用接口进行嵌套组合
type device struct {}
func (d *device) Write(p []byte) (int, error) {
	return 0, nil
}
func (d *device) Close() error {
	return nil
}
func main() {
	// 声明写入关闭器,并赋予device实例
	var wc io.WriteCloser = new(device)
	// 写入数据
	_, _ = wc.Write(nil)
	// 关闭设备
	_ = wc.Close()
	// 声明写入器,并赋予device实例
	var writeOnly io.Writer = new(device)
	_, _ = writeOnly.Write(nil)
}

给 io.WriteCloser 或 io.Writer 更换不同的实现者,可以动态地切换实现代码。

go语言接口和类型之间的转换

go语言中使用接口断言将接口转换成另外一个接口,也可以将接口转换为另外的类型,接口的转换在开发中非常常见,使用也非常频繁。

类型断言的格式
type Flyer interface {
	Fly()
}
type Walker interface {
	Walk()
}
type Bird struct {}
func (b *Bird) Fly() {
	fmt.Println("bird flay")
}
func (b *Bird) Walk() {
	fmt.Println("bird walk")
}
type Pig struct {}
func (p *Pig) Walk() {
	fmt.Println("pig walk")
}
func main() {
	// 创建动物的名字和实例的映射
	animals := map[string]any{
		"bird": new(Bird),
		"pig": new(Pig),
	}
	for name, ani := range animals {
		// 判断是否为飞行动物
		f, isFlyer := ani.(*Bird)
		// 判断是否为行走动物
		w, isWalker := ani.(*Pig)
		fmt.Printf("name: %s, isFlyer: %v, isWalker: %v\n", name, isFlyer, isWalker)
		if isFlyer {
			f.Fly()
		}
		if isWalker {
			w.Walk()
		}
	}
}
将接口转换为其他类型
func main() {
	p1 := new(Pig)
	var a Walker = p1
	p2 := a.(*Pig)
	fmt.Printf("%p, %p\n", p1, p2)  // 0x7c0438, 0x7c0438
}

总结:
接口和其它类型的转换可以在go语言中自由进行,前提是已经完全实现。
接口断言类似于流程控制中的if,但大量类型断言出现时,应使用更为高效的类型分支switch特性。

posted @ 2022-08-30 08:04  专职  阅读(519)  评论(0编辑  收藏  举报