2、接口

接口

接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。

Go 语言中使用组合实现对象特性的描述。对象的内部使用结构体内嵌组合对象应该具有的特性,对外通过接口暴露能使用的特性。

Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。

定义接口

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
}

Go语言的每个接口中的方法数量不会很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。

实现接口

类似于多态(但不是),File类型指针,可以赋值给DataWriter接口类型的变量,因为已经完全实现了接口方法

package main

import "fmt"

// DataWriter 定义接口
type DataWriter interface {
	write(data interface{})
}

// File 实现构造体
type File struct {
}

// write 实现接口方法
func (f File) write(data interface{}) {
	fmt.Println(data)
}

func main() {
	var w DataWriter = new(File)
	w.write("你好")
}

实现接口的条件

接口的方法与实现接口的类型方法格式一致

在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表

接口中所有方法均被实现

当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。

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

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

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

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

把 Socket 能够写入数据和需要关闭的特性使用接口来描述:

type Socket struct {
    
}
func (s *Socket) Write(p []byte) (n int, err 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 接口,如下图所示:

img

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

例如,现在需要有一个接口,这个接口可以实现服务的开启和日志功能,而日志功能是多个服务通用的,只有开启功能需要根据服务的不同来定义,每个服务都有这两个功能,那么就可以这么去做

package main

import "fmt"

//Service 服务接口
type Service interface {
	start()
	log()
}

// Logger 通用的日志处理器
type Logger struct{}

func (log *Logger) log() {
	fmt.Println("通用日志服务")
}

// GameService 游戏服务
type GameService struct {
	//内嵌日志处理器
	Logger
}

//实现游戏服务的启动功能
func (gs *GameService) start() {
	fmt.Printf("开启游戏服务")
}

func main() {
	var service Service = new(GameService)
	service.log()
	service.start()
}

此时,如果再新增服务的话,只需要内嵌日志处理器然后再实现服务特有的启动功能,就可以了

接口和nil

nil 在 Go语言中只能被赋值给指针和接口。接口在底层的实现有两个部分:type 和 data。

在源码中,显式地将 nil 赋值给接口时,接口的 type 和 data 都将为 nil。此时,接口与 nil 值判断是相等的。

但如果将一个带有类型的 nil 赋值给接口时,只有 data 为 nil,而 type 为 nil,此时,接口与 nil 判断将不相等。

类型断言

类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型,也可以作为接口和具体类型直接转换的方法,语法格式如下:

value, ok := x.(T)

x 表示一个接口的类型变量,T 表示一个具体的类型(也可为接口类型)。

该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型

注意:需要注意如果不接收第二个参数也就是上面代码中的 ok,断言失败时会直接造成一个 panic,也就是说,如果断言成功,就可以只接一个值,就是value

package main

import "fmt"

func main() {
	var a interface{} = 10
	var b interface{} = "hello"
	v1, o1 := a.(int)
	v2, o2 := b.(int)
	fmt.Println(v1, o1) //10 true
	fmt.Println(v2, o2) //0 false
}

空接口

interface{}
any
//在1.18后,可以使用any来表示空接口,源码中是对空接口做了别名
type any = interface{}

空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。

空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。

空接口比较

Go 语言中的空接口在保存不同的值后,可以和其他变量一样使用==进行比较操作。

类 型 说 明
map 不可比较,如果比较,程序会报错
切片(slice) 不可比较,如果比较,程序会报错
通道(channel) 可比较,必须由同一个 make 生成,也就是同一个通道才会是true,否则为 false
数组 可比较,编译期知道两个数组是否一致
结构体 可比较,可以逐个比较结构体的值
函数 可比较

类型分支

type-switch 流程控制的语法或许是Go语言中最古怪的语法。 它可以被看作是类型断言的增强版。它和 switch-case 流程控制代码块有些相似。

switch 接口变量.(type) {
    case 类型1:
        // 变量是类型1时的处理
    case 类型2:
        // 变量是类型2时的处理
    …
    default:
        // 变量不是所有case中列举的类型时的处理
}
package main

import "fmt"

func main() {
	var a interface{} = "hello"
	switch a.(type) {
	case int:
		fmt.Printf("a is int type,value = %s", a)
	case string:
		fmt.Printf("a is string type,value = %s", a)
	case float64:
		fmt.Printf("a is float64 type,value = %s", a)
	}

	//a is string type,value = hello
}