深析golang中interface接口

下面由golang教程栏目给大家深析golang中interface接口,希望对需要的朋友有所帮助!

一 接口介绍

如果说gorountine和channel是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代成为一道亮丽的风景,那么接口是Go语言整个类型系列的基石,让Go语言在基础编程哲学的探索上达到前所未有的高度。Go语言在编程哲学上是变革派,而不是改良派。这不是因为Go语言有gorountine和channel,而更重要的是因为Go语言的类型系统,更是因为Go语言的接口。Go语言的编程哲学因为有接口而趋于完美。C++,Java 使用”侵入式”接口,主要表现在实现类需要明确声明自己实现了某个接口。这种强制性的接口继承方式是面向对象编程思想发展过程中一个遭受相当多质疑的特性。Go语言采用的是“非侵入式接口”,Go语言的接口有其独到之处:只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方,所谓类型T的公开方法完全满足接口I的要求,也即是类型T实现了接口I所规定的一组成员。这种做法的学名叫做Structural Typing,有人也把它看作是一种静态的Duck Typing。

要这个值实现了接口的方法。

type Reader interface {  Read(p []byte) (n int, err os.Error) }  // Writer 是包裹了基础 Write 方法的接口。 type Writer interface {  Write(p []byte) (n int, err os.Error) }  var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer)

登录后复制

有一个事情是一定要明确的,不论 r 保存了什么值,r 的类型总是 io.Reader ,Go 是静态类型,而 r 的静态类型是 io.Reader。接口类型的一个极端重要的例子是空接口interface{},它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。也有人说 Go 的接口是动态类型的,不过这是一种误解。 它们是静态类型的:接口类型的变量总是有着相同的静态类型,这个值总是满足空接口,只是存储在接口变量中的值运行时可能被改变。对于所有这些都必须严谨的对待,因为反射和接口密切相关。

立即学习“go语言免费学习笔记(深入)”;

二  接口类型内存布局

在类型中有一个重要的类别就是接口类型,表达了固定的一个方法集合。一个接口变量可以存储任意实际值(非接口),只要这个值实现了接口的方法。interface在内存上实际由两个成员组成,如下图,tab指向虚表,data则指向实际引用的数据。虚表描绘了实际的类型信息及该接口所需要的方法集。

type Stringer interface {  String() string }  type Binary uint64  func (i Binary) String() string {  return strconv.FormatUint(i.Get(), 2) }  func (i Binary) Get() uint64 {  return uint64(i) }  func main() {  var b Binary = 32  s := Stringer(b)  fmt.Print(s.String()) }

登录后复制

深析golang中interface接口  

观察itable的结构,首先是描述type信息的一些元数据,然后是满足Stringger接口的函数指针列表(注意,这里不是实际类型Binary的函数指针集哦)。因此我们如果通过接口进行函数调用,实际的操作其实就是s.tab->fun[0](s.data) 。是不是和C++的虚表很像?但是他们有本质的区别。先看C++,它为每个类创建了一个方法集即虚表,当子类重写父类的虚函数时,就将表中的相应函数指针改为子类自己实现的函数,如果没有则指向父类的实现,当面临多继承时,C++对象结构里就会存在多个虚表指针,每个虚表指针指向该方法集的不同部分。我们再来看golang的实现方式,同C++一样,golang也为每种类型创建了一个方法集,不同的是接口的虚表是在运行时专门生成的,而c++的虚表是在编译时生成的(但是c++虚函数表表现出的多态是在运行时决定的).例如,当例子中当首次遇见s := Stringer(b)这样的语句时,golang会生成Stringer接口对应于Binary类型的虚表,并将其缓存。那么为什么go不采用c++的方式来实现呢?这根c++和golang的对象内存布局是有关系的。

首先c++的动态多态是以继承为基础的,在对象构造初始化的时首先会初始化父类,其次是子类,也就是说一个对象的内存布局是虚表,父类部分,子类部分(编译器不同可能会有差异),当一个父类指针指向子类时,会发生内存的截断,截断子类部分(内存地址偏移),但是此时子类的虚表中的函数指针实际上还是指向了自己的实现,所以此时的指针才会调用到子类的虚函数,如果不是虚函数,因为内存已经截断没有子类的非虚函数信息了,所以只能调用父类的了,这种继承关系让c++的虚表的初始化非常清晰,在一个对象初始化时先调用父类的构造此时虚表跟父类是一样的,接下来初始化子类,此时编译器就会去识别子类有没有覆盖父类的虚函数,如果有则虚表中相应的函数指针改成自己的虚函数实现指针。

