Go基础系列:Go接口

接口用法简介

接口(interface)是一种类型,用来定义行为(方法)。

type Namer interface {
    my_method1()
    my_method2(para)
    my_method3(para) return_type
    ...
}

但这些行为不会在接口上直接实现,而是需要用户自定义的方法来实现。所以,在上面的Namer接口类型中的方法my_methodN都是没有实际方法体的,仅仅只是在接口Namer中存放这些方法的签名(签名 = 函数名+参数(类型)+返回值(类型))。

当用户自定义的类型实现了接口上定义的这些方法,那么自定义类型的值(也就是实例)可以赋值给接口类型的值(也就是接口实例)。这个赋值过程使得接口实例中保存了用户自定义类型实例。

例如:

package main

import (
    "fmt"
)

// Shaper 接口类型
type Shaper interface {
    Area() float64
}

// Circle struct类型
type Circle struct {
    radius float64
}

// Circle类型实现Shaper中的方法Area()
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

// Square struct类型
type Square struct {
    length float64
}

// Square类型实现Shaper中的方法Area()
func (s *Square) Area() float64 {
    return s.length * s.length
}

func main() {
    // Circle类型的指针类型实例
    c := new(Circle)
    c.radius = 2.5

    // Square类型的值类型实例
    s := Square{3.2}

    // Sharpe接口实例ins1,它只能是值类型的
    var ins1 Shaper
    // 将Circle实例c赋值给接口实例ins1
    // 那么ins1中就保存了实例c
    ins1 = c
    fmt.Println(ins1)

    // 使用类型推断将Square实例s赋值给接口实例
    ins2 := s
    fmt.Println(ins2)
}

上面将输出:

&{2.5}
{3.2}

从上面输出结果中可以看出,两个接口实例ins1和ins2被分别赋值后,分别保存了指针类型的Circle实例c和值类型的Square实例s

另外,从上面赋值ins1和ins2的赋值语句上看:

ins1 = c
ins2 := s

是否说明接口实例ins就是自定义类型的实例?实际上接口是指针类型(指向什么见下文)。这个时候,自定义类型的实例c、s称为具体实例,ins实例是抽象实例,因为ins接口中定义的行为(方法)并没有具体的行为模式,而c、s中的行为是具体的。

因为接口实例ins也是自定义类型的实例,所以当接口实例中保存了自定义类型的实例后,就可以直接从接口上调用它所保存的实例的方法。例如:

fmt.Println(ins1.Area())   // 输出19.625
fmt.Println(ins2.Area())   // 输出10.24

这里ins1.Area()调用的是Circle类型上的方法Area(),ins2.Area()调用的则是Square类型上的方法Area()。这说明Go的接口可以实现面向对象中的多态:可以按需调用名称相同、功能不同的方法

接口实例中存的是什么

前面说了,接口类型是指针类型,但是它到底存放了什么东西?

接口类型的数据结构是2个指针,占用2个机器字长。

当将类型实例c赋值给接口实例ins1后,用println()函数输出ins1和c,比较它们的地址:

println(ins1)
println(c)

输出结果:

(0x4ceb00,0xc042068058)
0xc042068058

从结果中可以看出,接口实例中包含了两个地址,其中第二个地址和类型实例c的地址是完全相同的。而第二个地址c是Circle的指针类型实例,所以ins中的第二个值也是指针。

ins中的第一个是指针是什么?它所指向的是一个内部表结构iTable,这个Table中包含两部分:第一部分是实例c的类型信息,也就是*Circle,第二部分是这个类型(Circle)的方法集,也就是Circle类型的所有方法(此示例中Circle只定义了一个方法Area())。

所以,如图所示:

注意,上图中的实例c是指针,是指针类型的Circle实例。

对于值类型的Square实例s,ins2保存的内容则如下图:

实际上接口实例中保存的内容,在反射(reflect)中体现的淋漓尽致,reflect所有的一切都离不开接口实例保存的内容。

方法集(Method Set)规则

官方手册对Method Set的解释:https://golang.org/ref/spec#Method_sets

实例的method set决定了它所实现的接口,以及通过receiver可以调用的方法。

方法集是类型的方法集合,对于非接口类型,每个类型都分两个Method Set:值类型实例是一个Method Set,指针类型的实例是另一个Method Set。两个Method Set由不同receiver类型的方法组成:

实例的类型       receiver
--------------------------------------
 值类型:T       (T Type)
 指针类型:*T    (T Type)或(T *Type)

