引言
l. Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang支持面向对象编程特性是比较准确的。
2. Golang没有类(class),Go语言的结构体( struct)和其它编程语言的类class有同等的地位,你可以理解 Golang是基于 struct来实现OOP特性的。
3. Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
4. Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不样,比如继承: Golang没有 extends关键字,继承是通过匿名字段来实现。
5. Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统( type system)的一部分,通过接口( interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang中面向接口编程是非常重要的特性。
类型系统
类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容:
- 基础类型:int,bool,float等
- 复合类型,如数组、结构体、指针等
- 值语义和引用语义
- 面向对象,即所有具备面向对象特征(比如成员方法)的类型
- 接口
值语义和引用语义
- 值语义:值类型
- 引用语义:引用类型
结构体
基本语法
type 结构体名称 struct {
field1 type
field2 type
}
type Cat struct {
Name string
Age int
Color string
Hobby string
}
// 创建结构体变量的方式
var cat Cat
var cat Cat = Cat{"中分", 3, "黑白", "吃鱼"} // 必须字段顺序对应
var cat Cat = Cat{Name : "中分", Age : 3, Color : "黑白", Hobby : "吃鱼"} // 字段顺序可以不对应
var cat *Cat = new(Cat) // 返回的结构体指针
var cat *Cat = &Cat{} // 返回结构体指针
// 结构体指针的调用方式
(*cat).Name = "杜甫" <-----> cat.Name = "杜甫" // 两个等价,因为go设计者为了程序员使用方便,底层会对cat.Name进行处理,加上*,这是一个语法糖
- 结构体是自定义的数据类型,代表一类事物
- 结构体变量是具体的,实际地,代表一个具体变量
- 结构体的所有字段在内存中的分布是连续的
type Point struct {
x int
y int
}
type Rect1 struct { // x, y四个int全部连续
leftUp, rightDown Point
}
type Rect2 struct { // 指针变量内存连续
leftUp, rightDown *Point
}
- 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型
- 结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段
- 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
- struct的每个字段上,可以写上一个
tag
,该tag
可以通过反射机制获取,常见的使用场景就是序列化和反序列化
方法
方法:即结构体的行为
Golang 中的方法是作用在指定的数据类型上的,因此自定义类型,都可以有方法,而不仅仅是struct
// 声明
func (recevier type) methodName (参数列表) (返回值列表) {
方法体
return 返回值 // 不是必须的
}
type A struct {
Num int
}
func (a A) test0(){ // a 是副本
...
}
func (a *A) test1(){ // a 是结构体指针,结构体变量调用的时候,会传递地址给a
...
}
- 方法只能通过结构体变量来调用
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如果希望能在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang中的方法作用在指定的数据类型上的(即:和指定数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其他包访问
- 如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
方法与函数的区别
-
调用方式不一样
- 函数的调用方式:函数名(实参列表)
- 方法的调用方式:变量.方法名(实参列表)
-
对应普通函数,接收者为值类型,不能将指针类型的数据直接传递
工厂模式(构造函数)
如果结构体的首字母是大写的,那么我们可以在其他包访问,并且创建它的变量,但是如果结构体首字母是小写,其他包访问不了,就需要用工厂模式来创建变量
type student struct {
Name string
Age int
}
func NewStudent(name string, age int) *student {
return &student{
name,
age,
}
}
Getter && Setter
如果结构体的字段是小写的,则其他包就不能正常访问,应该借助Getter && Setter
type student struct {
name string
age int
}
func (stu *student) GetName() string {
return stu.name
}
func (stu *student) SetName(name string) {
stu.name = name
}
func (stu *student) GetAge() int {
return stu.age
}
func (stu *student) SetAge(age int) {
stu.age = age
}
抽象
将一个实物,抽象出其特征和行为,形成结构体,这个过程就是抽象
面向对象编程的三大特性
- 封装
- 继承
- 多态
封装
封装就是把抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其他包只有通过被授权的操作(方法),才能对字段进行操作
封装的理解和好处
- 隐藏实现细节
- 可以对数据进行雁阵个,保证安全合理
如何体现封装
- 对结构体中的属性进行封装
- 通过方法,包实现封装
封装的实现步骤
- 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
- 提供一个首字母大写的Set方法(类似其它语言的 public,用于对属性判断并赋值
继承
当多个结构体存在相同的数字那个和方法时,可以从这些结构体中抽向出结构体,在该结构体中定义这些相同的属性和方法,可以成为父结构体
其他的结构体不需要重新定义这些属性和方法,只需嵌套一个父结构体的匿名结构体即可
在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性
type Goods struct {
Name string
Price int
}
type Book struct {
Goods
Writer string
}
- 结构体可以使用嵌套匿名结构体所有字段和方法,即:首字母大写或者小写的字段、方法都可以使用
- 匿名结构体字段可以简化
- 当结构体和匿名子结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如果希望访问匿名结构体的字段和方法,可以通过匿名结构体来区分
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和犯法),在访问时,就必须明确自定匿名结构体名字,否则编译报错
- 如果一个struct嵌套了一个有名结构体,这种模式就是组合。如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
// 含匿名结构体的初始化
type Address struct {
province string
city string
}
type User struct {
name string
age int
Address
}
// 方法一:正常直观方式定义
u1 := &User{
name: "Ming",
age: 30,
Address: Address{
province: "Jiangsu",
city: "Nanjing",
},
}
fmt.Printf("%+v
", u1) // &{name:Ming age:30 Address:{province:Jiangsu city:Nanjing}}
// 同上
var u2 User
u2.name = "Qiang"
u2.age = 35
u2.Address = Address{province: "Jiangsu", city: "Suzhou"}
fmt.Printf("%+v
", u2) // {name:Qiang age:35 Address:{province:Jiangsu city:Suzhou}}
// 方法二:匿名嵌入时可以直接访问叶子属性而不需要给出完整的路径,也可以给出完整路径
var u3 User
u3.name = "A"
u3.age = 40
u3.province = "Jiangsu"
u3.city = "Wuxi"
fmt.Printf("%+v
", u3) // {name:A age:40 Address:{province:Jiangsu city:Wuxi}}
// 但下面的方式是错误的,编译不能通过
// cannot use promoted field Address.province in struct literal of type User
// cannot use promoted field Address.city in struct literal of type User
u4 := User{
name: "A",
age: 29,
province: "Jiangsu",
city: "Wuxi",
}
fmt.Printf("%+v
", u4)
- 结构体内不仅仅可以嵌套结构体,还可以嵌套int,float64等
// 如果一个结构体有int类型的匿名字段,就不能有第二个
// 如果需要有多个int类型字段,则必须给int字段指定名字
type M struct {
Name string
}
type E struct {
M
int
n int
}
func main() {
var e E
e.Name = "狐狸精"
e.int = 20
e.n = 40
fmt.Println(e)
}
- 以指针方式从一个类型“派生”
这段Go代码仍然有“派生”的效果,只是Foo创建实例的时候,需要外部提供一个Base类实例的指针。
type Foo struct {
*Base
...
}
多重继承
如果一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承
type M struct {
Name string
}
type N struct {
ability string
}
type E struct {
M
N
}
接口
讲多态之前先讲接口,因为Golang中的多态一般体现在接口的实现上
interface类型可以定义一组方法,但是这些不需要实现,并且interface不能包含任何变量。到某一个自定义类型要使用的时候,在根据具体情况把这些方法实现
- 基本语法
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想
- Golang中的接口,不需要显示的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有implement这样的关键字
type interfaceName interface {
method1(参数列表) 返回值列表
method2(参数列表) 返回值列表
...
}
func (t 自定义类型) method1(参数列表) 返回值列表{}
func (t 自定义类型) method2(参数列表) 返回值列表{}
接口注意事项与细节
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
type AInterface interface {
Say()
}
type Stu struct {
Name string
}
func (stu Stu) Say() {
fmt.Println("Stu Say(")
}
func main() {
var stu Stu
var a AInterface = stu
a.Say()
}
-
接口中所有的方法都没有方法体,即都是没有实现的方法
-
在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口
type AInterface interface {
Say()
Call()
}
type Stu struct {
Name string
}
func (stu Stu) Say() {
fmt.Println("Stu Say(")
}
func main() {
var stu Stu
var a AInterface = stu // 报错
a.Say()
}
-
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
-
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
type AInterface interface {
Say()
}
type intteger int
func (i intteger) Say() {
fmt.Println("integer Say i = ", i)
}
-
一个自定义类型可以实现多个接口
-
Golang接口中不能有任何变量
-
一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现
type A interface {
test01()
}
type B interface {
test02()
}
type C interface {
A
B
test03()
}
type Stu struct {
}
func (stu Stu) test01() {
}
func (stu Stu) test02() {
}
func (stu Stu) test03() {
}
func main() {
var stu Stu
var a A = stu
a.test01()
}
-
interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
-
空接口 interface{} 没有任何方法,所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口
-
结构体指针绑定接口
type A interface {
test01()
}
type AI struct {
}
func (this *AI) test01() {
fmt.Println("test01")
}
func main() {
var a A = &AI{} // 如果这里没有&,则会报错
a.test01()
fmt.Println(a)
}
-
侵入式接口和非侵入式接口的区别
- 侵入式接口:主要表现在于实现类需要明确声明自己实现了某个接口。向Java、C++
- 非侵入式接口:不需要显示的声明自己实现了哪个接口
-
接口赋值讨论:将对象实例赋值给接口
假设我们定义一个Integer类型的对象实例,代码如下,怎么将其赋值给LessAdder接口呢?应该用下面的语句(1),还是语句(2)呢?
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a * Integer) Add(b Integer){
*a += b
}
func main() {
var a Integer = 1
var b LessAdder = &a ... (1)
var c LessAdder = a ... (2)
}
答案是应该用语句(1)。
原因在于, Go语言可以根据下面的函数:func (a Integer) Less(b Integer) bool
自动生成一个新的Less()方法:
func (a *Integer) Less(b Integer) bool {
return (*a).Less(b)
}
类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口。而从另一方面来说,根据func (a *Integer) Add(b Integer)
这个函数无法自动生成以下这个成员方法:
func (a Integer) Add(b Integer) {
(&a).Add(b)
}
因为(&a).Add()改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用户的预期。所以, Go语言不会自动为其生成该函数。因此,类型Integer只存在Less()方法,缺少Add()方法,不满足LessAdder接口,故此上面的语句(2)不能赋值。
- 接口赋值讨论:接口赋值给另一个接口
在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等同的,可以相互赋值。
package one
type ReadWriter interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
}
/////////////////
package two
type IStream interface {
Write(buf []byte) (n int, err error)
Read(buf []byte) (n int, err error)
}
// 下面代码都通过
var file1 two.IStream = new(File)
var file2 one.ReadWriter = file1
var file3 two.IStream = file2
接口赋值并不要求两个接口必须等价。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A。
type Writer interface {
Write(buf []byte) (n int, err error)
}
就可以将上面的one.ReadWriter和two.IStream接口的实例赋值给Writer接口:
var file1 two.IStream = new(File)
var file4 Writer = file1
但是反过来并不成立:
var file1 Writer = new(File)
var file5 two.IStream = file1 // 编译不能通过
这段代码无法编译通过,原因是显然的: file1并没有Read()方法。
- 类型断言: 接口查询
var file1 Writer = ...
if file5, ok := file1.(two.IStream); ok {
...
}
- 类型断言:类型查询
在Go语言中,还可以更加直截了当地询问接口指向的对象实例的类型,例如:
var v1 interface{} = ...
switch v := v1.(type) {
case int: // 现在v的类型是int
case string: // 现在v的类型是string
...
}
对于内置类型, Println()采用穷举法,将每个类型转换为字符串进行打印。对于更一般的情况,首先确定该类型是否实现了String()方法,如果实现了,则用String()方法将其转换为字符串进行打印。
-
接口也支持继承
-
Any类型:
interface{}
,可以指向任何对象
多态
变量具有多种形态,面向对象的第三大特诊,在Go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。可以按照统一的接口来电泳不同的实现。这时接口变量就呈现不同的形态
- 接口体现多态的两种形式
- 多态参数:方法参数的体现
- 多态数组
- 接口数组中,存放实现接口的变量
类型断言
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
var x interface{}
var b2 float32 = 1.1
x = b2 // 空接口,可以接受任意类型
// x => float32 [使用类型断言]
y := x.(float32) // 这里如果不是float32就会panic
fmt.Printf("y 的类型是 %T 值是 %v", y, y)
代码说明:在进行类型断言时,如果类型不匹配,就会报 panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型
var x interface{}
var b2 float32 = 1.1
x = b2 // 空接口,可以接受任意类型
// x => float32 [使用类型断言]
if y, ok := x.(float32); ok { // 优雅的类型断言
fmt.Println("convert success")
fmt.Printf("y 的类型是 %T 值是 %v
", y, y)
} else {
fmt.Println("convert fail")
}
fmt.Println("继续执行....")