【启程Golang之旅】从结构到接口揭秘Go的“面向对象”面纱

2024-06-02 15:44

本文主要是介绍【启程Golang之旅】从结构到接口揭秘Go的“面向对象”面纱,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

欢迎来到Golang的世界!在当今快节奏的软件开发领域,选择一种高效、简洁的编程语言至关重要。而在这方面,Golang(又称Go)无疑是一个备受瞩目的选择。在本文中,带领您探索Golang的世界,一步步地了解这门语言的基础知识和实用技巧。

目录

初识面向对象

方法的引入

封装的引入

继承的引入

接口的引入

多态的引入

断言的引入


初识面向对象

在Go语言中,虽然它并没有像Java或C++那样显式地支持类和继承这样的传统面向对象编程(OOP)特性,但Go语言仍然支持面向对象编程的许多核心概念,如封装、继承(通过组合和接口实现)和多态。以下是Go语言中面向对象使用结构体的简单案例:

package main
import "fmt"
// Student 定义学生的结构体,将学生中的各个属性统一放入结构体中管理
type Student struct {// 变量名字大写外界可以访问到这个属性Name   stringAge    intSchool string
}func main() {// 直接创建// 创建学生结构体的实例、对象、变量;var t1 Studentfmt.Println(t1) // 在未赋值时,结果为 { 0 }// 开始赋值t1.Name = "张三"t1.Age = 18t1.School = "北大"fmt.Println(t1) // {张三 18 北大}// 第二种var t2 Student = Student{"张三", 18, "北大"}fmt.Println(t2) // {张三 18 北大}//第三种:返回的是一个指针var t3 *Student = new(Student)// t3是指针,t3指向的就是地址,应该给这个地址的指向的对象的字段赋值(*t3).Name = "张三"(*t3).Age = 45   // *的作用是根据地址取值fmt.Println(*t3) // {张三 45 }// 为了符合程序员的编程习惯,go提供了简化的赋值方式t3.School = "北大" // go编译器底层对t3.School转化 (*t3).School = "北大"fmt.Println(*t3) // {张三 45 北大}//第四种var t4 *Student = &Student{"张三", 18, "北大"}fmt.Println(*t4) // {张三 18 北大}
}

最终实现的效果如下:

当然我们也可以对结构体之间进行转换,因为结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型):

package main
import "fmt"
type Student struct {Age int
}
type Teacher struct {Age int
}func main() {var a Student = Student{Age: 18}var b Teacher = Teacher{Age: 20}a = Student(b) // 类型转换fmt.Println(a) // {20}fmt.Println(b) // {20}
}

结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转

package main
import "fmt"
type Student struct {Age int
}
type Stu Studentfunc main() {var s1 Student = Student{Age: 1}var s2 Stu = Stu{Age: 10}s1 = Student(s2) // 类型转换fmt.Println(s1) // {10}fmt.Println(s2) // {10}
}

方法的引入

在go语言中,虽然没有类的概念,但是可以通过定义结构体和与结构体关联的方法来实现面向对象的编程,在这种方式下,方法是与特定类型关联的函数,方法是作用在指定的数据类型上,和指定的数据类型绑定,因此自定义类型都可以有方法,而不仅仅是struct,方法的声明和调用的格式如下:

type A struct {Name string
}func (a A) test() {println(a.Name)
}

ok,接下来通过具体的代码示例进行简单的讲解,如下:

package main
import "fmt"
type Person struct {Name string
}func (p Person) test() {p.Name = "李四"fmt.Println(p.Name) // 李四
}
func main() {var p Personp.Name = "张三"p.test()fmt.Println(p.Name) // 张三
}

注意:根据上面的示例代码,我们注意到以下几点

1)test方法中的参数名字随意起

2)结构体person和test方法绑定,调用test方法必须靠指定的类型,person

3)如果其他类型变量调用test方法一定会报错

4)结构体对象传入test方法中,值传递和函数参数传递一致

5)方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。

结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式:

如果程序员希望在方法中改变结构体变量的值,可以通过结构体指针的方式来处理:

指针和不加指针的区别在于方法对原始变量的影响。使用指针接收者的方法可以直接修改原始变量的值,而不使用指针接收者的方法只能修改方法内部的副本,不会影响原始变量的值:

package main
import "fmt"
type integer int
func (i integer) print() {i = 30fmt.Println("i = ", i) // 30
}func (i *integer) change() {*i = 15fmt.Println("i = ", *i) // 15
}
func main() {var i integer = 10i.print()i.change()fmt.Println(i) // 15
}

