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