Go语言之面向对象编程(三)
Golang也拥有面向对象编程的封装、继承、多态特性。
一、封装
封装就是将抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的方法才能对字段进行操作。那么如何来实现封装呢?
- 将结构体、首字母小写(这样就是私有变量,只能在本包使用)
- 在结构体所在的包中提供一个工厂模式的函数,首字母大写,这时一个对外公开的函数
比如,对私有属性赋值,可以提供一个SetAttr属性工厂函数,在内部可以进行业务验证,进行属性赋值;获取属性的值,可以提供一个GetAttr属性工厂函数。
案例:员工包括姓名、年龄、薪资,其中薪资、年龄对外是隐私的,年龄大于等于18岁。
分析:薪资、年龄通过工厂函数进行赋值,并且年龄赋值时进行判断。
- model/person.go
package model import "fmt" type person struct { Name string age int salary int } // 结构体时小写变量名,提供工厂方法 func CreatePerson(name string) *person { return &person{ Name: name, } } // 提供年龄赋值工厂方法 func (p *person) SetAge(age int) { if age >= 18 { p.age = age } else { fmt.Println("年龄输入不合法") } } // 提供获取年龄的方法 func (p *person) GetAge() int { return p.age } // 提供薪资赋值工厂方法 func (p *person) SetSalary(salary int) { p.salary = salary } // 提供获取薪资的方法 func (p *person) GetSalary() int { return p.salary }
- main/main.go
package main import ( "fmt" "go_tutorial/day12/example/02/model" ) func main() { // 创建一个person的结构体变量 p := model.CreatePerson("ariry") p.SetAge(30) p.SetSalary(5000) fmt.Println("姓名:", p.Name, "年龄:", p.GetAge(), "薪资:", p.GetSalary()) }
可以看到person结构体、字段age、salary都是私有的,通过提供工厂方法进行创建、赋值、获取值操作。
二、继承
(一)基础
1、快速上手
继承主要解决的就时代码复用问题,不至于出现过多的代码冗余问题。当多个结构体存在相同的属性、方法时,可以从这些结构体中抽象出一个拥有共同属性、方法的结构体。其余结构体可以通过匿名结构体的方式来继承这个结构体即可。
案例:假如学生分为中学生、大学生,这些学生都有姓名、年龄、考试成绩,平时需要进行各种考试。不过大学生平日里空闲时间比较多,所以去图书馆看书。所以这里面可以抽象出:
- 属性有姓名、年龄、成绩
- 方法有考试
- 另外大学生额外的方法有看书
package main import "fmt" type Student struct { Name string Age int Score float64 } func (student *Student) Examing() { fmt.Print("姓名:", student.Name, "年龄:", student.Age, "考试成绩:", student.Score) } type MiddleStudent struct { Student // 学生匿名结构体 } type UgStudent struct { Student // 学生匿名结构体 } // 大学生特有读书方法 func (ugs *UgStudent) ReadBook() { fmt.Print(ugs.Name, "正在读书...") } func main() { // MiddleStudent继承Student,所以可以通过mt.Student.Name来进行属性的调用, // 同时也可以mt.Name,如果在本结构体中没找到该属性,会去匿名结构体中查找 mt := MiddleStudent{} mt.Student.Name = "中学生" //mt.Name = "中学生" mt.Student.Age = 15 //mt.Age = 15 mt.Student.Score = 85.5 //mt.Score = 85.5 mt.Examing() // 调用大学生特有的方法 ugs := UgStudent{} ugs.Name = "大学生" ugs.Age = 25 ugs.Score = 95.5 ugs.ReadBook() }
2、深入理解
- 结构体可以使用匿名结构体中的所有字段和方法,包括首字母小写的结构体、属性、方法等私有变量
- 匿名结构体字段访问可简化(mt.Student.Name可简化为mt.Name)
- 当结构体于匿名结构体有相同字段和方法时,采用就近原则,如若希望访问匿名结构体字段需要指明匿名结构体
- 如若结构体有两个或者多个匿名结构体时,并且匿名结构体都拥有相同字段和方法,但是结构体本身无同名字段和方法时,需指明匿名结构体,否则编译报错
package main import "fmt" type animal struct { name string hobby string } func (a *animal) showHobby() { fmt.Println("hobby:", a.hobby) } type cat struct { animal name string } func (c *cat) showHobby() { fmt.Println("hobby:", c.hobby) } type tigger struct { animal cat } func main() { /* 1、结构体可以使用匿名结构体中的所有字段和方法,包括首字母小写的结构体、属性、方法等私有变量 2、匿名结构体字段访问可简化 3、当结构体于匿名结构体有相同字段和方法时,采用就近原则,如若希望访问匿名结构体字段需要指明匿名结构体 */ // 声明一个cat结构体变量 var c cat c.name = "猫" // 就近原则,使用的name时cat结构体中的name,而非animal中的name c.hobby = "吃鱼..." // 匿名结构体可简写,查找顺序:本结构体-->各个匿名结构体-->如若没有报错 c.showHobby() /* 4、如若结构体有两个或者多个匿名结构体时,并且匿名结构体都拥有相同字段和方法,但是结构体本身无同名字段和方法时,需指明匿名结构体,否则编译报错 */ // 声明一个tigger结构体 t := tigger{} t.animal.name = "老虎..." // 需要指明具体的匿名结构体 t.animal.hobby = "吃小动物..." // 需要指明具体的匿名结构体 t.animal.showHobby() }
(二)进阶
1、组合
如果一个结构体嵌套了一个有名结构体,这种模式就是组合,如果时组合关系,那么在访问组合结构体的字段、方法时,必须带上结构体的名字。
package main type Student struct { Name string Age int } type MiddleStudent struct { s Student // 有名结构体,组合关系 } func main() { var ms MiddleStudent // 必须带上有名结构体的名字s ms.s.Name = "Alice" ms.s.Age = 25 }
2、匿名结构体初始化值
嵌套匿名结构体后,也可以在创建结构体变量时,直接指定各个匿名结构体字段的值。
package main import "fmt" type Student struct { Name string Age int } type MiddleStudent struct { Student } type UgStudent struct { *Student } func main() { // 直接给匿名结构体初始化赋值 ms := MiddleStudent{ Student{Name: "kity", Age: 21}, } ugs := UgStudent{ &Student{Name: "jack", Age: 25}, } fmt.Print(ms) fmt.Print(*ugs.Student) }
3、基本数据类型的匿名字段
结构体的匿名字段时基本数据类型,又该如何访问呢?
package main import "fmt" type Student struct { username string age int int // 匿名字段(基本数据类型) } func main() { // 匿名字段 s := Student{} s.username = "peter" s.age = 20 s.int = 85 fmt.Println(s) }
注意:如果一个结构体有int类型的匿名字段,就不能有第二个;如果需要有多个int的字段,则必须给int字段指定名称。
4、多继承
一个结构体嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现多继承。
package main import "fmt" type A struct { x1 string x2 int } type B struct { x1 float64 } type C struct { A B } func main() { c := C{} // 必须指明匿名结构体的 c.A.x1 = "abc" fmt.Println(c) }
如果结构体嵌套多个匿名结构体,并且拥有相同的字段,而本结构体又没有该字段,则进行属性操作时必须指明匿名结构体。
三、多态
多态的特性主要是通过接口来体现的,所以需要先了解清楚接口相关知识。
(一)接口基础
接口(interface)类型就是定义一组方法,但是这些方法都不需要实现。并且interface不能包含任何任何变量,如果某个变量要使用interface时,再去实现具体的方法。
基本语法:
type 接口名称 interface { method1(参数列表) 返回值列表 method2(参数列表)返回值列表 ... }
接口中的所有方法都是没有方法体,即都是没有实现的方法。只要一个变量事项了接口类型中的所有方法,那么这个变量就实现了这个接口。
案例:
我们电脑上有USB接口,这个接口可以插入多种设备,比如相机、手机、硬盘等。如果插入的的是硬盘那么就会读取硬盘空间的操作,如果是手机,那么就会执行手机对应的操作等。
package main import "fmt" // 定义一个接口 type Usb interface { // 声明两个未实现的方法 start() stop() } // 声明一个Phone的结构体 type Phone struct { } // Phone结构体变量接口实现方法 func (phone Phone) start() { fmt.Println("手机开始工作...") } func (phone Phone) stop() { fmt.Println("手机结束工作...") } // 声明一个Camera的结构体 type Camera struct { } // Camera结构体变量接口实现方法 func (camera Camera) start() { fmt.Println("相机开始工作...") } func (camera Camera) stop() { fmt.Println("相机结束工作...") } // 声明一个Computer结构体 type Computer struct { } // usb会自动根据传递过来的来判断是phone还是camera然后调用对应的结构体变量中实现的方法 func (computer Computer) use(usb Usb) { usb.start() usb.stop() } func main() { // 创建结构体变量 computer := Computer{} phone := Phone{} camera := Camera{} computer.use(phone) computer.use(camera) } /* 手机开始工作... 手机结束工作... 相机开始工作... 相机结束工作... */
(二)深入理解接口
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义数据类型的实例
- 空接口interface{}没有任何方法,所有类型都实现了空接口,即可以把任何一个类型的变量赋值给空接口
- 接口中所有的方法都没有方法体,即都是没有实现的方法
- Golang中,一个自定义类型将接口中所有的方法都实现了,才能说这个自定义类型实现了该接口
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
- 一个自定义类型可以实现多个接口
- Golang接口中不能存在任何变量
- 一个接口(A接口)可以继承多个其它接口(B、C接口),此时如果要实现A接口,则必须也实现B、C接口中的全部方法
- 接口类型是一个指针(引用类型),如果没有对接口初始化就使用,就会输出nil
package main import "fmt" // 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义数据类型的实例 type A interface { m1() } type S1 struct { } func (s1 S1) m1() { // 实现A接口中的m1方法 } func main() { s1 := S1{} var a1 A = s1 fmt.Println(a1) }
package main import "fmt" // 空接口interface{}没有任何方法,所有类型都实现了空接口,即可以把任何一个类型的变量赋值给空接口 type A interface { } func main() { var x int var i A = x fmt.Println(i) }
package main import "fmt" // 一个自定义类型可以实现多个接口 type A interface { a1() } type B interface { b1() } type integer int func (i integer) a1() { } func (i integer) b1() { } func main() { var i integer = 10 var a A = i var b B = i fmt.Println(a, b) }
package main import "fmt" // Golang中,一个自定义类型将接口中所有的方法都实现了,才能说这个自定义类型实现了该接口 type Animal interface { eat() } type Cat struct { } /* 这里如果写成这样,就是错误的,因为是*Cat指针类型实现了接口,而非Cat类型实现接口 func (c *Cat) eat() { } */ func (c Cat) eat() { } func main() { c := Cat{} var a Animal = c fmt.Println(a) }
(三)接口最佳实践
1、引入
如果对一个切片中都是int类型的数组或者切片进行排序,想必是一件比较简单的事情,比如可以通过golang中sort包Ints进行排序。
package main import ( "fmt" "sort" ) func main() { // 声明一个元素是int类型的切片 var intSlice = []int{2, 1, 10, 5} // 对切片进行排序 sort.Ints(intSlice) // 打印排序好的切片 fmt.Println(intSlice) }
但是如果实现对结构体根据某个字段进行排序,又当如何呢,如下面:
type UserInfo struct { Username string Age int }
将多个用户信息通过年龄进行排序。
2、结构体排序
此时可以通过sort包中Sort方法来实现:
func Sort(data Interface)
Sort方法的参数接收一个接口类型的参数data,只要自定义的数据类型实现了data接口类型的中的方法就可以传入,那么实现哎什么方法呢?
type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with index i // must sort before the element with index j. // // If both Less(i, j) and Less(j, i) are false, // then the elements at index i and j are considered equal. // Sort may place equal elements in any order in the final result, // while Stable preserves the original input order of equal elements. // // Less must describe a transitive ordering: // - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well. // - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well. // // Note that floating-point comparison (the < operator on float32 or float64 values) // is not a transitive ordering when not-a-number (NaN) values are involved. // See Float64Slice.Less for a correct implementation for floating-point values. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) }
即:Len、less、Swap三个方法即可,所以将UserInfo切片类型实现这三个方法,然后传入到 Sort方法即可。
package main import ( "fmt" "math/rand" "sort" ) // 声明一个UserInfo的结构体 type UserInfo struct { UserName string Age int } // 声明一个UserInfo的切片 type UserInfoSlice []UserInfo // 实现interface Len Less Swap func (uis UserInfoSlice) Len() int { // 返回切片的长度 return len(uis) } func (uis UserInfoSlice) Less(i, j int) bool { // 按照什么样的顺序进行排序 return uis[i].Age > uis[j].Age } func (uis UserInfoSlice) Swap(i, j int) { // 交换,更简单的交换方式 uis[i], uis[j] = uis[j], uis[i] temp := uis[i] uis[i] = uis[j] uis[j] = temp } func main() { // 随机生成不同年龄的UserInfo切片 var uis UserInfoSlice for i := 0; i < 20; i++ { ui := UserInfo{ UserName: fmt.Sprintf("NO~%d", rand.Intn(50)), Age: rand.Intn(100), } // 将ui加入到uis中 uis = append(uis, ui) } // 排序前顺序 for _, v := range uis { fmt.Println(v) } // 调用sort.Sort sort.Sort(uis) // 排序后顺序 fmt.Println() for _, v := range uis { fmt.Println(v) } }
(四)接口VS继承
接口于继承之间有什么区别呢?
不同点在于体现的价值不同,继承着重解决代码的复用问题,接口着重解决代码的设计和规范;联系是接口是对继承的一种扩展,当继承这些功能的同时,又希望不破环关系时,可以通过接口的方式来补充功能。
例如:猴子这种动物天性就是会爬树,从父辈那里继承过来的本领,但是小猴子不甘如此,希望会游泳,于是通过后天的不断努力终于学会了。这个例子中小猴子继承的就是父辈会爬树的本领,但是这个个例它还会游泳,如果将游泳加入到父辈哪里显然你不合理。
package main import "fmt" // 声明一个Monkey的结构体 type Monkey struct { Name string } // Monkey结构体天生拥有climbing方法 func (m *Monkey) Climbing() { fmt.Printf("%s爬树...", m.Name) } // 声明一个LittleMonkey结构体 type LittleMonkey struct { Monkey } // 声明一个接口 type OtherSkills interface { Swimming() } // LittleMonkey通过后天不断努力实现了OtherSkills func (lk *LittleMonkey) Swimming() { fmt.Printf("%s学会了游泳...", lk.Name) } func main() { // 创建一个LittleMonkey的结构体变量 lk := LittleMonkey{} lk.Name = "小猴子" // 调用天生的Climbing方法 lk.Climbing() // 通过后天的努力实现了OtherSkills,学会了Swimming lk.Swimming() }
那么这个例子就体现出了接口是对继承功能的一种扩展,可能有人有疑问了,为什么不将Swimming方法直接写在LittleMonkey结构体变量中呢?实际上要这样讲的话也可以说通,但是写代码还是需要注重规范性和灵活性,使用接口不是显得更好吗?那个特殊个例实现了这个OtherSkills ,就拥有里面的本领。
(五)多态
多态就是通过接口来实现的,统一的接口,不同的表现,此时接口变量体现不同的形态。比如usb Usb接口变量,既可以接收手机变量,又可以接收相机变量,体现了Usb接口的多态特性。
接口体现多态的两种形式:
- 多态参数
- 多态数组
1、多态参数
参考之前Usb接口案例,既可以接收手机变量,又可以接收相机变量,体现了Usb接口的多态特性。
2、多态数组
package main import "fmt" // 定义一个接口 type Usb interface { // 声明两个未实现的方法 start() stop() } // 声明一个Phone的结构体 type Phone struct { Name string } // Phone结构体变量接口实现方法 func (phone Phone) start() { fmt.Println("手机开始工作...") } func (phone Phone) stop() { fmt.Println("手机结束工作...") } // 声明一个Camera的结构体 type Camera struct { Name string } // Camera结构体变量接口实现方法 func (camera Camera) start() { fmt.Println("相机开始工作...") } func (camera Camera) stop() { fmt.Println("相机结束工作...") } func main() { // 创建Usb结构体数组变量 var usbArr [3]Usb usbArr[0] = Phone{"iphone11"} usbArr[1] = Phone{"xiaomi"} usbArr[2] = Camera{"Canon"} fmt.Println(usbArr) } /* [{iphone11} {xiaomi} {Canon}] */
可以看到将实现了接口的不同数据类型放到一个数组中,数组本身应该是放一种数据类型的,但是这里就体现了多态特性。