Go语言--接口(interface)

0 接口概念

接口本身是调用方和实现方均需要遵守的一种协议。接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

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

Go语言的设计是非侵入式的设计,也就是说,一个具体类型实现接口不需要在语法上显式地声明,只要具体类型的方法集是接口方法集的超集,就代表该类型实现了接口,编译器在编译时会进行方法集的校验,从而知道哪个类型实现了哪个接口。

0.1 其他语言的接口

Go语言的接口并不是其他编程语言(C++、Java、C#等)中所提供的接口概念。

在Go语言出现之前,接口主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须显式地声明你的确实现了该接口。为了实现一个接口,你需要从该接口集成:

interface IFoo {
    void Bar();
}

class Foo implements IFoo { // Java文法
    // ...
}

class Foo : public IFoo { // C++文法
    // ...
}

IFoo* foo = new Foo;

上面这类接口我们称之为侵入式接口。“侵入式”的主要表现在于实现类需要显式地声明自己实现了某个接口。这种强制性的接口继承是面向对象编程思想发展过程中一个遭受相当多置疑的特性。

0.2 非侵入式接口

在Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口。例如:

type File struct {
    // ...
}

func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error

这里我们定义了一个File类,并实现有Read()、 Write()、 Seek()、 Close()等方法。设想我们有如下接口:

type IFile interface {
    Read(buf []byte) (n int, err error)
    Write(buf []byte) (n int, err error)
    Seek(off int64, whence int) (pos int64, err error)
    Close() error
}

type IReader interface {
    Read(buf []byte) (n int, err error)
}

type IWriter interface {
    Write(buf []byte) (n int, err error)
}

type ICloser interface {
    Close() error
}

尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类实现了这些接口,可以进行赋值:

var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)

Go语言的非侵入式接口,看似只是做了很小的文法调整,实则影响深远。

其一, Go语言的标准库,再也不需要绘制类库的继承树图。你一定见过不少C++、 Java、 C#类库的继承树图。

在Go中,类的继承树并无意义,你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。

其二,实现类的时候,只需要关心自己应该提供哪些方法,不用再纠结接口需要拆得多细才合理。接口由使用方按需定义,而不用事前规划。

其三,不用为了实现一个接口而导入一个包,因为多引用一个外部的包,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。

<提示> 非侵入式接口设计是Go语言设计师经过多年的大项目经验总结出来的设计之道。只有让接口和实现着真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低不少。

0.3 Go语言接口

在Go语言中接口(interface)是一种类型,一种抽象的类型。接口(interface)是一组方法(method)的集合。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口不关心属性(数据),只关心行为(方法)。

1 声明接口

每个接口由数个方法组成,接口的定义格式如下:

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    …
}
  • 接口类型名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer,有关闭功能的接口叫Closer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。例如:
type Writer interface {
    Write([]byte) error   //方法中只有参数类型,没有参数变量名
}

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

2 实现接口的条件

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

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

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

示例1:

/*
**程序描述:实现数据写入器接口中的方法。
**运行结果:go run dataWriter.go
WriteData: data
*/

package main

import "fmt"

//定义一个数据写入器接口
type DataWriter interface {
    WriteData(data interface{}) error
}

//定义一个文件结构,用于实现DataWriter接口中的方法
type File struct {
    
}

//实现DataWriter接口中的WriteData()方法
func (d *File) WriteData(data interface{}) error {
    //模拟写入的数据
    fmt.Println("WriteData:", data)
    return nil
}

func main(){
    //实例化一个File对象
    file := new(File)
    
    //声明一个DataWriter接口变量
    var writer DataWriter
    
    //将File结构体实例的指针file赋值给接口变量writer,也就是*File类型
    writer = file
    
    //使用DataWriter接口变量进行数据写入,即调用DataWriter的WriteData()方法
    writer.WriteData("data")
}

本例中,调用方和实现方关系如下图所示:

《代码说明》调用方是接口类型变量writer,实现方是由具体类型通过添加方法的形式实现,上例中是File结构体类型添加WriteData()方法来实现DataWriter接口的。

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

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

Go语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计在前面已经说过,称之为非侵入式设计。

