Go语言面向对象编程
众所周知,面向对象编程(Object-Oriented Programming,OOP)是一种常用的编程范式,它以对象为核心,通过封装、继承和多态等概念来组织和管理代码,提供了一种结构化和模块化的方法,能够更好地组织和复用代码。
- 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式
- 继承:子类具备父类的属性和方法并且可以重新定义、修改、追加方法和属性
- 多态:不同对象中同种行为的不同实现方式
Go语言虽然没有类(class)的概念,但Go语言同样支持面向对象编程。
一、封装
Go语言中通过结构体对属性进行封装,相当于类。如下例中Student结构体类型有两个字段Name和Gender,相当于于类属性。
type Student struct { Name string Gender string }
Go语言中的方法在声明形式上仅仅比函数多了一个参数——receiver参数。receiver参数是方法于类型之间的纽带。Go语言方法的一般声明方法如下。
func (receiver T/*T) MethodName(参数列表) (返回值列表) { // 方法体 }
T或者*T被称为receiver的基类型,通过receiver,方法被绑定到类型T上。即该方法是类型T的一个方法,可以通过T或者*T的实例调用该方法。(不能为原生类型如map、int、float64等定义方法)
下面为Student结构体定义一个Study方法:
func (St *Student) Study(subject string) { fmt.Printf("%s can study %s well by reading and practicing\n", St.Name, subject) }
在面向对象编程中,还涉及类的属性访问权限,即类的属性是公共的还是私有的。在Go语言中,通过首字母大小写来控制访问权限:如果定义的常量、变量、类型、接口、结构、函数、方法等名称首字母是大写,则表示他们能被其他包访问或调用;反之如果首字母是小写,则只能在包内使用。
如Student结构体中添加age字段,则age不能被其他包直接访问,只能在包内使用。
type Student struct { Name string Gender string age int }
注:大家可能会想,如果非要在其他包中访问Student结构体中age字段,有没有办法?
同样,Go语言也可以获取和设置属性:
- 设置方法使用SetXxx
- 获取方法使用GetXxx
type Student struct { Name string Gender string age int } // SetName 设置属性 func (St *Student) SetName(name string) { St.Name = name } // GetName 获取属性 func (St *Student) GetName() string { return St.Name }
至此,上边”如何在包外访问私有成员“就有了方法:给结构体绑定设置/获取隐藏成员属性的方法或给结构体绑定函数,并将方法对外暴露(即首字母大写,公共可导出)。如下例中student结构体和结构体中的age字段都是首字母小写,即不可导出。
type student struct { age int } // GetAge 绑定方法设置成员属性 func (st *student) GetAge() int { return st.age } // SetAge 绑定方法设置成员属性 func (st *student) SetAge(newAge int) { st.age = newAge } // NewStudent 定义函数返回student实例的指针 func NewStudent() *student { st := new(student) return st }
如此,student及其成员属性均可以在包外被访问获取(因为其绑定的函数/方法首字母大写,是可导出的),同时student结构体及其成员属性完全隐藏。但也存在一个问题,就是SetXxx设置成员属性方法不是并发安全的,可以使用sync包或者channel同步来避免出现多线程不安全的问题。
在测试场景中,可以在上述例子代码所在的包中使用export_test.go来将age字段暴露出来,此处不做详细分解。
二、继承
Go语言采用组合来实现继承,具体的方式就是利用类型嵌入(type embedding)。
1.在接口类型中嵌入接口类型
import ( "fmt" "reflect" ) // 定义两个基础接口类型 type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error } // 通过嵌入上面的基础接口类型形成新的接口类型 type ReadCloser interface { Reader Closer } func main() { var r ReadCloser v := reflect.TypeOf(&r) elemType := v.Elem() n := elemType.NumMethod() for i := 0; i < n; i++ { fmt.Println("r's method: ", elemType.Method(i).Name) } } // 运行代码 // r's method: Close // r's method: Read
由输出结果看出,通过嵌入其他接口类型创建的新接口类型的方法集合包含了被嵌入接口类型的方法集合(在Go 1.14之前被嵌入接口类型的方法集合不能有交集,即被嵌入的多个接口类型中方法不能同名)。
2.在结构体类型中嵌入接口类型
import ( "fmt" "reflect" ) // 定义一个基础接口类型 type Reader interface { Read(p []byte) (n int, err error) } // 通过嵌入上面的基础接口类型形成新的结构体类型 type ReadCloser struct { err error Reader // 包含Reader接口类型的匿名字段 } func (rd ReadCloser) Close() error { return nil } func main() { var r ReadCloser v := reflect.TypeOf(&r) elemType := v.Elem() n := elemType.NumMethod() for i := 0; i < n; i++ { fmt.Println("r's method: ", elemType.Method(i).Name) } } // 运行代码 r's method: Close r's method: Read
通过例子可以看到,在结构体类型中嵌入接口类型,结构体类型的方法集合包含了被嵌入接口类型的方法集合。结构体类型嵌入了某接口类型时,结构体也实现了这个接口。需要注意的是,如果在结构体类型中嵌入多个接口类型且接口类型的方法集合存在交集,Go编译器将报错,除非结构体类型自身实现了交集中的所有方法。
3.在结构体类型中嵌入结构体类型
通过在结构体类型中嵌入结构体类型,外部结构体可以”继承“嵌入结构体类型的所有方法的实现。无论是外部结构体类型的实例还是外部结构体指针类型实例,都可以调用所有”继承“的方法。
import ( "fmt" "reflect" ) type Reader struct{} func (r Reader) Read() {} type Closer struct{} func (c Closer) Close() {} type ReadClose struct { Reader *Closer } func main() { var r *ReadClose v := reflect.TypeOf(&r) elemType := v.Elem() n := elemType.NumMethod() for i := 0; i < n; i++ { fmt.Println("r's method: ", elemType.Method(i).Name) } } // 运行代码 // r's method: Close // r's method: Read
三、多态
Go语言使用接口来实现多态特性。
假如要计算圆形和正方形的面积,因为圆形和正方形面积计算公式不同,所以要实现两个Area()方法。于是可以定义一个包含Area()方法的接口,让圆形和正方形都能调用该接口的方法。
package main import ( "fmt" "math" ) type Geometry interface { Area() float64 } type Square struct { SideLength float64 } func (s *Square) Area() float64 { return s.SideLength * s.SideLength } type Circle struct { Radius float64 } func (c *Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } func main() { var s, c Geometry s = &Square{2} c = &Circle{2} fmt.Println(s.Area()) fmt.Println(c.Area()) }
Go语言中,如果某个自定义类型T的方法集合是某个接口类型方法集合的超集,那么该类型就实现了这个接口,并且类型T的变量可以被赋值给该接口类型。