Golang--面向对象
Golang语言面向对象编程说明:
- Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。
- Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,可以理解Golang是基于struct来实现OOP特性的。
- Golang面向对象编程非常简洁,去掉了传统OOP语言的方法重载、构造函数和析构函数、隐藏的this指针等等。
- Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承:Golang没有extends 关键字,继承是通过匿名字段来实现。
1、结构体
package main
import "fmt"
//定义结构体
type Stu struct{
//变量名字大写外界可以访问,小写外界不能访问
Name string
Age int
Num string
}
func main(){
//创建结构体实例
var stu1 Stu
fmt.Println(stu1)//在未赋值时默认值:{ 0 }
stu1.Name = "张三"
stu1.Age = 18
stu1.Num = "1001"
fmt.Println(stu1)
//创建结构体实例
var stu2 Stu = Stu{"李四",19,"1002"}
fmt.Println(stu2)
//创建结构体实例
var stu3 = Stu{"王五",20,"1003"}
fmt.Println(stu3)
//创建结构体实例
stu4 := Stu{"赵六",21,"1004"}
fmt.Println(stu4)
//创建结构体实例 指针类型
stu5 := &Stu{"田七",22,"1005"}
fmt.Println(stu5)
//创建结构体实例 指针类型
stu6 := &Stu{"周八",23,"1006"}
fmt.Println(stu6)
//创建结构体实例
stu7 := new(Stu)
stu7.Name = "吴九"
stu7.Age = 24
stu7.Num = "1007"
fmt.Println(stu7)
}
1.1 结构体实例创建
package main import "fmt" //定义结构体 type Stu struct{ //变量名字大写外界可以访问,小写外界不能访问 Name string Age int Num string } func main(){ //方式一:直接创建 var s1 Stu s1.Name = "张三" s1.Age = 18 s1.Num = "1001" fmt.Println(s1) fmt.Printf("%T\n",s1) // 输出的是结构体的类型--> main.Stu //方式二:创建时直接赋值 var s2 = Stu{"李四",19,"1002"} fmt.Println(s2) //方式三:new关键字创建,返回结构体指针 var p *Stu = new(Stu) (*p).Name = "王五" (*p).Age = 20 p.Num = "1003" // 结构体指针可以直接访问结构体成员(简化) fmt.Println(*p) // 输出的是结构体的值 fmt.Println(p) // 输出的是结构体的地址 //方式四:返回结构体指针 var p2 = &Stu{"赵六",21,"1004"} p2.Age = 11 fmt.Println(*p2) //输出的是结构体的值 fmt.Println(p2) //输出的是结构体的地址 fmt.Printf("%p\n",p2) // 输出的是结构体的地址 fmt.Printf("%T\n",p2) // 输出的是结构体的类型--> *main.Stu }
1.2 创建结构体实例时指定字段值
package main
import "fmt"
type Stu struct{
Age int
Name string
}
func main(){
//方式一:按照顺序赋值--缺点:如果顺序改变,就会报错
s := Stu{10,"张三"}
fmt.Println(s)
//方式二:按照指定变量赋值,可以不按照顺序赋值
s1 := Stu{
Name:"李四",
Age:11, // 分行赋值,最后一行需要加逗号
}
fmt.Println(s1)
//方式三:想要返回结构体的指针类型
s3 := &Stu{12,"王五"}
fmt.Println(s3) // &{12 王五}
fmt.Println(*s3) //{12 王五}
}
1.3 跨包创建结构体实例
- 创建不同的包
- student --> package model
- main.go --> package main
结构体名字首字母小写,并可以进行跨包访问 --> 工厂模式
1.4 结构体之间的转换
1、结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数、类型、顺序需要完全一致)
package main import "fmt" type Stu struct{ Age int Name string } type Person struct{ Age int Name string } func main(){ s1 := Stu{18,"张三"} p1 := Person{19,"李四"} p1 = Person(s1) fmt.Println(s1) fmt.Println(p1) }
2、 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
package main import "fmt" type Stu struct{ Age int Name string } type Per Stu func main(){ var s1 Stu = Stu{20,"张三"} var p1 Per = Per{19,"李四"} p1 = Per(s1) fmt.Println(s1) // 输出的是结构体的值 fmt.Printf("%T\n",s1) // 输出的是结构体的类型--> main.Stu fmt.Println(p1) // 输出的是结构体的值 fmt.Printf("%T\n",p1) // 输出的是结构体的类型--> main.Per }
2、方法
方法:指作用在指定的数据类型上,和指定的数据类型绑定,因此自定义类型都可以方法,不仅仅时struct。
函数:函数是独立的代码块,可以接受参数并返回结果。
区别:
- 函数:不需要接收者,可以被独立调用。
- 方法:必须有一个接收者,该接收者可以是一个值(值接收者)或指针(指针接收者),并且在调用方法时,需要通过接收者的实例来调用。
调用:
package main import "fmt" type Stu struct{ Age int Name string } //(s Stu)用于给Stu绑定Print方法,s Stu表示接收者 //结构体传递是值传递,不影响原结构体的值 func (s Stu) Print(){ s.Age = 100 fmt.Println(s.Age,s.Name) // {100 张三} } func main(){ var s1 Stu = Stu{20,"张三"} s1.Print() //{100 张三} fmt.Println(s1) //{20 张三} }
注意:
- 结构体和方法绑定,调用该方法时必须靠指定的类型(其他类型变量调用会报错)
- 结构体对象传入方法中,是进行了值传递(副本),副本改变不影响原结构体
- 结构体对象传入方法中,如果需要在方法中改变原结构体的值,可以通过结构体指针的方式来处理
package main import "fmt" type Stu struct{ Age int Name string } //(s Stu)用于给Stu绑定Print方法,s Stu表示接收者 //结构体传递是值传递,不影响原结构体的值 func (s *Stu) Print(){ s.Age = 100 // 结构体指针可以直接访问结构体成员(简化) fmt.Println(s.Age,s.Name) // {100 张三} } func main(){ var s1 Stu = Stu{20,"张三"} (&s1).Print() //或者是s1.Print(),都可以实现调用 fmt.Println(s1) //{100 张三} }
Golang中的方法作用在指定的数据类型上的,和指定的数据类型绑定,因此自定义类型都可以有方法,而不仅仅时struct,比如int,float32等都可以有方法(借用type来给基本数据类型取别名,Golang认为是新的数据类型,用这个特性来绑定方法)
package main import "fmt" type interger int func (i interger) Print(){ i = 11111111 fmt.Println(i) // 11111111 //fmt.Printf("%T\n",i) // main.interger } func (i *interger) change(){ *i = 1111 fmt.Println(*i) } func main(){ var i interger = 100 i.Print() // 11111111 fmt.Println(i) // 100--不改变 i.change() //1111 fmt.Println(i) //1111--改变 }
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问;方法首字母大写,可以在本包和其他包访问
- 如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
package main import "fmt" type Stu struct{ Age int Name string } func (s *Stu) String() string{ str := fmt.Sprintf("Age:%d Name:%s",s.Age,s.Name) return str } // func (s Stu) String() string{ // str := fmt.Sprintf("Age:%d Name:%s",s.Age,s.Name) // return str // } func main(){ var s Stu = Stu{10,"张三"} fmt.Println(&s) //fmt.Println(s) }
简化:
- 使用结构体或结构体指针调用变量或方法时,指针调用可以不用*
- 使用结构体或结构体指针作为参数传参时,结构体传给结构体指针可以不用&(对于方法来说,接收者为值类型,可以传入指针类型(但还是值传递),接收者为指针类型,可以传入值类型)
3、封装
什么是封装:
封装就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作方法,才能对字段进行操作。
封装的好处:
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理
Golang中如何实现封装:
- 建议将结构体、字段(属性)的首字母小写(其它包不能使用,类似private,实际开发不小写也可能,因为封装没有那么严格)
- 给结构体所在包提供一个工厂模式的函数,首字母大写(类似一个构造函数)
- 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
- 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
4、继承
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法,其它的结构体不需要重所定义这些属性和方法,只需嵌套一个匿名结构体即可。在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。(提高代码复用性和扩展性)
package main
import "fmt"
type Animal struct{
Age int;
Wei int;
}
func (an *Animal) Eat(name string){
fmt.Printf("%s: eat\n",name)
}
func (an *Animal) Run(name string){
fmt.Printf("%s: run\n",name)
}
type Dog struct{
Animal
Name string
}
func (dog *Dog) Shout(){
fmt.Println("汪")
}
type Cat struct{
Animal
Name string
}
func (cat *Cat)Shout(){
fmt.Println("喵")
}
func main(){
var cat Cat = Cat{Animal{10,10},"小白"}
var dog Dog = Dog{Animal{6,20},"小黑"}
//cat
cat.Animal.Eat(cat.Name) // 小白: eat
cat.Animal.Run(cat.Name) // 小白: run
cat.Shout() // 喵
//dog
dog.Animal.Eat(dog.Name) // 小黑: eat
dog.Animal.Run(dog.Name) // 小黑: run
dog.Shout() // 汪
}
注意:
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
- 匿名结构体字段访问可以简化(例如:cat.Animal.Eat(cat.Name) --> cat.Eat(car.Name) )
调用过程:cat.Eat(car.Name)先在对应的Cat结构体中找是否有Eat方法,如果有则直接访问;如果没有则就去找嵌入的结构体中继续寻找(就近原则) - 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访间原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
- Golang中支持多继承:如一个结构体嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。为了保证代码的简洁性,建议大家尽量不使用多重继承,很多语言就将多重继承去除了(可能出现菱形继承的问题),但是Go中保留了
- 嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过指明匿名结构体类型名来区分。
- 结构体的匿名字段可以是基本数据类型(这与既不继承没关系)
package main import "fmt" type Animal struct{ int } func main(){ var a Animal = Animal{10} fmt.Println(a.int) // 10 }
-
嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
-
嵌入匿名结构体的指针也是可以的
-
结构体的字段可以是结构体类型的(组合模式)
5、接口
package main
import "fmt"
//接口的定义:定义规则,定义规范,定义某种能力
//interface接口类型
type SayHello interface{
//只声明,没有实现的方法
sayHello()
}
//接口的实现:定义一个结构体
type Chinese struct{
}
//实现接口的方法--具体的实现
func (ch Chinese) sayHello(){
fmt.Println("你好")
}
//接口的实现:定义一个结构体
type American struct{
}
//实现接口的方法--具体的实现
func (am American) sayHello(){
fmt.Println("hello")
}
//定义一个函数:接收具备SayHello接口的能力的变量、
func greet(s SayHello){
s.sayHello()
}
func main(){
c := Chinese{}
a := American{}
//调用函数
greet(c)
greet(a)
}
- 接口中可以定义一组方法,但不需要实现。并且接口中不能包含任何变量。到某个自定义类型要使用的时候(实现接口的时候),再根据具体情况把这些方法具体实现出来。
- 实现接口要实现所有的方法
- Golang中实现接口是基于方法的,不是基于接口的(例如:A接口有a和b方法,B接口有a和b方法,C结构体实现了a和b方法,那么C实现了A接口,也实现了B接口。只要实现全部方法即可。和实际接口耦合性很低)
- 接口目的是为了定义规范,具体由别人来实现即可
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
//SayHello是接口类型 //直接用接口创建实例,会出错: var s SayHello //error s.sayHello() //error //但是可以指向一个实现了该接口的自定义类型的变量 //American -> 实现了接口的自定义类型 var am American var s SayHello = am s.sayHello()
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
package main import "fmt" type Print interface{ print() } type interger int func (i interger)print(){ fmt.Printf("%T:%d\n",i,i) } type str string func (s str)print(){ fmt.Printf("%T:%s\n",s,s) } func f(p Print){ p.print() } func main(){ var i interger = 11111 var s str = "hello" f(i) //main.interger:11111 f(s) //main.str:hello }
- 一个自定义类型可以实现多个接口
- 一个接口(比如A接口)可以继承多个别的接口(比如B、C接口),这时如果要实现A接口,也必须将B、C接口的方法也全部实现
package main import "fmt" type A interface{ a() } type B interface{ b() } //C接口继承AB接口 type C interface{ A B c() } type D struct{ } func (d D)a(){ fmt.Println("a") } func (d D)b(){ fmt.Println("b") } func (d D)c(){ fmt.Println("c") } func Print(c C){ c.a() c.b() c.c() } func main(){ d := D{} Print(d) //a b c }
- interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
- 空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋值给空接口
var s str = str{} var a interface{} = s
6、多态
多态:变量(实例)具有多种形态。面向对象的第三大特征之一,在Go语言,多态特征是通过接口实现。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
package main
import "fmt"
//接口的定义:定义规则,定义规范,定义某种能力
//interface{} 空接口,没有任何方法
type SayHello interface{
//只声明,没有实现的方法
sayHello()
}
//接口的实现:定义一个结构体
type Chinese struct{
}
//实现接口的方法--具体的实现
func (ch Chinese) sayHello(){
fmt.Println("你好")
}
//接口的实现:定义一个结构体
type American struct{
}
//实现接口的方法--具体的实现
func (am American) sayHello(){
fmt.Println("hello")
}
//定义一个函数:接收具备SayHello接口的能力的变量、
func greet(s SayHello){
s.sayHello() //s可以通过上下文识别具体是什么类型的实例,这就体现了多态
}
func main(){
c := Chinese{}
a := American{}
//调用函数
greet(c)
greet(a)
}
接口体现多态特征:
- 多态参数:例如上面greet函数中的s变量
- 多态数组:比如上面的示例中,定义SayHello数组,存放中国人结构体和美国人结构体
package main import "fmt" //接口的定义:定义规则,定义规范,定义某种能力 //interface{} 空接口,没有任何方法 type SayHello interface{ //只声明,没有实现的方法 sayHello() } //接口的实现:定义一个结构体 type Chinese struct{ } //实现接口的方法--具体的实现 func (ch Chinese) sayHello(){ fmt.Println("你好") } //接口的实现:定义一个结构体 type American struct{ } //实现接口的方法--具体的实现 func (am American) sayHello(){ fmt.Println("hello") } //定义一个函数:接收具备SayHello接口的能力的变量、 func greet(s SayHello){ s.sayHello() } func main(){ //定义一个多态数组--接口类型可以存储任何实现了该接口的变量 var arr [3]SayHello arr[0] = Chinese{} arr[1] = American{} arr[2] = Chinese{} //遍历数组 for i := 0;i < len(arr);i++{ greet(arr[i]) // 你好 hello 你好 } }
7、断言
- 在Go语言中,断言(Type Assertion)是一种用于检查接口变量的动态类型是否与指定类型相匹配的操作。它允许你将一个接口类型的变量转换为具体的类型,以便访问该类型的特定方法或字段。
- 断言的语法是
x.(T)
,其中x
是一个接口类型的变量,T
是你想要断言的具体类型。- 如果
x
的动态类型确实是T
,那么断言会成功,并且返回T
类型的值。如果断言失败,程序会抛出一个运行时错误。- 断言语法x.(T)的返回值 -> 变量,flag -> 变量用于接收T类型的值,flag用于判断类型是否转化成功 -> true--成功,false--失败。
- 类型断言通常用于确保在调用特定方法或访问特定字段之前,接口变量确实是你所期望的类型。这有助于避免在运行时出现类型不匹配的错误。
package main
import "fmt"
//接口的定义:定义规则,定义规范,定义某种能力
//interface{} 空接口,没有任何方法
type SayHello interface{
//只声明,没有实现的方法
sayHello()
}
//接口的实现:定义一个结构体
type Chinese struct{
}
//实现接口的方法--具体的实现
func (ch Chinese) sayHello(){
fmt.Println("你好")
}
func (ch Chinese) gongxi(){
fmt.Println("恭喜发财")
}
//接口的实现:定义一个结构体
type American struct{
}
//实现接口的方法--具体的实现
func (am American) sayHello(){
fmt.Println("hello")
}
//定义一个函数:接收具备SayHello接口的能力的变量、
func greet(s SayHello){
s.sayHello()
ch,flag := s.(Chinese)// 类型断言:看s是否能转成Chinese类型并赋给ch变量
//就是把结构体转换成能实现当前接口的结构体,如果能转成,就赋值给ch,不能转成,就报错
if flag==true { //或者是 if ch,flag := s.(Chinese); flag{
ch.gongxi()
}else{
fmt.Println("类型断言失败,美国人不会恭喜发财")
}
}
func main(){
var ch Chinese = Chinese{}
var am American = American{}
greet(ch) // 你好 恭喜发财
greet(am) // hello 类型断言失败,美国人不会恭喜发财
}
Type Switch的基本用法
Type Switch是Go语言中一种特殊的switch语法,它比较的是类型而不是具体的值。它判断某个接口变量的类型,然后根据具体类型再做相应处理。
package main import "fmt" //接口的定义:定义规则,定义规范,定义某种能力 //interface{} 空接口,没有任何方法 type SayHello interface{ //只声明,没有实现的方法 sayHello() } //接口的实现:定义一个结构体 type Chinese struct{ } //实现接口的方法--具体的实现 func (ch Chinese) sayHello(){ fmt.Println("你好") } func (ch Chinese) gongxi(){ fmt.Println("恭喜发财") } //接口的实现:定义一个结构体 type American struct{ } //实现接口的方法--具体的实现 func (am American) sayHello(){ fmt.Println("hello") } //定义一个函数:接收具备SayHello接口的能力的变量、 func greet(s SayHello){ s.sayHello() //使用Type-Switch switch tp := s.(type) { //type属于go中的一个关键字,固定写法 case Chinese: tp.gongxi() case American: tp.gun() default: fmt.Println("类型断言失败") } } func (am American) gun(){ fmt.Println("gun之下出真知()") } func main(){ var ch Chinese = Chinese{} var am American = American{} greet(ch) // 你好 恭喜发财 greet(am) // hello gun之下出真知() }