也就是说:

  • 值类型的实例的Method Set只由值类型的receiver(T Type)组成
  • 指针类型的实例的Method Set由值类型和指针类型的receiver共同组成,即(T Type)(T *Type)

这是什么意思呢?从receiver的角度去考虑:

receiver        实例的类型
---------------------------
(T Type)        T 或 *T
(T *Type)       *T

上面的意思是:

  • 如果某类型实现接口的方法的receiver是(T *Type)类型的,那么只有指针类型的实例*T才算是实现了这个接口
  • 如果某类型实现接口的方法的receiver是(T Type)类型的,那么值类型的实例T和指针类型的实例*T都算实现了这个接口

举个例子。接口方法Area(),自定义类型Circle有一个receiver类型为(c *Circle)的Area()方法时,说明实现了接口的方法,但只有Circle实例的类型为指针类型时,这个实例才算是实现了接口,才能赋值给接口实例,才能当作一个接口参数。如下:

package main

import "fmt"

// Shaper 接口类型
type Shaper interface {
    Area() float64
}

// Circle struct类型
type Circle struct {
    radius float64
}

// Circle类型实现Shaper中的方法Area()
// receiver类型为指针类型
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    // 声明2个接口实例
    var ins1, ins2 Shaper

    // Circle的指针类型实例
    c1 := new(Circle)
    c1.radius = 2.5
    ins1 = c1
    fmt.Println(ins1.Area())

    // Circle的值类型实例
    c2 := Circle{3.0}
    // 下面的将报错
    ins2 = c2
    fmt.Println(ins2.Area())
}

报错结果:

cannot use c2 (type Circle) as type Shaper
in assignment:
        Circle does not implement Shaper (Area method has
pointer receiver)

它的意思是,Circle值类型的实例c2没有实现Share接口的Area()方法,它的Area()方法是指针类型的receiver。换句话说,值类型的c2实例的Method Set中没有receiver类型为指针的Area()方法

所以,上面应该改成:

ins2 = &c2

再声明一个方法,它的receiver是值类型的。下面的代码一切正常。

type Square struct{
    length float64
}

// 实现方法Area(),receiver为值类型
func (s Square) Area() float64{
    return s.length * s.length
}

func main() {
    var ins3,ins4 Shaper

    // 值类型的Square实例s1
    s1 := Square{3.0}
    ins3 = s1
    fmt.Println(ins3.Area())

    // 指针类型的Square实例s2
    s2 := new(Square)
    s2.length=4.0
    ins4 = s2
    fmt.Println(ins4.Area())
}

很经常的,我们会直接使用推断类型的赋值方式(如ins2 := c2)将实例赋值给一个变量,我们以为这个变量是接口的实例,但实际上并不一定。正如上面值类型的c2赋值给ins2,这个ins2将是从c2数据结构拷贝而来的另一个副本数据结构,并非接口实例,但这时通过ins2也能调用Area()方法:

c2 = Circle{3.2}
ins2 := c2
fmt.Println(ins2.Area())  // 正常执行

之所以能调用,是因为Circle类型中有Area()方法,但这不是通过接口去调用的。

所以,在使用接口的时候,应当尽量使用var先声明接口类型的实例,再将类型的实例赋值给接口实例(如var ins1,ins2 Shaper),或者使用ins1 := Shaper(c1)的方式。这样,如果赋值给接口实例的类型实例没有实现该接口,将会报错。

但是,为什么要限制指针类型的receiver只能是指针类型的实例的Method Set呢?

看下图,假如指针类型的receiver可以组成值类型实例的Method Set,那么接口实例的第二个指针就必须找到值类型的实例的地址。但实际上,并非所有值类型的实例都能获取到它们的地址。

哪些值类型的实例找不到地址?最常见的是那些简单数据类型的别名类型,如果匿名生成它们的实例,它们的地址就会被Go彻底隐藏,外界找不到这个实例的地址。

例如:

package main

import "fmt"

type myint int

func (m *myint) add() myint {
    return *m + 1
}
func main() {
    fmt.Println(myint(3).add())
}

以下是报错信息:找不到myint(3)的地址

abc\abc.go:11:22: cannot call pointer method on myint(3)
abc\abc.go:11:22: cannot take the address of myint(3)

这里的myint(3)是匿名的myint实例,它的底层是简单数据类型int,myint(3)的地址会被彻底隐藏,只会提供它的值对象3。

普通方法和实现接口方法的区别

对于普通方法,无论是值类型还是指针类型的实例,都能正常调用,且调用时拷贝的内容都由receiver的类型决定

func (T Type) method1   // 值类型receiver
func (T *Type) method2  // 指针类型receiver

