当前位置:   article > 正文

Go 学习笔记(35)— Go 接口 interface (接口声明、接口初始化、接口方法调用、接口运算、类型断言、类型查询、空接口)_interface 初始化

interface 初始化

1. 接口概念

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

接口内部存放的具体类型变量被称为接口指向的“实例”。接口只有声明没有实现,所以定义一个新接口,通常又变成声明一个新接口, 定义接口和声明接口二者通用,代表相同的意思。

最常使用的接口字面量类型就是空接口 interface{} ,由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型的实例。

Go 接口背后的本质是一种“契约”,通过契约我们可以将代码双方的耦合降至最低。Go 惯例上推荐尽量定义小接口,一般而言接口方法集合中的方法个数不要超过三个,单一方法的接口更受 Go 社区青睐。小接口有诸多优点,比如,抽象程度高、易于测试与实现、与组合的设计思想一脉相承、鼓励你编写组合的代码,等等。

这种“小接口”的 Go 惯例也已经被 Go 社区项目广泛采用。作者统计了早期版本的 Go 标准库(Go 1.13 版本)、Docker 项目(Docker 19.03 版本)以及 Kubernetes 项目(Kubernetes 1.17 版本)中定义的接口类型方法集合中方法数量,你可以看下:
interface从图中我们可以看到,无论是 Go 标准库,还是 Go 社区知名项目,它们基本都遵循了“尽量定义小接口”的惯例,接口方法数量在 1~3 范围内的接口占了绝大多数。

图片来源:https://time.geekbang.org/column/article/471952

注意:非命名类型由于不能定义自己的方法, 所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口。

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口

/* 定义接口 */
type interface_name interface {
    method_name1 [return_type]
    method_name2 [return_type]
    method_name3 [return_type]
    ...
    method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
    /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_t
ype] {
    /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_t
ype] {
    /* 方法实现*/
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

使用示例:

package main

import (
	"fmt"
)

type Phone interface {
	call()
}

type NokiaPhone struct {
}
type IPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
	fmt.Println("I am Nokia, I can call you!")
}

