• Golang 面向对象深入理解


    1 封装

    Java 中封装是基于类(Class),Golang 中封装是基于结构体(struct)

    Golang 的开发中经常直接将成员变量设置为大写使用,当然这样使用并不符合面向对象封装的思想。

    Golang 没有构造函数,但有一些约定俗成的方式:

    1. 提供 NewStruct(s Struct) *Struct 这样的函数
    2. 提供 (s *Struct) New() 这样的方法
    3. 也可以直接用传统的 new(struct) 或 Struct{} 来初始化,随后用 Set 方法对成员变量赋值
    type People struct {
    	name string
    	age  int
    }
    
    func (p *People) SetName(name string) {
    	p.name = name
    }
    
    func (p *People) GetName() string {
    	return p.name
    }
    
    func (p *People) SetAge(age int) {
    	p.age = age
    }
    
    func (p *People) GetAge() int {
    	return p.age
    }
    
    func main() {
    	peo := new(People)
    	peo.SetName("张三")
    	peo.SetAge(13)
    	fmt.Println(peo.GetName(), peo.GetAge())
        // 张三 13
    }
    

    2 继承 or 组合

    Golang 不支持继承,支持组合,算是“组合优于继承”思想的体现。

    虽然不支持继承,但 Golang 的匿名组合组合可以实现面向对象继承的特性。

    2.1 非匿名组合和匿名组合

    组合分为非匿名组合匿名组合

    非匿名组合不能直接使用内嵌结构体的方法,需要通过内嵌结构体的变量名,间接调用内嵌结构体的方法。

    匿名组合可以直接使用内嵌结构体的方法,如果有多个内嵌结构体,可以直接使用所有内嵌结构体的方法。

    非匿名组合实例:

    type People struct {
    	name string
    	age  int
    }
    
    func (p *People) SetName(name string) {
    	p.name = name
    }
    
    func (p *People) GetName() string {
    	return p.name
    }
    
    func (p *People) SetAge(age int) {
    	p.age = age
    }
    
    func (p *People) GetAge() int {
    	return p.age
    }
    
    type Student struct {
    	people People
    	grade  string
    }
    
    func (s *Student) SetGrade(grade string) {
    	s.grade = grade
    }
    
    func (s *Student) GetGrade() string {
    	return s.grade
    }
    
    func main() {
    	stu := Student{}
    	stu.people.SetName("张三")
    	stu.people.SetAge(13)
    	stu.SetGrade("七年级")
    	fmt.Println(stu.people.GetName(), stu.people.GetAge(), stu.GetGrade())
    	// 张三 13 七年级
    }
    

    非匿名组合主要体现在 Student 结构体中对 People 的组合需要明确命名。

    匿名组合实例:

    type People struct {
    	name string
    	age  int
    }
    
    func (p *People) SetName(name string) {
    	p.name = name
    }
    
    func (p *People) GetName() string {
    	return p.name
    }
    
    func (p *People) SetAge(age int) {
    	p.age = age
    }
    
    func (p *People) GetAge() int {
    	return p.age
    }
    
    type Student struct {
    	People
    	grade  string
    }
    
    func (s *Student) SetGrade(grade string) {
    	s.grade = grade
    }
    
    func (s *Student) GetGrade() string {
    	return s.grade
    }
    
    func (s *Student) GetName() string {
    	return fmt.Sprintf("student-%s",s.People.GetName())
    }
    
    func main() {
    	stu := Student{}
    	stu.SetName("张三")
    	stu.SetAge(13)
    	stu.SetGrade("七年级")
    	fmt.Println(stu.GetName(), stu.GetAge(), stu.GetGrade())
    	// student-张三 13 七年级
    }
    

    从上面实例可以看出,匿名组合中:

    1. Student 中的 People 没有显式命名
    2. Student 可以直接使用 People 的方法
    3. 注意 StudentGetName 方法,与 People 中的 GetName 方法重复,可以看做是面相对象编程中的重写(Overriding)
    4. StudentGetName 方法中使用 s.People.GetName() 调用People 中的 GetName 方法,这是对匿名组合的显式调用,类似 Java 中的 super 用法

    可以看出,匿名组合的使用感官上类似面向对象编程的继承,可以说是一种『伪继承』的实现,但匿名组合并不是继承!

    2.2 组合的使用方式

    2.2.1 结构体中内嵌结构体

    上面实例所用的就是结构体中内嵌结构体,不再多说。

    2.2.2 结构体中内嵌接口

    内嵌接口如下面例子:

    type Member interface {
    	SayHello() // 问候语
    	DoWork()   // 开始工作
    	SitDown()  // 坐下
    }
    
    type Normal struct{}
    
    func (n *Normal) SayHello() {
    	fmt.Println("normal:", "大家好!")
    }
    
    func (n *Normal) DoWork() {
    	fmt.Println("normal:", "记笔记")
    }
    
    func (n *Normal) SitDown() {
    	fmt.Println("normal:", "坐下")
    }
    
    type People struct {
    	name string
    	age  int
    }
    
    func (p *People) SetName(name string) {
    	p.name = name
    }
    
    func (p *People) GetName() string {
    	return p.name
    }
    
    func (p *People) SetAge(age int) {
    	p.age = age
    }
    
    func (p *People) GetAge() int {
    	return p.age
    }
    
    type Student struct {
    	Member
    	People
    	grade string
    }
    
    func (s *Student) SetGrade(grade string) {
    	s.grade = grade
    }
    
    func (s *Student) GetGrade() string {
    	return s.grade
    }
    
    func (s *Student) SayHello() {
    	fmt.Println("student:", s.name, "说: 老师好!")
    }
    
    type Teacher struct {
    	Member
    	People
    	subject string
    }
    
    func (t *Teacher) SetSubject(subject string) {
    	t.subject = subject
    }
    
    func (t *Teacher) GetSubject() string {
    	return t.subject
    }
    
    func (t *Teacher) SayHello() {
    	fmt.Println("teacher", t.name, "说: 同学们好!")
    }
    
    func (t *Teacher) DoWork() {
    	fmt.Println("teacher", t.name, "讲课!")
    }
    
    func main() {
    	stu := &Student{Member: &Normal{}}
    	stu.SetName("张三")
    	stu.SetAge(13)
    	stu.SetGrade("七年级")
    
    	tea := &Teacher{Member: &Normal{}}
    	tea.SetName("李四")
    	tea.SetAge(31)
    	tea.SetSubject("语文")
    
    	var member Member
    	member = stu
    	member.SayHello()
    	member.SitDown()
    	member.DoWork()
    	// student: 张三 说: 老师好!
    	// normal: 坐下
    	// normal: 记笔记
    
    	member = tea
    	member.SayHello()
    	member.SitDown()
    	member.DoWork()
    	// teacher 李四 说: 同学们好!
    	// normal: 坐下
    	// teacher 李四 讲课!
    }
    

    从上面例子可以看出:

    1. TeacherStudent 结构体都没有完全实现 Member 的方法
    2. Normal 结构体实现了 Member 方法
    3. TeacherStudent 初始化时注入了 Normal 结构
    4. TeacherStudent 可以作为 Member 类型的接口使用,并默认使用 Normal 的实现。除非 TeacherStudent 有自己的实现。

    TeacherStudent 并非没有实现 Member 接口。编译器自动为类型 *Teacher*Student 实现了 Member 中定义的方法,类似:

    func (t *Teacher) SitDown() {  
        t.Member.SitDown()  
    }
    

    所以即使在初始化时没有指定 NormalTeacherStudent 也可以赋值给 Member 类型的变量。但调用未实现的 Member 的方法时会报 panic

    官方实践

    可以参考 sort.Reverse 方法,reverse 结构体内嵌了 Interface 接口,并只实现了 Less 方法

    type IntSlice []int
    
    func (x IntSlice) Len() int           { return len(x) }
    func (x IntSlice) Less(i, j int) bool { return x[i] < x[j] }
    func (x IntSlice) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
    
    type Interface interface {
        // 长度
    	Len() int
        // 对比两个数 i,j,返回结果作为排序的依据
    	Less(i, j int) bool
        // 交换两个数 i,j
    	Swap(i, j int)
    }
    
    type reverse struct {
    	Interface
    }
    
    func (r reverse) Less(i, j int) bool {
    	return r.Interface.Less(j, i)
    }
    
    func Reverse(data Interface) Interface {
    	return &reverse{data}
    }
    

    使用时:

    lst := []int{4, 5, 2, 8, 1, 9, 3}
    sort.Sort(sort.Reverse(sort.IntSlice(lst)))
    fmt.Println(lst)
    // 打印:[9 8 5 4 3 2 1]
    

    可以看出,Reverse 就是用内嵌接口的方式,接收一个 Interface 接口,将 Less 方法的两个参数反转了。

    2.2.3 接口中内嵌接口

    是针对方法的组合。如 Golang 的 ReadWriter 接口就是 Reader 和 Writer 接口的组合。

    type Reader interface {
    	Read(p []byte) (n int, err error)
    }
    
    type Writer interface {
    	Write(p []byte) (n int, err error)
    }
    
    // ReadWriter is the interface that groups the basic Read and Write methods.
    type ReadWriter interface {
    	Reader
    	Writer
    }
    

    3 多态

    多态的定义比较宽松:指一个行为具有多种不同的表现形式。

    本质上多态分两种:编译时多态(静态)和运行时多态(动态)

    1. 编译时多态在编译期间,多态就已经确定。重载是编译时多态的一个例子。
    2. 运行时多态在编译时不确定调用哪个具体方法,一直延迟到运行时才能确定。

    通常情况下,我们讨论的多态都是运行时多态。Golang 接口就是基于动态绑定实现的多态。

    由于 Golang 结构体是『组合』而非『继承』,不能相互转换,所以只有基于接口的多态。

    3.1 向上转型

    向上转型是实现多态的必要条件。即用父类的引用指向一个子类对象,通过父类引用调用方法时会调用子类的方法。通过父类引用无法调用子类的特有方法,需要『向下转型』。

    type Member interface {
    	SayHello() // 问候语
    	DoWork()   // 开始工作
    	SitDown()  // 坐下
    }
    
    type People struct {
    	name string
    	age  int
    }
    
    func (p *People) SetName(name string) {
    	p.name = name
    }
    
    func (p *People) GetName() string {
    	return p.name
    }
    
    func (p *People) SetAge(age int) {
    	p.age = age
    }
    
    func (p *People) GetAge() int {
    	return p.age
    }
    
    type Student struct {
    	People
    	grade string
    }
    
    func (s *Student) SetGrade(grade string) {
    	s.grade = grade
    }
    
    func (s *Student) GetGrade() string {
    	return s.grade
    }
    
    func (s *Student) SayHello() {
    	fmt.Println("student:", s.name, "说: 老师好!")
    }
    
    func (s *Student) DoWork() {
    	fmt.Println("student:", s.name, "记笔记")
    }
    
    func (s *Student) SitDown() {
    	fmt.Println("student:", s.name, "坐下")
    }
    
    type Teacher struct {
    	People
    	subject string
    }
    
    func (t *Teacher) SetSubject(subject string) {
    	t.subject = subject
    }
    
    func (t *Teacher) GetSubject() string {
    	return t.subject
    }
    
    func (t *Teacher) SayHello() {
    	fmt.Println("teacher", t.name, "说: 同学们好!")
    }
    
    func (t *Teacher) DoWork() {
    	fmt.Println("teacher", t.name, "讲课!")
    }
    
    func (t *Teacher) SitDown() {
    	fmt.Println("teacher:", t.name, "站着讲课,不能坐下!")
    }
    
    func main() {
    	stu := &Student{}
    	stu.SetName("张三")
    	stu.SetAge(13)
    	stu.SetGrade("七年级")
    
    	tea := &Teacher{}
    	tea.SetName("李四")
    	tea.SetAge(31)
    	tea.SetSubject("语文")
    
    	var member Member
    	member = stu
    	member.SayHello()
    	member.SitDown()
    	member.DoWork()
    	// student: 张三 说: 老师好!
    	// student: 张三 坐下
    	// student: 张三 记笔记
    
    	member = tea
    	member.SayHello()
    	member.SitDown()
    	member.DoWork()
    	// teacher 李四 说: 同学们好!
    	// teacher: 李四 站着讲课,不能坐下!
    	// teacher 李四 讲课!
    }
    

    这里是基于接口实现的『向下转型』,没有父类、子类之分。而在 Java 中,当子类没有重写父类方法时,父类的引用会调用到父类的方法里。其实也有类似的实现,在上面已经有了,即2.2.2 结构体中内嵌接口

    3.2 向下转型

    上面提到,用父类的引用指向一个子类对象,通过父类引用无法调用子类的特有方法。但在某些情况下需要调用子类的特有方法,例如子类有一些特殊逻辑需要处理,这时就需要『向下转型』还原出子类的引用。

    在 Java 里,通常用 User user = (User) people; 来向下转型。

    Golang 向下转型通过类型断言。基本用法是t,ok := intefaceValue.(T)

    一个例子:

    type Member interface {
    	SayHello() // 问候语
    }
    
    type People struct {
    	name string
    	age  int
    }
    
    func (p *People) SetName(name string) {
    	p.name = name
    }
    
    func (p *People) GetName() string {
    	return p.name
    }
    
    func (p *People) SetAge(age int) {
    	p.age = age
    }
    
    func (p *People) GetAge() int {
    	return p.age
    }
    
    type Student struct {
    	People
    	grade string
    }
    
    func (s *Student) SetGrade(grade string) {
    	s.grade = grade
    }
    
    func (s *Student) GetGrade() string {
    	return s.grade
    }
    
    func (s *Student) SayHello() {
    	fmt.Println("student:", s.name, "说: 老师好!")
    }
    
    func main() {
    	stu := &Student{}
    	stu.SetName("张三")
    	stu.SetAge(13)
    	stu.SetGrade("七年级")
    
    	var member Member = stu
    	member.SayHello()
    	// student: 张三 说: 老师好!
    	if student, ok := member.(*Student); ok {
    		student.SetName("王五")
    		student.SayHello()
    		// student: 王五 说: 老师好!
    	}
    }
    
    1. 使用 Member 类型的引用,指向一个 Student 对象
    2. 想对 Student 对象设置一个新名称,使用类型断言向下转型,可以调用 StudentSetName 方法(严格的说,是 PeopleSetName 方法)

    除了上面场景,还有一种场景需要类型断言:当有一个函数,其参数是 interface{} 类型时:

    func SayHello(inter interface{}) {
    	if member, ok := inter.(Member); ok {
    		member.SayHello()
    	}
    }
    
    func main() {
    	stu := &Student{}
    	stu.SetName("张三")
    	stu.SetAge(13)
    	stu.SetGrade("七年级")
    
    	SayHello(stu)
    	// student: 张三 说: 老师好!
    }
    

    补充:除了类型断言,Golang 还有一种 type-switch 的方式可以用于对 interface 的类型探测,从而进行不同逻辑的处理。

    4 参考资料

    1. Golang 中 struct 嵌入 interface
    2. 面向对象思考与 golang cobra 库实现原理
    3. 多态中的向上转型与向下转型
  • 相关阅读:
    scrapy--豆瓣top250--中间件
    FrameWork之旅 -- Activity过去的门面ActionBar
    多媒体应用设计师 第5章 多媒体信息显示、发布及搜索技术
    b=a++和b=++a区别及a=++a、a=a++区别
    《Python》典型数据结构
    花998购买的拍摄技巧和7天起号培训文档,学了一周的总结。
    Redis_01_Redis的引入
    [深度学习]yolov10+deepsort+pyqt5实现目标追踪
    javacc之路4---错误恢复
    Lambda 表达式怎么用?
  • 原文地址:https://www.cnblogs.com/ccwd/p/17800795.html