【零基础入门Go语言】struct 和 interface:Go语言是如何实现继承的?

提到面向对象编程中的继承,许多人脑海中可能会浮现出 Java、C++ 等语言中那一套熟悉的类继承体系。然而,Go 语言作为一门别具一格的编程语言,并没有遵循传统的继承模式。那么,在 Go 语言的世界里,它是怎样实现类似于继承的功能,让代码变得更加高效和灵活的呢?这就不得不深入探讨 Go 语言中的 struct 和 interface 了。接下来,就让我们一同开启这段探索之旅。

结构体

结构体(Struct)是一种聚合类型,里面可以包含任意类型的值,这些值就是我们定义的结构体的成员,也称为字段。在 Go语言中,要自定义一个结构体,需要使用 type+struct 关键字组合。

type person struct {
	name string
	age int
}

// 通用格式
type structName struct {
	fieldName typeName
}

在定义结构体时,字段的声明方法与平时声明一个变量是一样的,都是变量名在前,类型在后,只不过在结构体中,变量名称为成员名或字段名。

结构体的成员字段并不是必需的,也可以一个字段都没有,这种结构体称为空结构体。空结构体在 Go语言中是一个比较神奇且全能的存在,我们在实际开发时经常会用到这个东西,后面会专门做内容讲解空结构体的相关点。

结构体也是一种类型,所以对于以后自定义的结构体,我会称为某结构体或某类型,两者是一个意思。比如person结构体和person类型其实是一个意思。
定义好结构体后就可以使用它了,因为它是一个聚合类型,所以可以比普通的类型携带更多数据。

声明和使用

结构体类型也可以使用与普通的字符串、整型一样的方式进行声明和初始化。

// 完整声明
// 声明后未初始化时,默认会使用结构体里字段的零值。
var p person

// 简短声明
p := person{"随便寻个地方", 22}

采用字面量初始化结构体时,初始化值的顺序很重要,必须与字段定义的顺序一致。那么是否可以不按照顺序初始化呢?当然可以,只不过需要指出字段名称。

p := person{age:22, name:"随便寻个地方"}

当然你也可以只初始化字段age,字段name使用默认的零值,如下面的代码所示,仍然可以编译通过。

p := person{age:22}

声明了一个结构体变量后就可以使用它了。在Go语言中,访问一个结构体的字段与调用一个类型的方法一样,都是使用点操作符 “.”​。

fmt.Println(p.name, p.age)

结构体中的字段

结构体的字段可以是任意类型,包括自定义的结构体类型,比如下面的代码:

type person struct {
	name string
	age int
	addr address
}

type address struct {
	province string
	city string
}

通过这种方式,用代码描述现实中的实体会更匹配,复用程度也更高。对于嵌套结构体字段的结构体,其初始化与正常的结构体大同小异,只需要根据字段对应的类型初始化即可。

p:=person{
	age:30,
    name:"飞雪无情",
    addr:address{
        province: "北京",
        city:     "北京",
    },
}

如果需要访问结构体最里层的 province 字段的值,同样也可以使用点操作符,只不过需要使用两个点。

// 第一个点获取 addr,第二个点获取 addr 的 province。
fmt.Println(p.addr.province)

接口

接口是和调用方的一种约定,它是一个高度抽象的类型,不用和具体的实现细节绑定在一起。接口要做的是定义好约定,告诉调用方自己可以做什么,但不用知道它的内部实现,这和我们见到的具体的类型如 int、map、slice 等不一样。

接口的定义和结构体稍微有些差别,虽然都以 type关键字开始,但接口的关键字是 interface,表示自定义的类型是一个接口。也就是说 Stringer 是一个接口,它有一个方法 String() string

type Stringer interface {
    String() string
}

针对 Stringer 接口来说,它会告诉调用者可以通过它的 String() 方法获取一个字符串,这就是接口的约定。至于这个字符串怎么获得的,长什么样,接口不关心,调用者也不用关心,因为这些是由接口实现者来做的。

接口的实现

接口的实现者必须是一个具体的类型,继续以 person 结构体为例,让它来实现 Stringer 接口。

func (p person) String()  string{
    return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}

给结构体类型 person 定义一个方法,这个方法和接口里方法的签名(名称、参数和返回值)一样,这样结构体 person 就实现了 Stringer 接口。

注意:如果一个接口有多个方法,那么需要实现接口的每个方法才算是实现了这个接口。

实现了 Stringer 接口后就可以使用了。

func printString(s fmt.Stringer){
    fmt.Println(s.String())
}

这个被定义的函数 printString,它接收一个 Stringer 接口类型的参数,然后打印出 Stringer 接口的 String 方法返回的字符串。

