go基础-接口

这篇具有很好参考价值的文章主要介绍了go基础-接口。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、概述

接口是面向对象编程的重要概念,接口是对行为的抽象和概括,在主流面向对象语言Java、C++,接口和类之间有明确关系,称为“实现接口”。这种关系一般会以“类派生图”的方式进行,经常可以看到大型软件极为复杂的派生树,随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
Go语言接口模型非常特别,就目前观察是独创。go接口设计是非侵入式,只要类型方法是接口方法的超集,那么就认为类型实现了接口,两者之间不需要显示关联,当然也没有implements关键字,称为隐式实现。相比Java、C++主流面向对象语言需要显示实现接口,go的方式更加灵活、松散、耦合更低,当然也更加隐晦、代码可读性降低,在实际开发中体验也不如 Java、C++好。在不修改类型定义情况下,可以为其添加接口,这在Java、C++下是不可思议的。go的接口满足鸭子模型,所谓鸭子类型:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。
这种设计解决一些问题,松散的关系不再有复杂派生树,看似降低了复杂度。但是带来了一些列新问题,如接口不支持默认实现,修改接口定义隐式破坏实现关系,面向接口编程场景下容易类型错误。降低影响方法就是接口抽象粒度尽量小,这样又降低了接口的抽象能力,导致架构碎片化,不利于大型软件架构设计。综合考虑 Go 接口模型设计是否优秀见仁见智了。

二、基本使用

接口定义,描述一堆方法的集合,给出方法声明即可,不能有默认实现,也不能有变量

type User interface {
	Say()
	GetName() string
}

定了一个user接口,包含两个方法

任何类型都可以实现这两个方法,不需要显示使用implements关键字。满足两个条件,与接口方法签名完全一致,是接口方法的超集。即可判定类型实现了接口。

type Sales struct {					// 定义类型
	name string
}

func (p *Sales) GetName() string {	// 接口方法一
	return p.name
}

func (p *Sales) Say() {				// 接口方法二
	fmt.Println("Hi, I'm", p.name)
}

func (p *Sales) peddle() {			
	fmt.Printf("%s peddle", p.name)
}

Sales类型满足两个条件,可判断实现了User接口。从代码上看两者没有直接关联,这就是隐式实现。

按照上面的两个条件,Sales也实现了如下接口

type Person interface {
    GetName() string
}

可以看到非常松散,就是这么简洁。再次强调只要满足两个条件:与接口方法签名一致,是接口方法的超集,即可判定类型实现了接口。从类型自身角度看,完全不知道自己实现了哪些接口。

通过实例调用方法

var sales Sales = Sales{name: "tom"}
sales.Say()

通过接口调用方法,只要类型实现了接口,就可以赋值给接口变量,并使用接口调用方法

var user User = &Sales{name: "tom"}		// 赋值给接口变量,注意是地址
user.Say()								// 通过接口调用方法

fmt.Printf("%T\n", user)				// *main.Sales

重要:接口是引用类型,和指针一样,只能指向实例的地址。

接口主要目标是解耦,通常称为面向接口编程,主流使用方式,函数形参是接口类型,调用时候传递接口变量,这也是接口存在的意义。

func PrintName(user User) {					// 形参是User接口类型
	fmt.Println("姓名:", user.GetName())
}

var sales User = &Sales{name: "tom"}		
PrintName(sales)							// 传入user接口变量

形参是接口类型,可传入所有实现了该接口的实例,不在依赖具体类型。

在标准库中大量使用接口。比如排序是普片需求,标准库提供了排序函数,形参是接口类型,任何实现了该接口的类型,都可直接使用排序函数