如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出,以后定义结构体的话,常定义String()作为输出结构体信息的方法,会自动调用。

方法与函数的区别

1)绑定指定类型

方法:需要绑定指定数据类型;函数:不需要绑定数据类型

2)调用方式不一样

函数的调用方式:函数名(实参列表);方法的调用方式:变量.方法名(实参列表)

对于函数来说,参数类型对应是什么就要传入什么;对于方法来说,接收者为值类型可以传入指针类型,接收者为指针类型,可以传入值类型,示例代码如下:

package main
import "fmt"
type Student struct {Name string
}// 定义方法
func (s Student) test01() {fmt.Println(s.Name) // 张三
}// 定义函数
func method01(s Student) {fmt.Println(s.Name) // 张三
}func main() {// 调用函数var s Student = Student{"张三"}method01(s)// 调用方法s.test01()
}

如果想跨包创建实例的话,和以前的方法一致,如下:

要知道我们跨包访问变量的话,变量的首字母必须大写,对于结构体来说也是一样的,那有没有办法让结构体首字母小写也能跨包呢?这里需要采用工厂模式:

封装的引入

在go语言中,封装是面向对象编程(OOP)的一个重要概念,尽管go语言并没有明确地支持类和继承这样的传统OOP特性,但它仍然提供了封装的能力。封装主要是指将数据(字段)和与这些数据相关的操作(方法)组合在一个结构体(struct)中,并通过控制对结构体的访问权限来保护数据的完整性:

golang中如何实现封装:

1)建议将结构体、字段(属性)的首字母小写(其它包不能使用,类似private,实际开发不小写也可能,因为封装没有那么严格)

2)给结构体所在包提供一个工厂模式的函数,首字母大写 (类似一个构造函数)

3)提供一个首字母大写的set方法(类似其它语言的public),用于对属性判断并赋值

这里我给出如下封装代码:

package testUtils
import "fmt"
type person struct {Name stringage  int // 首字母小写,其他包不能直接访问
}// NewPerson 定义工厂模式的函数,相当于构造器
func NewPerson(name string) *person {return &person{Name: name,}
}// 定义set和get方法,对age字段进行封装,因为在方法中可以加一系列的限制操作,确保被封装字段的安全合理性
func (p *person) SetAge(age int) {if age < 0 || age > 150 {fmt.Println(age, "年龄不合法")}p.age = age
}
func (p *person) GetAge() int {return p.age
}

接下来在main函数中开始调用结构体中的实例,如下:

package main
import ("fmt""testUtils"
)func main() {// 创建person结构体的实例p := testUtils.NewPerson("张三")p.SetAge(20)fmt.Println(p.Name)     // 张三fmt.Println(p.GetAge()) // 20fmt.Println(*p)         // {张三 20}
}

继承的引入

        当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法,其它的结构体不需要重新定义这些属性和方法,只需嵌套一个匿名结构体即可。也就是说:在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性,如下:

如下代码给出具体的示例:

package main
import "fmt"
// 定义动物结构体
type Animal struct {Name stringAge  int
}// 给Animal绑定方法
func (a *Animal) Speak() {fmt.Println("动物说话")
}
func (a *Animal) showInfo() {fmt.Println("动物名称:", a.Name, "年龄:", a.Age)
}
// 定义猫结构体
type Cat struct {Animal // 匿名嵌入
}// 对cat绑定特有方法
func (c *Cat) CatSpeak() {fmt.Println("喵喵~")
}
func main() {// 创建Cat结构体示例cat := &Cat{}cat.Animal.Name = "小猫"cat.Animal.Age = 2cat.Animal.Speak()    // 动物说话cat.Animal.showInfo() // 动物名称:小猫 年龄:2cat.CatSpeak()        // 喵喵~
}

注意

1)结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用:

2)匿名结构体字段访问可以简化:

3)当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访间原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分

4)golang中支持多继承:如一个结构体嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。为了保证代码的简洁性,建议大家尽量不使用多重继承,很多语言就将多重继承去除了,但是go中保留了。

5)如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分

6)结构体的匿名字段是基本数据类型

7)嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

8)嵌入匿名结构体的指针也是可以的

9)结构体的字段可以是结构体类型的(组合模式)

接口的引入

在go语言中,接口(Interface)是一种类型,它定义了一组方法的集合,但没有为这些方法提供实现,任何实现了这些方法的类型都隐式地实现了该接口,无需显式声明。这种特性使得接口在Go中成为实现多态性的主要方式:

package main
import "fmt"
// 接口的定义:定义规则、定义规范、定义某种能力
type SayHello interface {// 声明没有实现的方法sayHello()
}// 接口的实现,定义一个结构体
type Chinese struct{}
type Amerian struct{}// 实现接口的方法
func (person *Chinese) sayHello() {fmt.Println("你好,中国")
}
func (person *Amerian) sayHello() {fmt.Println("Hello, America")
}func main() {// 创建中国人var chinese = Chinese{}// 创建美国人var amerian = Amerian{}// 调用接口的方法chinese.sayHello() // 你好,中国amerian.sayHello() // Hello, America
}

注意

1)接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量。

2)只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

3)一个自定义类型可以实现多个接口。

4)一个接口(比如A接口)可以继承多个别的接口(比如B,c接口),这时如果要实现A接口,也必须将B,c接口的方法也全部实现。

5)interface类型默认是一个指针(引l用类型),如果没有对interface初始化就使用,那么会输出nil

6)空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把如何一个变量赋给空接口。

最后总结

1)接口中可以定义一组方法,但不需要实现,不需要方法体。并且接口中不能包含任何变量。到某个自定义类型要使用的时候(实现接口的时候),再根据具体情况把这些方法具体实现出来。

2)实现接口要实现所有的方法才是实现。

3)golang中的接,不需要显式的实现接口,golang中没有implement关键字。

4)接口目的是为了定义规范,具体由别人来实现即可。

多态的引入

在go语言中,多态(Polymorphism)是一个重要的面向对象编程的概念,它指的是不同对象对同一消息做出不同的响应,在go中多态性主要通过接口(Interface)来实现,但go并没有传统意义上的类和继承机制:

在go中,多态性主要体现在以下几个方面:

1)接口作为类型:Go的接口定义了一组方法的集合,任何实现了这些方法的类型都可以被视为该接口类型的实例。这意味着,我们可以将实现了相同接口的不同类型的对象赋值给接口类型的变量,并通过这个接口变量调用接口中定义的方法。由于不同的类型可能会以不同的方式实现这些方法,因此通过接口调用这些方法时就会表现出多态性。

2)隐藏具体实现:通过接口,我们可以隐藏对象的具体类型和实现细节,只关注对象的行为(即接口中定义的方法)。这使得我们可以编写更加灵活和可重用的代码,因为我们可以将任何实现了特定接口的对象传递给函数或方法,而无需关心其具体的类型。

3)动态类型绑定:在运行时,Go会根据接口变量所引用的对象的实际类型来调用相应的方法实现。这种动态类型绑定的特性使得我们可以在不修改代码的情况下,通过替换实现了相同接口的不同对象来改变程序的行为。

package mainimport "fmt"// 定义一个接口
type Shape interface {Area() float64Perimeter() float64
}// 矩形类型实现了Shape接口
type Rectangle struct {width, height float64
}func (r Rectangle) Area() float64 {return r.width * r.height
}func (r Rectangle) Perimeter() float64 {return 2 * (r.width + r.height)
}// 圆形类型实现了Shape接口
type Circle struct {radius float64
}func (c Circle) Area() float64 {return 3.14 * c.radius * c.radius
}func (c Circle) Perimeter() float64 {return 2 * 3.14 * c.radius
}// 一个函数,接受Shape接口类型的参数
func printInfo(s Shape) {fmt.Println("Area:", s.Area())fmt.Println("Perimeter:", s.Perimeter())
}func main() {rect := Rectangle{width: 4, height: 5}circle := Circle{radius: 3}printInfo(rect)   // 调用Rectangle的Area和Perimeter方法printInfo(circle) // 调用Circle的Area和Perimeter方法
}

断言的引入

        在Go语言中,断言(Assertion)通常与接口(Interface)和类型断言(Type Assertion)相关。类型断言用于在运行时检查接口变量中存储的具体类型,并尝试将其转换为该类型。如果接口变量确实包含该类型的值,则断言成功;否则,断言失败并可能导致运行时错误:

非安全断言(也称为显式类型断言):使用类型变量.(类型)的形式进行断言。如果接口变量不包含该类型的值,则会引发运行时错误(panic):

var x interface{} = "hello"  
s := x.(string) // 如果x包含字符串,则s将接收该字符串;否则,panic