printString 这个函数的优势就在于它是面向接口编程的,只要一个类型实现了 Stringer 接口,都可以打印出对应的字符串,而不用管具体的类型实现。

因为 person 实现了 Stringer 接口,所以变量 p 可以作为函数 printString 的参数,可以用如下方式打印:

printString(p)

结果为:

the name is 随便寻个地方,age is 22

现在让结构体 address 也实现 Stringer 接口,如下面的代码所示:

func (addr address) String()  string{
    return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}

因为结构体 address 也实现了 Stringer 接口,所以 printString 函数不用做任何改变,可以直接被使用,打印出地址。

printString(p.addr)
//输出:the addr is 北京北京

这就是面向接口的好处,只要定义和调用双方满足约定,就可以使用,而不用管具体实现。接口的实现者也可以更好的升级重构,而不会有任何影响,因为接口约定没有变。

值接收者和指针接收者

我们已经知道,如果要实现一个接口,必须实现这个接口提供的所有方法,而且在上一章讲解方法的时候,我们也知道定义一个方法,有值类型接收者和指针类型接收者两种。二者都可以调用方法,因为Go语言编译器自动做了转换,所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中,值类型接收者和指针类型接收者不一样,下面我会详细分析二者的区别。

在上一小节中,已经验证了结构体类型实现了Stringer接口,那么结构体对应的指针是否也实现了该接口呢?我通过下面这个代码进行测试:

printString(&p)

测试后会发现,把变量 p 的指针作为实参传给 printString 函数也是可以的,编译运行都正常。这就证明了以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口。

示例中值接收者(p person)实现了 Stringer 接口,那么类型 person 和它的指针类型 *person 就都实现了 Stringer 接口。

现在,我把接收者改成指针类型,如下代码所示:

func (p *person) String()  string{
    return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}

修改成指针类型接收者后会发现,示例中这行printString§代码编译不通过,提示如下错误:

./main.go:17:13: cannot use p (type person) as type fmt.Stringer in argument to printString:
    person does not implement fmt.Stringer (String method has pointer receiver)

意思就是类型 person 没有实现 Stringer 接口。这就证明了以指针类型接收者实现接口的时候,只有对应的指针类型才被认为实现了该接口。

我用如下表格为你总结这两种接收者类型的接口实现规则:

方法接收者实现接口的类型
(p person)person 和 *person
(p *person)*person
  • 当值类型作为接收者时,person 类型和 *person 类型都实现了该接口。
  • 当指针类型作为接收者时,只有 *person 类型实现了该接口。

可以发现,实现接口的类型都有 *person ,这也表明指针类型比较万能,不管哪一种接收者,它都能实现该接口。

继承和组合

在 Go语言中没有继承的概念,所以结构体、接口之间也没有父子关系,Go语言提倡的是组合,利用组合达到代码复用的目的,这也更灵活。

我们以 Go 语言 io 标准包自带的接口为例,讲解类型的组合(也可以称之为嵌套)。

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

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

//ReadWriter是Reader和Writer的组合
type ReadWriter interface {
    Reader
    Writer
}

ReadWriter 接口就是 ReaderWriter 的组合,组合后,ReadWriter 接口具有 ReaderWriter 中的所有方法,这样新接口 ReadWriter 就不用定义自己的方法了,组合 ReaderWriter 的就可以了。

不止接口可以组合,结构体也可以组合,现在把 address 结构体组合到结构体 person 中,而不是当成一个字段。

type person struct {
    name string
    age uint
    address
}

直接把结构体类型放进来,就是组合,不需要字段名。组合后,被组合的 address 称为内部类型,person 称为外部类型。修改了 person 结构体后,声明和使用也需要一起修改。

p:=person{
        age:30,
        name:"飞雪无情",
        address:address{
            province: "北京",
            city:     "北京",
        },
    }
//像使用自己的字段一样,直接使用
fmt.Println(p.province)

因为 person 组合了 address,所以 address 的字段就像 person 自己的一样,可以直接使用。

类型组合后,外部类型不仅可以使用内部类型的字段,也可以使用内部类型的方法,就像使用自己的方法一样。如果外部类型定义了和内部类型同样的方法,那么外部类型的会覆盖内部类型,这就是方法的覆写。关于方法的覆写,这里不再进行举例,你可以自己试一下。

小提示:方法覆写不会影响内部类型的方法实现。

类型断言

有了接口和实现接口的类型,就会有类型断言。类型断言用来判断一个接口的值是否是实现该接口的某个具体类型。

还是以我们上面小节的示例演示,我们先来回忆一下它们,如下所示:

func (p *person) String()  string{
    return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
}

func (addr address) String()  string{
    return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
}

可以看到,*personaddress 都实现了接口 Stringer,然后我通过下面的示例讲解类型断言:

var s fmt.Stringer
s = p1
p2 := s.(*person)
fmt.Println(p2)

如上所示,接口变量 s 称为接口 fmt.Stringer 的值,它被 p1 赋值。然后使用类型断言表达式 s.(*person),尝试返回一个 p2。如果接口的值 s 是一个 *person,那么类型断言正确,可以正常返回 p2。如果接口的值 s 不是一个 *person,那么在运行时就会抛出异常,程序终止运行。

小提示:这里返回的 p2 已经是 *person 类型了,也就是在类型断言的时候,同时完成了类型转换。

在上面的示例中,因为 s 的确是一个 *person,所以不会异常,可以正常返回 p2。但是如果我再添加如下代码,对 s 进行 address 类型断言,就会出现一些问题:

a:=s.(address)
fmt.Println(a)

这个代码在编译的时候不会有问题,因为 address 实现了接口 Stringer,但是在运行的时候,会抛出如下异常信息:

panic: interface conversion: fmt.Stringer is *main.person, not main.address

这显然不符合我们的初衷,我们本来想判断一个接口的值是否是某个具体类型,但不能因为判断失败就导致程序异常。考虑到这点,Go 语言为我们提供了类型断言的多值返回,如下所示:

a,ok:=s.(address)
if ok {
    fmt.Println(a)
}else {
    fmt.Println("s不是一个address")
}

类型断言返回的第二个值 “ok” 就是断言是否成功的标志,如果为 true 则成功,否则失败。

总结

这节课虽然只讲了结构体和接口,但是所涉及的知识点很多,并且非常杂乱,需要深入地学习。且由于涉及到面向对象相关的内容,在面试的时候很有可能会被问到一些比较复杂的问题,这些在后面都会一一讲解。

结构体是对现实世界的描述,接口是对某一类行为的规范和抽象。通过它们,我们可以实现代码的抽象和复用,同时可以面向接口编程,把具体实现细节隐藏起来,让写出来的代码更灵活,适应能力也更强。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/953093.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Delphi+SQL Server实现的(GUI)户籍管理系统

1.项目简介 本项目是一个户籍管理系统,用于记录住户身份信息,提供新户登记(增加)、户籍变更(修改)、户籍注销(删除)、户籍查询、曾用名查询、迁户记录查询以及创建备份、删除备份共8…

第2课 “Hello World” 与 print

1 Hello World 2 print 函数解析 2.1 基本用法 2.2 输出多个对象 2.3 使用sep参数 2.4 使用flush参数 2.5 输出到文件 3 格式化输出 3.1 格式化输出整数 3.2 格式化输出16进制整数 3.3 格式化输出浮点数(float) 3.4 格式化输出字符串(string) 3.5 输出列表与字典 …

计算机网络(四)网络层

4.1、网络层概述 简介 网络层的主要任务是实现网络互连,进而实现数据包在各网络之间的传输 这些异构型网络N1~N7如果只是需要各自内部通信,他们只要实现各自的物理层和数据链路层即可 但是如果要将这些异构型网络互连起来,形成一个更大的互…

qt 窗口(window/widget)绘制/渲染顺序 QPainter QPaintDevice Qpainter渲染 失效 无效 原因

qt窗体布局 窗体渲染过程 qt中窗体渲染逻辑顺序为 本窗体->子窗体/控件 递归,也就是说先渲染父窗体再渲染子窗体。其中子窗体按加入时的先后顺序进行渲染。通过下方的函数调用堆栈可以看出窗体都是在widget组件源码的widgetprivate::drawwidget中进行渲染的&am…

网络安全-kail linux 网络配置(基础篇)

一、网络配置 1.查看网络IP地址, 我的kail:192.168.15.128 使用ifconfig查看kail网络连接情况,ip地址情况 又复制了一台kail计算机的IP地址。 再看一下windows本机:使用ipconfig进行查看: 再看一下虚拟机上的win7I…

Edge浏览器内置的截长图功能

Edge浏览器内置截图功能 近年来,Edge浏览器不断更新和完善,也提供了长截图功能。在Edge中,只需点击右上角的“...”,然后选择“网页捕获”->“捕获整页”,即可实现长截图。这一功能的简单易用,使其成为…

【NLP】语言模型的发展历程 (1)

语言模型的发展历程系列博客主要包含以下文章: 【NLP】语言模型的发展历程 (1)【NLP】大语言模型的发展历程 (2) 本篇博客是该系列的第一篇,主要讲讲 语言模型(LM,Language Model) 的发展历程。 文章目录 一、统计语…

【ASP.NET学习】ASP.NET MVC基本编程