指针类型的receiver决定了无论是值类型还是指针类型的实例,都拷贝实例的指针。值类型的receiver决定了无论是值类型还是指针类型的实例,都拷贝实例本身

所以,对于person数据结构:

type person struct {}
p1 := person{}       // 值类型的实例
p2 := new(person)    // 指针类型的实例

p1.method1()p2.method1()都是拷贝整个person实例,只不过Go对待p2.method1()时多一个"步骤":将其解除引用。所以p2.method1()等价于(*p2).method1()

p1.method2()p2.method2()都拷贝person实例的指针,只不过Go对待p1.method2()时多一个"步骤":创建一个额外的引用。所以,p1.method2()等价于(&p1).method2()

而类型实现接口方法时,method set规则决定了类型实例是否实现了接口。

receiver        实例的类型
---------------------------
(T Type)        T 或 *T
(T *Type)       *T

对于接口abc、接口方法method1()、method2()和结构person:

type abc interface {
    method1
    method2
}

type person struct {}
func (T person) method1   // 值类型receiver
func (T *person) method2  // 指针类型receiver

p1 := abc(person)  // 接口变量保存值类型实例
p2 := abc(&person) // 接口变量保存指针类型实例

p2.method1()p2.method2()以及p1.method1()都是允许的,都会通过接口实例去调用具体person实例的方法。

p1.method2()是错误的,因为method2()的receiver是指针类型的,导致p1没有实现接口abc的method2()方法。

接口类型作为参数

将接口类型作为参数很常见。这时,那些实现接口的实例都能作为接口类型参数传递给函数/方法。

例如,下面的myArea()函数的参数是n Shaper,是接口类型。

package main

import (
    "fmt"
)

// Shaper 接口类型
type Shaper interface {
    Area() float64
}

// Circle struct类型
type Circle struct {
    radius float64
}

// Circle类型实现Shaper中的方法Area()
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    // Circle的指针类型实例
    c1 := new(Circle)
    c1.radius = 2.5
    myArea(c1)
}

func myArea(n Shaper) {
    fmt.Println(n.Area())
}

上面myArea(c1)是将c1作为接口类型参数传递给n,然后调用c1.Area(),因为实现了接口方法,所以调用的是Circle的Area()。

如果实现接口方法的receiver是指针类型的,但却是值类型的实例,将没法作为接口参数传递给函数,原因前面已经解释过了,这种类型的实例没有实现接口。

以接口作为方法或函数的参数,将使得一切都变得灵活且通用,只要是实现了接口的类型实例,都可以去调用它。

用的非常多的fmt.Println(),它的参数也是接口,而且是变长的接口参数:

$ go doc fmt Println
func Println(a ...interface{}) (n int, err error)

每一个参数都会放进一个名为a的Slice中,Slice中的元素是接口类型,而且是空接口,这使得无需实现任何方法,任何东西都可以丢带fmt.Println()中来,至于每个东西怎么输出,那就要看具体情况。

接口类型的嵌套

接口可以嵌套,嵌套的内部接口将属于外部接口,内部接口的方法也将属于外部接口。

例如,File接口内部嵌套了ReadWrite接口和Lock接口。

type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}
type Lock interface {
    Lock()
    Unlock()
}
type File interface {
    ReadWrite
    Lock
    Close()
}

除此之外,类型嵌套时,如果内部类型实现了接口,那么外部类型也会自动实现接口,因为内部属性是属于外部属性的。

原文地址:https://www.cnblogs.com/f-ck-need-u/p/9940845.html

时间: 2024-10-11 07:51:25

Go基础系列:Go接口的相关文章

Go基础系列:接口类型探测和type-switch

接口类型探测:类型断言 接口实例中存储了实现接口的类型实例,类型的实例有两种:值类型实例和指针类型实例.在程序运行过程中,接口实例存储的实例类型可能会动态改变.例如: // ins是接口实例 var ins Shaper // ins存储值类型的实例 ins = c1 // 一段时间后... ... // ins存储指针类型的实例,存储的类型发生改变 ins = c2 // 一段时间后... // ins可能存储另一个类型实例 ins = s1 所以,需要一种探测接口实例所存储的是值类型还是指针

C#基础系列——一场风花雪月的邂逅:接口和抽象类