<提示> 传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。经常可以看到大型软件极为复杂的派生树。随着系统的功能不断增加,这课“派生树”会变得越来越复杂。对于Go语言来说,非侵入式设计让实现者的所有类型都是平行的、组合的。如何组合则留到使用者编译时再确认。因此,使用Go语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”。

3 接口类型变量、接口初始化和接口方法调用

Go语言中,接口是一种类型,因此它也有自己的类型变量。接口类型变量能够存储所有实现了该接口的具体类型实例。单纯地声明一个接口变量没有任何意义,接口只有被初始化为具体的类型时才有意义。

没有初始化的接口变量,其默认值是 nil。

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

 1、将实例对象赋值给接口变量。

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

示例1:

//声明一个Sayer接口
type Sayer interface {
    say()
}

//定义dog结构体
type dog struct {}

//定义cat结构体
type cat struct {}

//dog结构体实现Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

//cat结构体实现Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
}

func main() {
    var x Sayer // 声明一个Sayer接口类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x,即初始化接口变量
    x.say()     // 通过接口变量x调用cat类型的say方法,输出:喵喵喵
    x = b       // 可以把dog实例直接赋值给x,即初始化接口变量
    x.say()     // 通过接口变量x调用dog类型的say方法,输出:汪汪汪
}

2、将一个接口变量赋值给另一个接口变量。

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

示例2:

//声明一个Sayer接口
type Sayer interface {
    say()
}

//声明一个Action接口
type Action interface {
    say()
    act()
}

//定义dog结构体
type dog struct {}


//dog结构体实现Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

//dog结构体实现Action接口的act()方法
func (d dog) act() {
    fmt.Println("狗会奔跑")
}

func main() {
    var x Action  // 声明一个Action接口类型的变量x
    var y Sayer   // 声明一个Sayer接口类型的变量y
    a := dog{}    // 实例化一个dog
    x = a         // 初始化接口变量x
    x.act()       //输出:狗会奔跑
    y = x         //将接口变量x赋值给接口变量y,初始化接口变量y
    y.say()       //输出:汪汪汪
}

《代码说明》Action接口有两个方法:say()、act(),Sayer接口只有一个方法:say()。可以看到,Sayer接口的方法集是Action接口的方法集的子集,那么就可以将Action接口类型的变量x赋值给Sayer接口类型的变量y,从而初始化接口变量y。

接口方法调用

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

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

4 接口的动态类型和静态类型

动态类型

接口绑定的具体实例的类型称为接口的动态类型。接口可以绑定不同类型的实例,所以接口的动态类型是随着其绑定的不同类型实例而发生变化的。接口的动态类型是在运行时才确定的。

静态类型

接口被定义时,其类型就已经被确定,这个类型叫接口的静态类型。接口的静态类型在其被定义就被确定,接口的静态类型的本质特征就是接口的方法签名集合。两个接口如果方法签名集合相同(方法的顺序可以不同),则这两个接口在语义上完全等价,它们之间不需要强制类型转换就可以互相赋值。原因是Go编译器校验接口是否能赋值,是比较二者的方法集,而不是看具体类型名。a接口的方法集是A,b接口的方法集是B,如果B是A的子集,则a的接口变量可以直接赋值给b的接口变量。反之,则需要使用接口的类型断言,下面会讲到。

5 理解接口与类型的的关系

类型和接口之间有一对多和多对一的关系。

5.1 一个类型可以实现多个接口(一对多)

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

示例2:网络上两个程序通过一个双向的通信连续实现数据的交换,连接的一端称为一个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接口。如下图所示:

                                                                      接口的使用和实现过程

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

//使用io.Writer接口的代码,并不知带Socket和io.Closer的存在
func usingWriter(writer io.Writer) {
    writer.Write(nil)
}

//使用io.Closer接口的代码,并不知道Socket和io.Writer的存在
func usingCloser(closer io.Closer) {
    closer.Close()
}

func main(){
    s := new(Socket)  //实例化Socket
    usingWriter(s)    //将s传递给接口变量writer
    usingCloser(s)    //将s传递给接口变量closer
}

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

5.2 多个类型可以实现相同的接口(多对一)

 Go语言中不同类型还可以实现同一接口。

//声明一个Mover接口
type Mover interface {
    move()
}

//定义dog结构体
type dog struct {
    name string
}

//定义一个car结构体
type car struct {
    brand string
}