type Interface interface {		// 排序接口
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

func Sort(data Interface) {		// 标准库排序函数
	...
}

和结构体一样,接口也支持继承特性

type User interface {
	Say()
	GetName() string
}

type Admin interface {
    User				// 继承User接口
    TeamName string		// 自有属性
}

需要实现包括继承的所有方法,才判定实现了该接口

其他自定义类型也可以实现接口,如下X类型实现了Plus接口

type Plus interface {
	incr()
}

type X int

func (x *X) incr() {
	*x += 1
	fmt.Println(*x)
}

三、接口断言

在接口变量上操作,用于检查接口类型变量是否实现了期望的接口或者具体的类型。使用接口的本质,就是实例类型和接口类型之间转换,而是否允许转换就依赖接口断言,也可称为显示类型转换

value, ok := x.(T)

x 表示接口变量,T 表示具体类型(也可是接口类型),可根据该布尔值判断 x 是否为 T 类型。

  • 如果 T 是实例类型,类型断言会检查 x 的动态类型是否满足 T。如果成功返回 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果成功返回 值是 T 的接口值。
  • 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。

简单总结,尝试把 x 转换为 T 类型,赋值给 value 变量。

使用上面案例进行断言

var tony User = &Sales{name: "tony"}	// tony 赋值给接口变量

if value, ok := tony.(User); ok {		// true,  接口类型断言
    value.Say()	
}

_, ok = tony.(*Sales)					// true, 具体类型断言, 注意这里使用了指针类型

注意如果不接收第二个返回值(也就是 ok),断言失败时会直接造成一个 panic,对nil断言同样也会 panic。

admin := user.(Admin)	// Admin是管理员接口,断言失败panic

具体类型变量断言,可以先转为为接口,然后再进行断言

user1 := Sales{"tom"}

var data interface{} = user1	// 转换为空接口变量

if _, ok := data.(Sales); ok {	// 再进行断言
    fmt.Println("yes")
}

断言常见使用场景,异常捕获时判定错误类型

func ProtectRun(entry func()) {
    defer func() {
        err := recover()			// 获取错误类型
        
        switch err.(type) {			// 断言错误类型, 不同类型的错误采取不同的处理方式
        case runtime.Error: 
            fmt.Println("runtime error:", err)
        default:
            fmt.Println("error:", err)
        }
    }()

    ...
}

四、接口转换

go语言基本数据类型转换比较严格,所有基础类型不支持隐式转换,如下案例都不支持

s := "a" + 1

// 不同长度的整型, 也支持自动转换
var i int = 10
var n int8 = 20
m := i+n

go只能显示转换

s := "a" + string(1)	// a1

var i int = 10
var n int8 = 20
m := i + int(n)			// 30

使用接口的本质就是类型转换,赋值时转换为接口变量,执行时候转换为实例。 go 语言对于接口类型的转换则非常的灵活,实例和接口之间的转换、接口和接口之间的转换都可能是隐式的转换

var f os.File
var a io.ReadCloser = &f        // 隐式转换, *os.File 满足 io.ReadCloser 接口
var b io.Reader = a             // 隐式转换, io.ReadCloser 满足 io.Reader 接口
var c io.Closer = a             // 隐式转换, io.ReadCloser 满足 io.Closer 接口
var d io.Reader = a.(io.Reader) // 显式转换(断言), io.Closer 不满足 io.Reader 接口,注意这里没有接收第二个参数,失败会panic

有时候对象和接口之间太灵活了,导致需要人为地限制这种无意之间的适配。常见的做法是定义一个含特殊方法来区分接口。比如 runtime 包中的 Error 接口就定义了一个特有的 RuntimeError 方法,用于避免其它类型无意中适配了该接口

type runtime.Error interface {
    error
    RuntimeError()
}

不过这种做法只是君子协定,如果有人刻意伪造接口也是很容易的。再严格一点的做法是给接口定义一个私有方法。只有满足了这个私有方法的对象才可能满足这个接口,而私有方法的名字是包含包的绝对路径名的,因此只能在包内部实现这个私有方法才能满足这个接口。测试包中的 testing.TB 接口就是采用类似的技术

type testing.TB interface {
    Error(args ...interface{})
    Errorf(format string, args ...interface{})
    ...
    private()
}

不过这种通过私有方法禁止外部对象实现接口的做法也是有代价的:首先是这个接口只能包内部使用,外部包正常情况下是无法直接创建满足该接口对象的;其次,这种防护措施也不是绝对的,恶意的用户依然可以绕过这种保护机制。

通过嵌入匿名的 testing.TB 接口来伪造私有的 private 方法,因为接口方法是延迟绑定,编译时 private 方法是否真的存在并不重要。

type TB struct {
    testing.TB
}

func (p *TB) Fatal(args ...interface{}) {
    fmt.Println("TB.Fatal disabled!")
}

func main() {
    var tb testing.TB = new(TB)
    tb.Fatal("Hello, playground")
}

在自己的 TB 结构体类型中重新实现了 Fatal 方法,然后通过将对象隐式转换为 testing.TB 接口类型(因为内嵌了匿名的 testing.TB 对象,因此是满足 testing.TB 接口的),然后通过 testing.TB 接口来调用我们自己的 Fatal 方法。

五、空接口

接口定义没有声明任何方法,称为空接口,按照go规范任何类型都实现了空接口,因为都满足了两个实现条件。这就比较有意思了,空接口可以等于任何值,类似Java中的Object对象。

var data interface{}		// 定义空接口变量

data = 1
fmt.Printf("type=%T, value=%v\n", data, data)
data = "hello"
fmt.Printf("type=%T, value=%v\n", data, data)

输出如下

type=int, value=1
type=string, value=hello

函数形参是空接口类型,就表示可接收任何类型,然后再在断言,不同的类型,采用不同的逻辑,在开发框架层时经常使用

func assertion(T interface{}) {
	switch T.(type) {
	case User:
		fmt.Println("user")
	case Admin:
		fmt.Println("admin")
	default:
		fmt.Println("default")
	}
}

T.(type)能在switch中使用,可以理解定制的语法糖,否则需要使用if逐个类型断言

空接口在标准库空也有普遍使用,比如panic函数终止程序时,可传递空接口类型的参数,捕获错误时可获取文章来源地址https://www.toymoban.com/news/detail-623478.html

type any = interface{}

func panic(v any)

到了这里,关于go基础-接口的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 第七章 面向对象编程(基础)

    (1)类是抽象的,概念的,代表一类事物,比如人类、猫类... 即它是数据类型。 (2)对象是具体的,实际的,代表一个具体事物,即实例。 (3)类是对象的模板,对象是类的一个个体,对应一个实例。 属性是类的一个组成部分,一般是基本数据类型,也可是引用类型(对

    2024年02月06日
    浏览(86)
  • 【python】08.面向对象编程基础

    活在当下的程序员应该都听过\\\"面向对象编程\\\"一词,也经常有人问能不能用一句话解释下什么是\\\"面向对象编程\\\",我们先来看看比较正式的说法。 \\\"把一组数据结构和处理它们的方法组成对象(object),把相同行为的对象归纳为类(class),通过类的封装(encapsulation)隐藏内部

    2024年01月20日
    浏览(50)
  • 【java 基础二 】- 面向对象、类、接口等

    Java面向对象编程(OOP)是一种编程范式,其旨在通过将程序逻辑封装在对象中来使代码更易于理解和维护。Java是一种面向对象的编程语言,它支持封装、继承和多态等概念。以下是Java面向对象编程的核心概念: 对象(Object):对象是具有属性和行为的实体。在Java中,对象是通过

    2024年02月08日
    浏览(47)
  • Java基础(七)面向对象编程(高级)

    回顾类中的实例变量(即非static的成员变量) 创建两个Circle对象: Circle类中的变量radius是一个实例变量(instance variable),它属于类的每一个对象,c1中的radius变化不会影响c2的radius,反之亦然。 如果想让一个成员变量被类的所有实例所共享,就用static修饰即可,称为类变量(

    2023年04月16日
    浏览(43)
  • C++ 递归与面向对象编程基础

    递归是一种使函数调用自身的技术。这种技术提供了一种将复杂问题分解为简单问题的方法,从而更容易解决问题。 递归可能有点难以理解。理解其工作原理的最佳方法是通过实验来尝试。 将两个数字相加很容易做到,但将一系列数字相加就更复杂了。在下面的示例中,通

    2024年04月16日
    浏览(39)
  • 【C#基础】C# 面向对象编程

    序号 系列文章 5 【C#基础】C# 运算符总结 6 【C#基础】C# 常用语句讲解 7 【C#基础】C# 常用数据结构 😊大家好,我是writer桑,前面一章已经学习了 C# 中的常用数据结构,那本章就开始学习 C# 程序中面向对象编程的知识,希望看完大家能够有所收获,感谢支持! 面向对象编程

    2024年02月04日
    浏览(54)
  • 【Java基础篇 | 面向对象】—— 聊聊什么是接口(下篇)

    个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【JavaSE_primary】 本专栏旨在分享学习JavaSE的一点学习心得,欢迎大家在评论区交流讨论💌 上篇(【Java基础篇 | 面向对象】—— 聊聊什么是接口(上篇))中我们已经

    2024年02月05日
    浏览(31)
  • Python基础-11 面向对象编程_各种方法

    python本身支持类的动态属性 直接使用等号=,给一个属性赋值 如果这个属性存在,则会修改该属性的值 如果这个属性不存在,则会给对象添加一个新的属性及对应的值 如果需要禁止动态添加类的属性,可以使用 __slots__ 属性,如下示例 这个属性直接定义在类中,是一个元组,用来规定

    2023年04月17日
    浏览(48)
  • 7.面向对象编程(基础部分)|Java学习笔记

    java 设计者 引入 类与对象(OOP) ,根本原因就是现有的技术,不能完美的解决新的新的需求. 类是抽象的,概念的,代表一类事物,比如人类,猫类…, 即它是数据类型. 对象是具体的,实际的,代表一个具体事物, 即 是实例. 类是对象的模板,对象是类的一个个体,对应一个实例

    2024年02月08日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包