前言:最近一个认识的朋友准备转行做编程,看他自己边看视频边学习,挺有干劲的.那天他问我接口和抽象类这两个东西,他说,既然它们如此相像, 我用抽象类就能解决的问题,又整个接口出来干嘛,这不是误导初学者吗.博主呵呵一笑,回想当初的自己,不也有此种疑惑么...今天打算针对他的问题,结合一个实际的使用场景来说明下抽象类和接口的异同,到底哪些情况需要用接口?又有哪些情况需要用抽象类呢? C#基础系列目录: C#基础系列——Linq to Xml读写xml C#基础系列——扩展方法的使用 C#基础系列——序

夯实Java基础系列6:一文搞懂抽象类和接口,从基础到面试题,揭秘其本质区别!

目录 抽象类介绍 为什么要用抽象类 一个抽象类小故事 一个抽象类小游戏 接口介绍 接口与类相似点: 接口与类的区别: 接口特性 抽象类和接口的区别 接口的使用: 接口最佳实践:设计模式中的工厂模式 接口与抽象类的本质区别是什么? 基本语法区别 设计思想区别 如何回答面试题:接口和抽象类的区别? 参考文章 微信公众号 Java技术江湖 个人公众号:黄小斜 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl

C#基础系列:开发自己的窗体设计器(PropertyGrid显示中文属性名)

既然是一个窗体设计器,那就应该能够设置控件的属性,设置属性最好的当然是PropertyGrid了,我们仅仅需要使用一个PropertyGrid.SelectedObject = Control就可以搞定,让PropertyGrid显示Control的所有属性.可是这里显示的属性名是英文的.对于我们开发人员来说这无可厚非,我们也乐于接受.并且让PropertyGrid显示中文属性名,这对于我们开发人员的使用来说显得多此一举.可是,对于我这种类型的一个应用工具,英文属性名对于很多客户来说可能就很难懂

Spring基础系列12 -- Spring AOP AspectJ

Spring基础系列12 -- Spring AOP AspectJ 转载:http://www.cnblogs.com/leiOOlei/p/3613352.html 本文讲述使用AspectJ框架实现Spring AOP. 再重复一下Spring AOP中的三个概念, Advice:向程序内部注入的代码. Pointcut:注入Advice的位置,切入点,一般为某方法. Advisor:Advice和Pointcut的结合单元,以便将Advice和Pointcut分开实现灵活配置. Aspe

【C++自我精讲】基础系列六 PIMPL模式

0 前言 很实用的一种基础模式. 1 PIMPL解释 PIMPL(Private Implementation 或 Pointer to Implementation)是通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏. 2 PIMPL优点 举例: //x.h class X { public: void Fun(); private: int i; //add int i; }; //c.h #include <x.h> class C { public: void Fun()

J2EE开发实战基础系列之开卷有益

时隔七年再次接触培训有关的事情,是兴奋,更多的是恐惧,不知该如何下手. 本系列针对有Java语法基础的开发者或者爱好者,从工作开发角度出发讲解,不同于其他视频,一切皆以实用为主,过程中如有疑问,请提问于我,回答将发布在教程中添加提问部分,提问者越多,教程覆盖越全面,以实际问题为主. ----------------------------------------------------------------------------------------- 首先介绍下目前J2EE方面培训的入门

Spring基础系列9 -- Spring AOP

Spring基础系列9 -- Spring AOP 转载:http://www.cnblogs.com/leiOOlei/p/3556054.html Spring AOP即Aspect-oriented programming,面向切面编程,是作为面向对象编程的一种补充,专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题.简单地说,就是一个拦截器(interceptor)拦截一些处理过程.例如,当一个method被执行,Spring AOP能够劫持正在运行的method,在met

C#基础系列——Attribute特性使用

前言:上篇 C#基础系列--反射笔记 总结了下反射得基础用法,这章我们来看看C#的另一个基础技术--特性. 1.什么是特性:就博主的理解,特性就是在类的类名称.属性.方法等上面加一个标记,使这些类.属性.方法等具有某些统一的特征,从而达到某些特殊的需要.比如:方法的异常捕捉,你是否还在某些可能出现异常的地方(例如数据库的操作.文件的操作等)经常使用try...catch.这个时候如果使用特性,就可以大大减少方法里面的try...catch的使用.你只需要定义一个专门捕捉异常的特性类Excepti

[转]C#进阶系列——WebApi 接口返回值不困惑:返回值类型详解

本文转自:http://www.cnblogs.com/landeanfen/p/5501487.html 阅读目录 一.void无返回值 二.IHttpActionResult 1.Json(T content) 2.Ok(). Ok(T content) 3.NotFound() 4.其他 5.自定义IHttpActionResult接口的实现 三.HttpResponseMessage 四.自定义类型 五.总结 正文 前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学