func (iPhone IPhone) call() {
	fmt.Println("I am iPhone, I can call you!")
}
func main() {
	var phone Phone
	phone = new(NokiaPhone)
	phone.call()	// I am Nokia, I can call you!
	phone = new(IPhone)
	phone.call()	// I am iPhone, I can call you!
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

2. 接口声明

Go 语言的接口分为接口字面量类型和接口命名类型, 接口的声明使用 interface 关键字。

接口字面量类型的声明语法如下:

interface {
	MethodSignature1
	MethodSignature2
}
  • 1
  • 2
  • 3
  • 4

使用接口字面量的场景很少,一般只有空接口 interface{} 类型变量的声明才会使用。

接口命名类型使用 type 关键字声明,语法如下:

type InterfaceName interface {
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
}
  • 1
  • 2
  • 3
  • 4

对各个部分的说明:

  • 接口类型名:使用 type 将接口定义为自定义的类型名。 Go 语言的接口在命名时,一般会在单词后面添加 er ,如有写操作的接口叫 Writer ,有字符串功能的接口叫 Stringer ,有关闭功能的接口叫 Closer 等;
  • 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问;
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略;

2.1 方法声明

接口定义使用方法声明,而不是方法签名,因为方法名是接口的组成部分。例如:

// 方法声明=方法名+方法签名
MethodName (InputTypeList )OutputTypeList
  • 1
  • 2

接口中的“方法声明”非常类似于 C 语言中的函数声明的概念, Go 编译器在做接口匹配判断时是严格校验方法名称和方法签名的。

接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混合。接口支持嵌入匿名接口宇段,就是一个接口定义里面可以包括其他接口, Go 编译器会自动进行展开处理,有点类似 C 语言中宏的概念。例如:

type Reader interface {
    Read(p []byte) (n int , err error)
}

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

// 如下3 种声明是等价的,最终的展开模式都是第 3 种格式
type ReadWriter interface {
    Reader
    Writer
}

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

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

Writer 这个接口可以调用 Write() 方法写入一个字节数组( []byte ),返回值告知写入字节数( n int )和可能发生的错误( err error

我们在接口类型的方法集合中声明的方法,它的参数列表不需要写出形参名字,返回值列表也是如此。也就是说,方法的参数列表中形参名字与返回值列表中的具名返回值,都不作为区分两个方法的凭据。

type MyInterface interface {
    M1(int) error
    M2(io.Writer, ...string)
}
  • 1
  • 2
  • 3
  • 4

比如下面的 MyInterface 接口类型的定义与上面的 MyInterface 接口类型定义都是等价的:

type MyInterface interface {
    M1(a int) error
    M2(w io.Writer, strs ...string)
}

type MyInterface interface {
    M1(n int) error
    M2(w io.Writer, args ...string)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

不过,Go 语言要求接口类型声明中的方法必须是具名的,并且方法名字在这个接口类型的方法集合中是唯一的。

Go 1.14 版本以后,Go 接口类型允许嵌入的不同接口类型的方法集合存在交集,但前提是交集中的方法不仅名字要一样,它的函数签名部分也要保持一致,也就是参数列表与返回值列表也要相同,否则 Go 编译器照样会报错。

比如下面示例中 Interface3 嵌入了 Interface1Interface2,但后两者交集中的 M1 方法的函数签名不同,导致了编译出错:

type Interface1 interface {
    M1()
}
type Interface2 interface {
    M1(string) 
    M2()
}

type Interface3 interface{
    Interface1
    Interface2 // 编译器报错:duplicate method M1
    M3()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2.2 新接口类型声明特点

  1. 接口的命名一般以“ er ”结尾;
  2. 接口定义的内部方法声明不需要 func 引导;
  3. 在接口定义中,只有方法声明没有方法实现;

3. 接口初始化

接口类型一旦被定义后,它就和其他 Go 类型一样可以用于声明变量,比如:

var err error   // err是一个error接口类型的实例变量
var r io.Reader // r是一个io.Reader接口类型的实例变量
  • 1
  • 2

接口只有被初始化为具体的类型时才有意义。没有初始化的接口变量,其默认值是 nil

var i io.Reader
fmt.Printf("%T\n", i)	// nil
  • 1
  • 2

这些类型为接口类型的变量被称为接口类型变量,如果没有被显式赋予初值,接口类型变量的默认值为 nil。如果要为接口类型变量显式赋予初值,我们就要为接口类型变量选择合法的右值。

接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接初始化方法, 具体如下。

3.1 实例赋值接口

如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查。接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。

3.2 接口变量赋值接口变量

已经初始化的接口类型变量 a 直接赋值给另一种接口变量 b ,要求 b 的方法集是 a 的方法集的子集。此时 Go 编译器会在编译时进行方法集静态检查。这个过程也是接口初始化的一种方式,此时接口变量 b 绑定的具体实例是接口变量 a 绑定的具体实例的副本。例如:

file , := os .OpenFile ( "notes.txt", os .O_RDWR | os .O_CREATE , 0755 )
var rw io.ReadWriter = file
//io.ReadWriter 接口可以直接赋值给 io.Writer 接口变量
var w io.Writer = rw
  • 1
  • 2
  • 3
  • 4

4. 接口方法调用

接口方法调用和普通的函数调用是有区别的。接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。接口方法调用不是一种直接的调用,有一定的运行时开销。

直接调用未初始化的接口变量的方法会产生 panic 。例如:

package main

type Printer interface {
	Print()
}

type S struct{}

func (s S) Print() {
	println("print")
}

func main() {
	var i Printer
	// 没有初始化的接口调用其方法会产生 panic
	// panic: runtime error: invalid memory address or nil pointer dereference
	// i.Print()

	// i 必须初始化
	i = S{}
	i.Print()
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

5. 接口运算

编程过程中有时需要确认已经初始化的接口变量指向实例的具体类型是什么,也需要检查运行时的接口类型。 Go 语言提供两种语法结构来支持这两种需求,分别是类型断言和类型查询。

Go 的语言中提供了断言的功能。Go 中的所有程序都实现了 interface{} 的接口,这意味着,所有的类型如 stringintint64 甚至是自定义的 struct 类型都就此拥有了 interface{} 的接口,那么在一个数据通过 func funcName(interface{}) 的方式传进来的时候,也就意味着这个参数被自动的转为 interface{} 的类型。

func funcName(a interface{}) string {
    return string(a)
}
  • 1
  • 2
  • 3

编译器报错:

cannot convert a (type interface{}) to type string: need type as
sertion
  • 1
  • 2

此时,意味着整个转化的过程需要类型断言。

5.1 类型断言

接口类型断言的语法形式如下:

var i interface
i.(T)
  • 1
  • 2
  • i 必须是接口变量,如果是具体类型变量,则编译器会报 non - interface type xxx on left
  • T 可以是接口类型名,也可以是具体类型名。

那么这句代码的含义就是断言存储在接口类型变量 i 中的值的类型为 T

func main() {
	a := 1
	v := a.(int)
	fmt.Println(a)
}
  • 1
  • 2
  • 3
  • 4
  • 5

报错:

invalid type assertion: a.(int) (non-interface type int on left)
  • 1

修改后的代码:

func main() {
	a := 1
	v, ok := interface{}(a).(int)	// 将 a 转换为接口类型
	if ok {
		fmt.Printf("v type is %T\n", v)
	}
	fmt.Println(a)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Go 语言中,interface{} 代表空接口,任何类型都是它的实现类型。现在你只要知道,任何类型的值都可以很方便地被转换成空接口的值就行了。

你可能会对这里的 {} 产生疑惑,为什么在关键字 interface 的右边还要加上这个东西?

请记住,一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。

比如你今后肯定会遇到的 struct{},它就代表了不包含任何字段和方法的、空的结构体类型。而空接口 interface{} 则代表了不包含任何方法定义的、空的接口类型。

当然了,对于一些集合类的数据类型来说,{} 还可以用来表示其值不包含任何元素,比如空的切片值 []string{},以及空的字典值 map[int]string{}

接口查询的两层语义

  1. 如果 TypeNname 是一个具体类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否就是具体类型 TypeNname
  2. 如果 TypeName 是一个接口类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否同时实现了 TypeName 接口。

Go 中的 interface 类型是不能直接转换成其他类型的,需要使用到断言。

package main

func main() {
	var itf interface{} = 1
	i, ok := itf.(string)
	println("值:", i, "; 断言结果", ok)

	j, ok := itf.(int)
	println("值:", j, "; 断言结果", ok)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

接口断言的两种语法表现:

5.1.1 直接赋值模式

o := i.(TypeName)
  • 1

分析:

  1. TypeName 是具体类型名,此时如果接口 i 绑定的实例类型就是具体类型 TypeName ,则变量 o 的类型就是 TypeName , 变量 o 的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本) 。
  2. TypeName 是接口类型名, 如果接口 i 绑定的实例类型满足接口类型 TypeName ,则变量 o 的类型就是接口类型TypeName , o 底层绑定的具体类型实例是 i 绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本〉。
  3. 如果上述两种情况都不满足, 则程序抛出 panic

示例代码:

package main

import "fmt"

type Inter interface {
	Ping()
	Pang()
}

type Anter interface {
	Inter
	String()
}

type St struct {
	Name string
}

func (St) Ping() {
	println("ping")
}

func (*St) Pang() {
	println("pang")
}

func main() {
	st := &St{"abcd"}
	var i interface{} = st
	// 判断i 绑定的实例是否实现了接口类型Inter
	o := i.(Inter)
	o.Ping()
	o.Pang()

	/*
		如下语句会引发 panic ,因为 i 没有实现接口Anter
		p := i.(Anter)
		p.String()
	*/
	// 判断 i 绑定的实例是否就是具体类型 St
	s := i.(*St)
	fmt.Printf("%s", s.Name)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

由于可能出现 panic,所以我们并不推荐使用这种类型断言的语法形式。

关于类型断言,需要注意两点:

  • 如果 i 是一个非接口值,那么必须在做类型断言之前把它转换为接口值。因为 Go 中的任何类型都是空接口类型的实现类型,所以一般会这样做: interface{}(i).(TypeNname)
  • 如果类型断言的结果为否,就意味着该类型断言是失败的,失败的类型断言会引发 panic(运行时异常),解决方法是:
var i1, ok := interface{}(i).(TypeNname)
  • 1

其中 ok 值体现了类型断言的成败,如果成功,i1 就会是经过类型转换后的 TypeNname 类型的值,否则它将会是 TypeNname 类型的零值(或称为默认值)
断言

func main() {
	a := "1"
	// var b int
	b, ok := interface{}(a).(int)
	if ok {
		fmt.Printf("b type is %v", b)
	} else {
		fmt.Printf("b  is %v", b)	// b  is 0
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

5.1.2 comma,ok 表达式模式

if o, ok := i.(TypeName); ok {
}
  • 1
  • 2

语义分析:

  1. TypeName 是具体类型名,此时如果接口 i 绑定的实例类型就是具体类型 TypeName ,则 oktrue , 变量 o 的类型就是 TypeName ,变量 o 的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本) 。
  2. TypeName 是接口类型名, 此时如果接口 i 绑定的实例的类型满足接口类型 TypeName , 则 oktrue ,变量 o 的类型就是接口类型 TypeName , o 底层绑定的具体类型实例是 i 绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本)。
  3. 如果上述两个都不满足,则 okfalse , 变量 oTypeName 类型的“零值”,此种条件分支下程序逻辑不应该再去引用。因为此时的。没有意义。
value, ok := a.(string)
  • 1

总的来说:如果断言失败,那么 ok 的值将会是 false ,但是如果断言成功 ok 的值将会是 true ,同时value 将会得到所期待的正确的值。

var a int64 = 13
var i interface{} = a
v1, ok := i.(int64) 
fmt.Printf("v1=%d, the type of v1 is %T, ok=%t\n", v1, v1, ok) // v1=13, the type of v1 is int64, ok=true
v2, ok := i.(string)
fmt.Printf("v2=%s, the type of v2 is %T, ok=%t\n", v2, v2, ok) // v2=, the type of v2 is string, ok=false
v3 := i.(int64) 
fmt.Printf("v3=%d, the type of v3 is %T\n", v3, v3) // v3=13, the type of v3 is int64
v4 := i.([]int) // panic: interface conversion: interface {} is int64, not []int
fmt.Printf("the type of v4 is %T\n", v4) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

修改上述代码:

func main() {
	st := &St{"abcd"}
	var i interface{} = st
	// 判断i 绑定的实例是否实现了接口类型Inter
	if o, ok := i.(Inter); ok {
		o.Ping()
		o.Pang()
	}

	//  i 没有实现接口 Anter
	if p, ok := i.(Anter); ok {
		p.String()
	}

	// 判断 i 绑定的实例是否就是具体类型 St
	if s, ok := i.(*St); ok {
		fmt.Printf("%s", s.Name)
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

另外一个完整的示例如下:

package main

import "fmt"

/*
func funcName(a interface{}) string {
return string(a)
}
*/
func funcName(a interface{}) string {
	value, ok := a.(string)
	if !ok {
		fmt.Println("It is not ok for type string")
		return ""
	}
	fmt.Println("The value is ", value)
	return value
}
func main() {
	// str := "123"
	// funcName(str)
	//var a interface{}
	//var a string = "123"
	var a int = 10
	funcName(a)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

5.2 类型查询

类型查询,就是根据变量,查询这个变量的类型。为什么会有这样的需求呢?

Go 中有一个特殊的类型 interface{},这个类型可以被任何类型的变量赋值,如果想要知道到底是哪个类型的变量赋值给了 interface{} 类型变量,就需要使用类型查询来解决这个需求,示例代码如下:

func main() {
	var x interface{} = 13
	switch x.(type) {
	case nil:
		println("x is nil")
	case int:
		println("the type of x is int")
	case string:
		println("the type of x is string")
	case bool:
		println("the type of x is string")
	default:
		println("don't support the type")
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

输出结果:

the type of x is int
  • 1

不过,通过 x.(type),我们除了可以获得变量 x 的动态类型信息之外,也能获得其动态类型对应的值信息,现在我们把上面的例子改造一下:


func main() {
    var x interface{} = 13
    switch v := x.(type) {
    case nil:
        println("v is nil")
    case int:
        println("the type of v is int, v =", v)
    case string:
        println("the type of v is string, v =", v)
    case bool:
        println("the type of v is bool, v =", v)
    default:
        println("don't support the type")
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这里我们将 switch 后面的表达式由 x.(type) 换成了 v := x.(type) 。对于后者,你千万不要认为变量 v 存储的是类型信息,其实 v 存储的是变量 x 的动态类型对应的值信息,这样我们在接下来的 case 执行路径中就可以使用变量 v 中的值信息了。

输出结果

the type of v is int, v = 13
  • 1
package main

import (
    "fmt"
)

func main() {
    // 定义一个interface{}类型变量,并使用string类型值”abc“初始化
    var a interface{} = "abc"

    // 在switch中使用 变量名.(type) 查询变量是由哪个类型数据赋值。
    switch v := a.(type) {
    case string:
        fmt.Println("字符串")
    case int:
        fmt.Println("整型")
    default:
        fmt.Println("其他类型", v)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

如果使用 .(type) 查询类型的变量不是 interface{} 类型,则在编译时会报如下错误:

cannot type switch on non-interface value a (type string)
  • 1

如果在 switch 以外地方使用 .(type),则在编译时会提示如下错误:

use of .(type) outside type switch
  • 1

所以,使用 type 进行类型查询时,只能在 switch 中使用,且使用类型查询的变量类型必须是 interface{}

接口类型查询的语法格式如下:

switch v := i.(type) {
    case typel :
    	xx xx
    case type2 :
    	xx xx
    default :
    	xx xx
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

类型查询和类型断言

  1. 类型查询和类型断言具有相同的语义,只是语法格式不同。二者都能判断接口变量绑定的实例的具体类型,以及判断接口变量绑定的实例是否满足另一个接口类型。
  2. 类型查询使用 case 字句一次判断多个类型,类型断言一次只能判断一个类型,当然类型断言也可以使用 if-else-if 语句达到同样的效果。

示例如下:

func main() {
	var t interface{}
	t = functionOfSomeType()
	switch t := t.(type) {
	default:
		fmt.Printf("unexpected type %T", t) // %T prints whatever type t has
	case bool:
		fmt.Printf("boolean %t\n", t) // t has type bool
	case int:
		fmt.Printf("integer %d\n", t) // t has type int
	case *bool:
		fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
	case *int:
		fmt.Printf("pointer to integer %d\n", *t) // t has type *int
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

或者使用 if-else-if 代替

func sqlQuote(x interface{}) string {
	if x == nil {
		return "NULL"
	} else if _, ok := x.(int); ok {
		return fmt.Sprintf("%d", x)
	} else if _, ok := x.(uint); ok {
		return fmt.Sprintf("%d", x)
	} else if b, ok := x.(bool); ok {
		if b {
			return "TRUE"
		}
		return "FALSE"
	} else if s, ok := x.(string); ok {
		return sqlQuoteString(s) // (not shown)
	} else {
		panic(fmt.Sprintf("unexpected type %T: %v", x, x))
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

5.3 类型断言和查询总结

package main

import (
	"fmt"
)

var container = []string{"aaa", "bbb", "ccc"}

func main() {
	container := map[string]string{"a": "aaa", "b": "bbb", "c": "ccc"}

	// 方式1。类型断言
	_, ok1 := interface{}(container).([]string)
	_, ok2 := interface{}(container).(map[string]string)

	// %T 表示该值的 Go 类型
	if !(ok1 || ok2) {
		fmt.Printf("Error: unsupported container type: %T\n", container)
		return
	}
	fmt.Printf("The element is %#v , (container type: %T)\n", container["a"], container)

	// 方式2。
	elem, err := getElement(container)
	if err != nil {
		fmt.Printf("Error: %s\n", err)
		return
	}
	fmt.Printf("The element is %#v , (container type: %T)\n", elem, container)
}

//	空接口包含所有的类型,输入的参数均会被转换为空接口
// 	函数入参已经声明 containerI 为 interface 类型,所以不需要再次进行 interface{}(container) 转换
func getElement(containerI interface{}) (elem string, err error) {
	//	变量类型会被保存在t中
	// 	方式2。类型查询
	switch t := containerI.(type) {
	case []string:
		elem = t[1]
	case map[string]string:
		elem = t["a"]
	default:
		err = fmt.Errorf("unsupported container type: %T", containerI)
		return
	}
	return
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

6. 空接口

如果一个接口类型定义中没有一个方法,那么它的方法集合就为空,比如下面的 EmptyInterface 接口类型:

type EmptyInterface interface {

}
  • 1
  • 2
  • 3

这个方法集合为空的接口类型就被称为空接口类型,但通常我们不需要自己显式定义这类空接口类型,我们直接使用 interface{} 这个类型字面值作为所有空接口类型的代表就可以了。

由于 Go 是强类型的语言,使用空接口可以为外界提供一个更加通用的能力。然而在处理接口的过程中却需要默默承受解析空接口带来的痛苦。通过使用空接口,常见的 fmt.Println 函数提供了打印任何类型的功能。


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

如果不使用空接口,那么每一个类型都需要实现一个对应的 Println 函数,是非常不方便的。

不过,空接口带来便利的同时,也意味着我们必须在内部解析接口的类型,并对不同的类型进行相应的处理。以 fmt.Println 为例,Println 函数内部通过检测接口的具体类型来调用不同的处理函数。如果是自定义类型,还需要使用反射、递归等手段完成复杂类型的打印功能。


func (p *pp) printArg(arg interface{}, verb rune) {
    switch f := arg.(type) {
      case bool:
        p.fmtBool(f, verb)
      case float32:
        p.fmtFloat(float64(f), 32, verb)
      case float64:
        p.fmtFloat(f, 64, verb)
      case complex64:
        p.fmtComplex(complex128(f), 64, verb)
      ....
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

类似的 API 设计还有用于序列化与反序列化的 JSON 标准库等。

JSON 标准库内部使用了反射来判断接口中存储的实际类型,以此分配不同的序列化器。


func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
  ...  
  switch t.Kind() {
    case reflect.Bool:
      return boolEncoder
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
      return intEncoder
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
      return uintEncoder
      ...

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

除此之外,对于跨服务调用的 API,使用空接口可以提高它们的扩展性。因为在这种场景下,修改 API 的成本通常比较高,服务器需要改造并发布新的 SDK,客服端还需要适配新的 SDK 并联调测试。

如下所示,在 Info 结构体中增加扩展类型 map[string]interface{},新的功能如果需要传递新的信息,当前服务甚至可以不用修改 API


type info struct{
  ExtraData               map[string]interface{}        `json:"extra_data"` 
  ...
}
  • 1
  • 2
  • 3
  • 4
  • 5

可以看出,空接口为 API 带来了扩展性和灵活性,但是也为模块的内部处理增加了额外的成本。因为 API 内部处理空接口时使用了大量的反射,而反射通常比较消耗性能。

Go 语言没有泛型, 如果一个函数需要接收任意类型的参数, 则参数类型可以使用空接口类型,空接口不是真的为空,接口有类型和值两个概念。

package main

import "fmt"

type Inter interface {
	Ping()
	Pang()
}

type St struct{}

func (St) Ping() {
	println("ping")
}

func (*St) Pang() {
	println("pang")
}

func main() {
	var st *St = nil
	var it Inter = st

	fmt.Printf("%p\n", st) // 0x0
	fmt.Printf("%p\n", it) // 0x0

	if it != nil {
		it.Pang() // pang
		// 下面的语句会导致panic
		// 方法转换为函数调用,第一个参数是St 类型,由于 *St 是nil ,无法获取指针所指的对象值,所以导致panic
		// it.Ping()
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

这个程序暴露出 Go 语言的一点瑕疵, fmt.Printf("%p\n", it) 的结果是 0x0 ,但 it! = nil 的判断结果却是 true

空接口有两个字段, 一个是实例类型, 另一个是指向绑定实例的指针,只有两个都为 nil 时,空接口才为 nil

Go 规定:如果一个类型 T 的方法集合是某接口类型 I 的方法集合的等价集合或超集,我们就说类型 T 实现了接口类型 I,那么类型 T 的变量就可以作为合法的右值赋值给接口类型 I 的变量。

如果一个变量的类型是空接口类型,由于空接口类型的方法集合为空,这就意味着任何类型都实现了空接口的方法集合,所以我们可以将任何类型的值作为右值,赋值给空接口类型的变量,

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

// or

var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t  // ok
i = &t // ok
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

参考书籍:

  1. Go 语言核心编程
  2. Go 语言圣经

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/907948
推荐阅读
相关标签
  

闽ICP备14008679号