那么go有什么不同呢,首先我们很清楚go是没有严格意义上的继承的,go的接口不存在继承关系,只要实现了接口定义的方法都可以成为接口类型,这给go的虚表初始化带来很大的麻烦,到底有多少类型实现了这个接口,一个类型到底实现了多少接口这让编译器很confused。举个例子,某个类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为O(mXn),不过可以使用预先排序的方式进行优化,实际的时间复杂度为O(m+n)这样看来其实还行那为什么要在运行时生成虚表呢,这不是会拖慢程序的运行速度吗,注意我们这里是某个类型,某个接口,是1对1的关系,如果有n个类型,n个接口呢,编译器难道要把之间所有的关系都理清吗?退一步说就算编译器任劳任怨把这事干了,可是你在写过程中你本来就不想实现那个接口,而你无意中给这个类型实现的方法中包含了某些接口的方法,你根本不需要这个接口(况且go的接口机制会导致很多这种无意义的接口实现),你欺负编译器就行了,这也太欺负人了吧。如果我们放到运行时呢,我们只要在需要接口的去分析一下类型是否实现了接口的所有方法就行了很简单的一件事。

三 空接口

接口类型的一个极端重要的例子是空接口:interface{} ,它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。 注意,[]T不能直接赋值给[]interface{}

//t := []int{1, 2, 3, 4} wrong //var s []interface{} = t t := []int{1, 2, 3, 4} //right s := make([]interface{}, len(t)) for i, v := range t {  s[i] = v }

登录后复制

str, ok := value.(string) if ok {  fmt.Printf("string value is: %qn", str) } else {  fmt.Printf("value is not a stringn") }

登录后复制

在Go语言中,我们可以使用type switch语句查询接口变量的真实数据类型,语法如下:

type Stringer interface {   String() string }  var value interface{} // Value provided by caller. switch str := value.(type) { case string:   return str //type of str is string case Stringer: //type of str is Stringer   return str.String() }

登录后复制

也可以使用“comma, ok”的习惯用法来安全地测试值是否为一个字符串:

str, ok := value.(string) if ok {   fmt.Printf("string value is: %qn", str) } else {   fmt.Printf("value is not a stringn") }

登录后复制

四 接口赋值