安全断言(也称为类型选择):使用类型变量, ok := 类型变量.(类型)的形式进行断言。如果接口变量包含该类型的值,则ok为true,并且该值会被赋予相应的变量;如果接口变量不包含该类型的值,则ok为false,并且不会引发运行时错误:

var x interface{} = "hello"  
if s, ok := x.(string); ok {  // 如果x是字符串,则s将接收该字符串,并且ok为true  fmt.Println(s)  
} else {  // 如果x不是字符串,则不会执行这里的代码  fmt.Println("x is not a string")  
}

在Go中,断言通常用于处理空接口interface{}类型的变量,因为空接口可以存储任何类型的值。通过使用断言,我们可以将空接口变量转换为具体的类型,以便我们可以调用该类型的特定方法或访问其字段。

断言在Go的并发编程和接口交互中特别有用,特别是当你不知道一个接口变量具体包含什么类型的值时。通过断言,你可以编写更加灵活和健壮的代码,能够处理不同类型的值,并在运行时进行类型检查。

这篇关于【启程Golang之旅】从结构到接口揭秘Go的“面向对象”面纱的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1024396

相关文章

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

揭秘世界上那些同时横跨两大洲的国家

我们在《世界人口过亿的一级行政区分布》盘点全球是那些人口过亿的一级行政区。 现在我们介绍五个横跨两州的国家,并整理七大洲和这些国家的KML矢量数据分析分享给大家,如果你需要这些数据,请在文末查看领取方式。 世界上横跨两大洲的国家 地球被分为七个大洲分别是亚洲、欧洲、北美洲、南美洲、非洲、大洋洲和南极洲。 七大洲示意图 其中,南极洲是无人居住的大陆,而其他六个大洲则孕育了众多国家和

三国地理揭秘:为何北伐之路如此艰难,为何诸葛亮无法攻克陇右小城?

俗话说:天时不如地利,不是随便说说,诸葛亮六出祁山,连关中陇右的几座小城都攻不下来,行军山高路险,无法携带和建造攻城器械,是最难的,所以在汉中,无论从哪一方进攻,防守方都是一夫当关,万夫莫开;再加上千里运粮,根本不需要打,司马懿只需要坚守城池拼消耗就能不战而屈人之兵。 另一边,洛阳的虎牢关,一旦突破,洛阳就无险可守,这样的进军路线,才是顺势而为的用兵之道。 读历史的时候我们常常看到某一方势

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

无线领夹麦克风什么牌子好用?揭秘领夹麦克风哪个牌子音质好!

随着短视频行业的星期,围绕着直播和视频拍摄的电子数码类产品也迎来了热销不减的高增长,其中除了数码相机外,最为重要的麦克风也得到了日益增长的高需求,尤其是无线领夹麦克风,近几年可谓是异常火爆。别看小小的一对无线麦克风,它对于视频拍摄的音质起到了极为关键的作用。 不过目前市面上的麦克风品牌种类多到让人眼花缭乱,盲目挑选的话容易踩雷,那么无线领夹麦克风什么牌子好用?今天就给大家推荐几款音质好的

负债不再是障碍?银行信贷“白名单“揭秘

谈及银行信贷产品,常闻有言称存在无需考量负债与查询记录之奇品,此等说法十有八九为中介诱人上钩之辞。轻信之下,恐将步入连环陷阱。除非个人资质出类拔萃,如就职于国央企或事业单位,工龄逾年,五险一金完备,还款能力卓越,或能偶遇线下产品对查询记录稍显宽容,然亦非全然无视。宣称全然不顾者,纯属无稽之谈。 银行非慈善机构,不轻易于困境中援手,更偏爱锦上添花之举。若无坚实资质,即便求助于银行亦难获青睐。反

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

Go Playground 在线编程环境

For all examples in this and the next chapter, we will use Go Playground. Go Playground represents a web service that can run programs written in Go. It can be opened in a web browser using the follow

go基础知识归纳总结

无缓冲的 channel 和有缓冲的 channel 的区别? 在 Go 语言中,channel 是用来在 goroutines 之间传递数据的主要机制。它们有两种类型:无缓冲的 channel 和有缓冲的 channel。 无缓冲的 channel 行为:无缓冲的 channel 是一种同步的通信方式,发送和接收必须同时发生。如果一个 goroutine 试图通过无缓冲 channel

Java 后端接口入参 - 联合前端VUE 使用AES完成入参出参加密解密

加密效果: 解密后的数据就是正常数据: 后端:使用的是spring-cloud框架,在gateway模块进行操作 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.0-jre</version></dependency> 编写一个AES加密