//dog结构体实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会奔跑\n", d.name)
}

//car结构体实现Mover接口
func (c car) move() {
    fmt.Printf("%s速度80迈\n", c.brand)
}

func main() {
    var x Mover
    var a = dog{name: "旺财"}
    var b = car{brand: "宝马"}
    x = a
    x.move()
    x = b
    x.move()
}

 运行结果:

旺财会奔跑
宝马速度80迈

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

例如:Service接口定义了两个方法:一个是开启服务的方法:Start(),另一个是输出日志的方法:Log()。使用GameService结构体来实现Service接口,GameService自己的结构只能实现Start()方法,而Service接口中的Log()方法已经被一个能输出日志的日志器结构Logger实现了。所以,可以选择将Logger结构嵌入到GameService结构中,这样做的好处是能最大程度地避免代码冗余,简化代码结构。详细实现过程如下:

//声明Service接口
type Service interface {
    Start()
    Log(string)
}

//定义日志器结构体
type Logger struct {
}

//实现Service的Log方法
func (l *Logger) Log(s string) {

}

//定义游戏服务结构体
type GameService struct {
    Logger     //嵌入日志器结构体
}

//实现Service的Start方法
func (g *GameService) Start(){

}

//实例化GameService,并将实例赋值给Service接口变量
var s Service = new(GameService)
s.Start()
s.Log("hello")

《代码说明》上例中,GameService结构体并不是完全由自己实现了Service接口,它自己只实现了Start()方法,而Log()方法是由嵌入的Logger结构实现的,通过二者的结合才实现了Service接口中所有的方法。通过这种方式来实现接口也是可以的。

6 值接收器和指针接收器实现接口的区别

使用值接收器实现接口和使用指针接收器实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

示例1:值接收器实现接口方法例子。

type Mover interface {
    move()
}

type dog struct {}

//值接收器实现接口
func (d dog) move() {
    fmt.Println("狗会奔跑")
}

func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    fmt.Printf("1-x:%T\n", x)
    x.move()
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
    fmt.Printf("2-x:%T\n", x)
    x.move()            // Go语言自动会处理为:(*x).move()
}

运行结果:

1-x:main.dog
狗会奔跑
2-x:*main.dog
狗会奔跑

《代码说明》从上面的代码中我们可以发现,使用值接收器实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

示例2:指针接收器实现接口方法例子。

type Mover interface {
    move()
}

type dog struct {}

//指针接收器实现接口
func (d *dog) move() {
    fmt.Println("狗会奔跑")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型,编译报错
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
}

《代码说明》上面代码运行会报错:cannot use wangcai (type dog) as type Mover in assignment:

dog does not implement Mover (move method has pointer receiver)

因为此时实现Mover接口的move()方法的接收器是*dog类型,所以不能传入dog类型的wangcai,此时接口变量只能存储*dog类型的值。

总结

  • 使用值接收器实现接口,结构体类型和结构体指针类型变量都能存。
  • 使用指针接收器实现接口,只能存结构体指针类型变量。

7 接口嵌套—将多个接口放在一个接口内

在Go语言中,不仅结构体与结构体之间可以嵌套,接口与接口之间也可以通过嵌套创造出新的接口。接口与接口的嵌套组合形成的新接口,只要接口中的所有方法均被实现,则这个新接口中所有嵌套接口的方法均可以被调用。

示例:使用接口嵌套组合的例子。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套组合形成新的接口Animal
type Animal interface {
    Sayer
    Mover
}

//嵌套得到的接口的使用与普通接口一样,这里我们让Cat结构实现animal接口
type Cat struct {
    name string
}

func (c Cat) say() {
    fmt.Println("喵喵喵")
}

func (c Cat) move() {
    fmt.Println("猫会动")
}

func main() {
    var x Animal
    x = Cat{name: "花花"}
    x.move()
    x.say()
}

运行结果:

猫会动
喵喵喵

8 接口运算

接口是一个抽象的类型,接口像一层胶水,可以灵活地解耦软件的每一个层次,基于接口编程是Go语言编程的基本思想。

前面我们已经介绍了接口变量的初始化操作,有时我们需要知道已经初始化的接口变量绑定的具体实例是什么类型的,以及这个具体类型是否还实现了其他的接口,即需要检查运行时的接口类型。Go语言提供两种语法结构来支持这两个需求,分别是类型断言和接口类型查询。