package main  import ( "fmt" )  type LesssAdder interface {   Less(b Integer) bool   Add(b Integer) }  type Integer int  func (a Integer) Less(b Integer) bool {   return a 

go语言可以根据下面的函数:

func (a Integer) Less(b Integer) bool

登录后复制

自动生成一个新的Less()方法

func (a *Integer) Less(b Integer) bool

登录后复制

这样,类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口。 而根据

func (a *Integer) Add(b Integer)

登录后复制

这个函数无法生成以下成员方法:

func(a Integer) Add(b Integer) {   (&a).Add(b) }

登录后复制

因为(&a).Add()改变的只是函数参数a,对外部实际要操作的对象并无影响(值传递),这不符合用户的预期。所以Go语言不会自动为其生成该函数。因此类型Integer只存在Less()方法,缺少Add()方法,不满足LessAddr接口。(可以这样去理解:指针类型的对象函数是可读可写的,非指针类型的对象函数是只读的)将一个接口赋值给另外一个接口 在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就等同的,可以相互赋值。 如果A接口的方法列表时接口B的方法列表的子集,那么接口B可以赋值给接口A,但是反过来是不行的,无法通过编译。

五 接口查询

接口查询是否成功,要在运行期才能够确定。他不像接口的赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。

var file1 Writer = ...if file5,ok := file1.(two.IStream);ok {...}

登录后复制

这个if语句检查file1接口指向的对象实例是否实现了two.IStream接口,如果实现了,则执行特定的代码。

在Go语言中,你可以询问它指向的对象是否是某个类型,比如,

var file1 Writer = ...if file6,ok := file1.(*File);ok {...}

登录后复制

这个if语句判断file1接口指向的对象实例是否是*File类型,如果是则执行特定的代码。

slice := make([]int, 0)slice = append(slice, 1, 2, 3)var I interface{} = sliceif res, ok := I.([]int);ok {  fmt.Println(res) //[1 2 3]}

登录后复制

这个if语句判断接口I所指向的对象是否是[]int类型,如果是的话输出切片中的元素。

func Sort(array interface{}, traveser Traveser) error {  if array == nil {    return errors.New("nil pointer")  }  var length int //数组的长度  switch array.(type) {  case []int:    length = len(array.([]int))  case []string:    length = len(array.([]string))  case []float32:    length = len(array.([]float32))  default:    return errors.New("error type")  }  if length == 0 {    return errors.New("len is zero.")  }  traveser(array)  return nil}

登录后复制

通过使用.(type)方法可以利用switch来判断接口存储的类型。

小结: 查询接口所指向的对象是否为某个类型的这种用法可以认为是接口查询的一个特例。接口是对一组类型的公共特性的抽象,所以查询接口与查询具体类型区别好比是下面这两句问话的区别:

你是医生么?

是。

你是莫莫莫

第一句问话查询的是一个群体,是查询接口;而第二个问句已经到了具体的个体,是查询具体类型。

除此之外利用反射也可以进行类型查询,会在反射中做详细介绍。

更多golang相关技术文章,请访问golang栏目!

以上就是深析golang中interface接口的详细内容,更多请关注【创想鸟】其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至253000106@qq.com举报,一经查实,本站将立刻删除。

发布者:PHP中文网,转转请注明出处:https://www.chuangxiangniao.com/p/3164875.html

(0)
上一篇 2025年3月30日 12:12:17
下一篇 2025年3月28日 09:35:36

AD推荐 黄金广告位招租... 更多推荐

相关推荐

  • 什么是golang爬虫

    golang爬虫是指使用golang编写的程序,通过模拟客户端的请求,访问指定的网站,并且对网站的内容进行分析与提取,可以自动化获取数据、分析竞品、监控网站等带来很大的帮助,学习golang爬虫不仅可以提高自己的技术水平,还可以更好地应对日…

    2025年3月30日
    100
  • Go语言连接Vertica数据库:如何正确使用正则表达式避免问号冲突?

    Golang连接Vertica数据库:正则表达式问号冲突的解决之道 本文探讨使用Golang database/sql 包操作Vertica数据库时,如何避免正则表达式中的问号 ? 被误认为是SQL参数占位符的问题。 问题: 当使用 db.…

    2025年3月30日
    100
  • 分享一个Go json 踩坑记录

    下面由golang教程栏目给大家分享一个go json 踩坑记录,希望对需要的朋友有所帮助!                                                                          …

    编程技术 2025年3月30日
    100
  • 详解Golang中的Struct(结构体)

    Go语言中提供了对struct的支持;struct,中文翻译称为结构体,与数组一样,属于复合类型,并非引用类型。【相关推荐:Go视频教程】 Go语言的struct,与C语言中的struct或其他面向对象编程语言中的类(class)类似,可以…

    2025年3月30日
    100
  • 一文详解Golang中的反射

    本篇文章带大家主要来聊聊golang中反射,希望对你有新的认知。 虽然很多人使用 Go 语言有一定时间了,甚至有的使用了 1 年 2 年,然后对于 Go 语言中的反射还是模棱两可,使用起来的时候,心里也不是非常有底气。【相关推荐:golan…

    2025年3月30日 编程技术
    100
  • go语言中str是什么意思

    在go语言中,str指的是“字符串”,是一个不可改变的字节序列。字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-…

    2025年3月30日 编程技术
    100
  • golang序列化方法有哪些

    golang序列化方法有:1、利用Gob包管理gob流,gob是和类型绑定的,如果发现多了或者少了,会依据顺序填充或者截断。2、利用json包,能实现RFC 7159中定义的JSON编码和解码;在序列化的过程中,如果结构体内的成员是小写的,…

    2025年3月30日
    100
  • 如何优化Linux Golang日志管理

    本文探讨在Linux系统中优化Golang应用日志管理的策略,涵盖日志级别、格式、轮转、异步处理、聚合分析以及安全等多个方面。 一、日志级别与输出 精细化日志级别控制: 根据应用场景,合理设置日志级别(DEBUG、INFO、WARN、ERR…

    2025年3月29日
    100
  • Linux Golang日志如何优化

    在linux环境下使用golang进行日志优化,可以采取以下几种策略: 选择高效的日志库 zap:由Uber开源的高性能日志库,支持多种日志级别和输出方式,包括console、json、file等。zap使用Go语言本身的特性,如指针和结构…

    互联网 2025年3月28日
    100
  • 如何通过Golang日志定位问题

    在golang中,日志是定位问题的关键工具之一。以下是一些建议,帮助您通过golang日志定位问题: 使用标准库log包:Golang的标准库log包提供了基本的日志功能,包括时间戳、日志级别和消息。您可以使用log.Println()、l…

    互联网 2025年3月28日
    100

发表回复

登录后才能评论