文章目录 ASP.NET MVCMVC 编程模式ASP.NET MVC - Internet 应用程序创建MVC web应用程序应用程序信息应用程序文件配置文件 用新建的ASP.NET MVC程序做一个简单计算器1. **修改视图文件**2. **修改控制器文件** 用新建的ASP.NET MVC程序做一个复杂计算器1.创建模型(…

蓝桥云客第 5 场 算法季度赛

题目: 2.开赛主题曲【算法赛】 - 蓝桥云课 问题描述 蓝桥杯组委会创作了一首气势磅礴的开赛主题曲,其歌词可用一个仅包含小写字母的字符串 S 表示。S 中的每个字符对应一个音高,音高由字母表顺序决定:a1,b2,...,z26。字母越靠后…

计算机网络 (37)TCP的流量控制

前言 计算机网络中的TCP(传输控制协议)流量控制是一种重要机制,用于确保数据在发送方和接收方之间的传输既高效又稳定。 一、目的 TCP流量控制的主要目的是防止发送方发送数据过快,导致接收方无法及时处理,从而引起数据…

【Elasticsearch7.11】postman批量导入少量数据

JSON 文件内的数据格式,json文件数据条数不要过多,会请求参数过大,最好控制再10000以内。 {"index":{"_id":"baec07466732902d22a24ba01ff09751"}} {"uuid":"baec07466732902d22a24ba01ff0975…

Spring Boot 支持哪些日志框架

Spring Boot 支持多种日志框架,主要包括以下几种: SLF4J (Simple Logging Facade for Java) Logback(默认)Log4j 2Java Util Logging (JUL) 其中,Spring Boot 默认使用 SLF4J 和 Logback 作为日志框架。如果你需要使…

AIDD - 人工智能药物设计 -深度学习赋能脂质纳米颗粒设计,实现高效肺部基因递送

Nat. Biotechnol. | 深度学习赋能脂质纳米颗粒设计,实现高效肺部基因递送 今天为大家介绍的是来自美国麻省理工和爱荷华大学卡弗医学院团队的一篇论文。可离子化脂质(ionizable lipids)是脂质纳米颗粒(lipid nanoparticles&#…

【SVN】版本发布快捷操作

摘要:因为每次发版都需要制作一份相同的文件夹,而大部分的包都不需要变更,但是文件又非常大,记录自己的操作经验。 首先在SVN Repository Browser 界面把上一次的版本复制一份,复制的时候重命名为新的版本号 右击要复…

AR 眼镜之-拍照/录像动效切换-实现方案

目录 📂 前言 AR 眼镜系统版本 拍照/录像动效切换 1. 🔱 技术方案 1.1 技术方案概述 1.2 实现方案 1)第一阶段动效 2)第二阶段动效 2. 💠 默认代码配置 2.1 XML 初始布局 2.2 监听滑动对 View 改变 3. ⚛️…

HTML5实现好看的端午节网页源码

HTML5实现好看的端午节网页源码 前言一、设计来源1.1 网站首页界面1.2 登录注册界面1.3 端午节由来界面1.4 端午节习俗界面1.5 端午节文化界面1.6 端午节美食界面1.7 端午节故事界面1.8 端午节民谣界面1.9 联系我们界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载结束语 H…

Android使用系统消息与定时器实现霓虹灯效果

演示效果: 界面设计: 在帧布局FrameLayout中添加6个TextView 依次设置这6个TextView的宽,高,权重 也可在XML中直接设置 添加自定义颜色 关联自定义颜色到数组变量 关联6个TextView控件到数组变量 处理自定义系统消息 Handler _sysHandler new Han…

多活架构的实现原理与应用场景解析

一、多活架构为何如此重要? 企业的业务运营与各类线上服务紧密相连,从日常的购物消费、社交娱乐,到金融交易、在线教育等关键领域,无一不依赖于稳定可靠的信息系统。多活架构的重要性愈发凸显,它宛如一位忠诚的卫士,为业务的平稳运行保驾护航。 回想那些因系统故障引发的…

【JVM-2.2】使用JConsole监控和管理Java应用程序:从入门到精通

在Java应用程序的开发和运维过程中,监控和管理应用程序的性能和资源使用情况是非常重要的。JConsole是Java Development Kit(JDK)自带的一款图形化监控工具,它可以帮助开发者实时监控Java应用程序的内存、线程、类加载以及垃圾回收…

《自动驾驶与机器人中的SLAM技术》ch2:基础数学知识

目录 2.1 几何学 向量的内积和外积 旋转矩阵 旋转向量 四元数 李群和李代数 SO(3)上的 BCH 线性近似式 2.2 运动学 李群视角下的运动学 SO(3) t 上的运动学 线速度和加速度 扰动模型和雅可比矩阵 典型算例:对向量进行旋转 典型算例:旋转的复合 2.3 …