8.1 类型断言(Type Assertion)

8.1.1 类型断言的格式

类型断言的格式如下:

t := i.(T)
  • i:表示接口变量。
  • T:表示转换后的目标类型。
  • t:表示转换后的变量。

接口类型断言的两层语义

(1)如果 T 是一个具体类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否就是具体类型 T。如果是,则变量 t 的类型就是 T,变量t 的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本)。

(2)如果 T 是一个接口类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否同时实现了 T 接口。如果是,则变量 t 的类型就是接口类型T,t 底层绑定的具体类型实例是接口变量 i 绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本)。

<注意> 如果变量i对应的具体类型没有完全实现T接口的方法,这个语句将会触发宕机。触发宕机不是很友好,因此上面的语句还有一种写法:

t, ok := i.(T)

这种写法下,如果发生接口为实现时,将会把ok置为false,变量t置为T类型的零值。正常实现时,ok为true。

接口值

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成。这两部分分别称为接口的动态类型和动态值。

例子:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

图解如下:

8.2 类型查询(Type Switches)

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

switch i.(type) {   //或者写成 switch v := i.(type) {
case type1:
    xxxx
case type2:
    xxxx
case type3:
    xxxx
default:
    xxxx
}
  • i:表示需要判断的接口变量。
  • type1、type2、type3:表示接口变量可能具有的类型列表,满足时,会执行指定case分支语句。

语义分析

接口查询有两层语义,一是查询一个接口变量底层绑定的底层变量的具体类型是什么;二是查询接口变量绑定的底层变量是否实现还了其他接口。

(1)i 必须是接口变量。具体类型实例的类型是静态的,在类型声明后就不再变化,所以具体类型的变量不存在类型查询,类型查询一定是对一个接口变量进行操作。也就是说,上文中的 i 必须是接口变量,如果 i 是未初始化的接口变量,则 i.(type) 的值是nil。例如:

package main

import (
    "fmt"
    "io"
)

func main() {
    var i io.Reader
    switch v := i.(type) {  //此处i是未初始化的接口变量,所以v的值为nil
    case nil:
        fmt.Printf("v type: %T\n", v)  //v type: <nil>
    default:
        fmt.Println("default")
    }
}

(2)case 关键字后面可以跟接口类型名,也可以接非接口类型名。匹配是按照 case 子句的顺序进行的。

  • 如果case 后面跟的是一个接口类型名,且接口变量 i 绑定的实例类型实现了该接口,则匹配成功,v 的类型是接口类型,v 底层绑定的实例是 i 绑定具体类型实例的副本。
  • 如果case 后面跟的是一个具体类型名,且接口变量 i 绑定的实例类型和该具体类型相同,则匹配成功,此时 v 就是该具体类型变量,v 的值是 i 绑定的实例值的副本。
  • 如果case 后面跟着多个类型,使用逗号分隔,接口变量 i 绑定的实例类型只要和其中一个类型匹配,则直接使用 i 赋值给v,相当于 v := i。这个语法有点奇怪,按理说编译器不应该允许这样的操作,语言实现者可能想让 type switch 语句和普通的 switch 语句保持一样的语法规则,允许发生这种情况。例如:
f, err := os.OpenFile("notes.txt", os.O_RDWR|os.CREATE, 0755)
if err != nil {
    log.Fatal(err)
}
defer f.Close()

var i io.Reader = f
switch v := i.(type) {
//多个类型,i 满足其中任何一个就算匹配成功
case *os.File, io.ReaderWriter:
    //此时相当于执行 v := i,v 和 i 是等价的,使用v没有意义
    if v == i {
        fmt.Println(true)  //true
    }
default:
    return
}
  • 如果所有的case 分支都不满足,则执行default语句,此时执行的仍然是 v := i,最终 v 的值是 i。此时使用 v 没有任何意义。
  • fallthrough 语句不能在 type switch 语句中使用。

8.3 类型断言和类型查询的总结

(1)类型断言和类型查询具有相同的语义,只是语法格式不同。二者都能判断接口变量绑定的实例变量的具体类型,以及判断接口变量绑定的实例是否满足另一个接口类型。

(2)类型查询使用case 分支,一次可以判断多个类型,类型断言一次只能判断一个类型,当然类型断言也可以使用 if...else if 语句达到相同的效果。

8.4 接口使用形式

接口类型是“第一公民”,可以用在任何使用变量的地方,使用灵活,方便解耦,主要使用在如下地方:

(1)作为结构体的内嵌字段。

(2)作为函数或方法的形参。

(3)作为函数或方法的返回值。

(4)作为其他接口定义的内嵌字段。

9 空接口 — interface{}

没有任何方法的接口,称之为空接口。空接口表示为:interface{}。系统中任何类型都符合空接口的要求,空接口有点类似于Java语言中的Object类。Go语言中的基本类型 int、float 和 string 也符合空接口。Go的系统类型中没有类的概念,所有的类型都是一样的身份,没有Java语言对基本类型的开箱和装箱操作,所有的类型都是统一的。Go语言的空接口有点像C语言的空指针类型(void *),只不过void *  是指针,而Go语言的空接口内部封装了指针而已。

<提示> 空接口的内部实现保存了对象的类型和指针,使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢,因此在开发过程中,应该在需要的地方使用空接口,而不是在所有的地方使用空接口,否则会影响程序的执行效率。

9.1 空接口的用途

空接口和泛型

Go语言没有泛型,如果一个函数需要接收任意类型的参数,则参数类型可以使用空接口类型,这是弥补没有泛型的一种方式。例如:

//典型的就是fmt标准包里的print函数
func Fprint(w io.Writer, a ...interface{}) (n int, err error)

将值保存到空接口

func main() {
    var any interface{}
    fmt.Println(any)
    fmt.Printf("any Type: %T\n\n", any)
    
    any = 1
    fmt.Println(any)
    fmt.Printf("any Type: %T\n\n", any)
    
    any = "hello"
    fmt.Println(any)
    fmt.Printf("any Type: %T\n\n", any)
    
    any = true
    fmt.Println(any)
    fmt.Printf("any Type: %T\n", any)
}

运行结果:

<nil>
any Type: <nil>

1
any Type: int

hello
any Type: string

true
any Type: bool

《代码分析》从运行结果可以看到,空接口变量的零值是 nil。当将具体类型的值或者变量赋值非空接口变量时,空接口变量的类型就表现为该具体类型。

从空接口获取值

保存到空接口的值,如果直接取出指定类型的值时,会发生编译错误。例如:

func main() {
    var a int = 1
    
    var i interface{} = a
    
    var b int = i  //声明变量b,并尝试赋值i
}

报错信息:cannot use i (type interface {}) as type int in assignment: need type assertion.

编译器告诉我们,不能将变量i 视为int 类型赋值给变量b。虽然变量 i 在赋值完成后的内部类型为int,但 i 还是一个interface{}类型的变量。类似于无论集装箱装的是茶叶还是烟草,集装箱本身依然是金属做的,不会因为所装物品的类型改变而改变。

同时,编译器提示我们得使用 type assersion,即类型断言。修改代码如下:

var b int = i.(int)
fmt.Println(b)       // 1

9.2 空接口的值比较

空接口变量在保存不同的值后,可以和其他变量值一样使用“==”进行比较操作。空接口的比较有以下几种特性。

1. 类型不同的空接口之间的比较结果不同

保存有类型不同的空接口变量进行相等比较时,Go语言会优先比较值的类型。因此类型不同,比较结果也是不相同的。代码如下:

func main() {
    //a保持年整型
    var a interface{} = 100
    
    //b保存字符串
    var b interface{} = "hello"
    
    //比较两个空接口是否相等
    fmt.Println(a == b)  // false
}

2. 不能比较空接口中的动态值

当接口中保存有动态类型的值时,不能进行相等比较,否则将会触发panic错误。例如:

func main() {
    // c 保存包含10的整型切片
    var c interface{} = []int{10}
    
    // d 保存包含20的整型切片
    var d interface{} = []int{20}
    
    // 这里会发生panic错误
    fmt.Println(c == d)
}

运行时错误:panic: runtime error: comparing uncomparable type []int. 提示[]int是不可比较的类型。

下图中列举了类型及比较的几种情况:

参考

《Go语言从入门到进阶实战(视频教学版)》

《Go语言核心编程》

Go语言基础之接口

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值