Golang学习笔记(05-2-OOP)

115 阅读6分钟

1. 封装

封装(encapsulation)是将字段和对字段的操作封装到一个特定代码块中,数据被保护在内部,这样其它程序想要访问或操作这些数据必须通道特定的、被授权的方法!封装有利于隐藏实例的细节,且可以对数据进行校验,保证数据的安全性。
在Go的开发中,并不是特别强调封装,这点与其它的OOP语言有所区别。Go中的实现方式如下:

  • 将结构体、字段的首字母小写,这样改结构体或者字段就不能导出,变成了包中的私有的
  • 结构体小写,并提供一个首字母大写的构造函数(工厂函数),这样就能通过构造函数去实例化
  • 提供 Set() 和 Get() 方法,实现对数据操作的封装和校验
package person

import (
	"fmt"
)

type person struct {
	Name string
	age  int
}

func NewPerson(name string) *person {
    // 以下两种写法都是可以的,部分编辑器可能会提示有异常
	//p := new(person)
	//p.Name = name
	//return p
	return &person{
		Name: name,
	}
}

// 对数据进行校验
func (p *person) SetAge(age int) {
	if age > 0 && age < 200 {
		p.age = age
		return
	}
	fmt.Println("输入的年龄不合法")
}

func (p *person)GetAge() int {
	return p.age
}
package main

import (
	"fmt"
	"studygo/day06/01-enc01/person"
)

func main()  {
	tom := person.NewPerson("Tom")
	fmt.Printf("%T %v\n", tom, tom)
	tom.SetAge(100)
	fmt.Printf("%T %v\n", tom, tom.GetAge())
}
[root@heyingsheng studygo]# go run day06/01-enc01/main/main.go
*person.person &{Tom 0}
*person.person 100

2. 继承

继承的目的是实现代码的复用,继承可以实现字段的继承和方法的继承,Go的继承是通过结构体嵌套实现的!Go中继承的实现逻辑如下:

  • 将不同结构体中相同的属性和方法抽象出来,组成一个新的结构体和结构体方法
  • 其它结构体在定义时,将该结构体作为一个字段,这样实现继承该抽象出来的结构体和方法

2.1. 继承的简单案例

在下面的案例中,将玩家、怪物和其它生物的共有数据提取出来,生成一个Animal的结构体,其它的所有怪物和玩家都继承Animal的方法和属性,这样就将相同的代码合并和简化!

package main

import "fmt"

// 生物信息
type Animal struct {
	Name string
	Blood int
}
// 玩家信息
type Player struct {
	Animal  // 匿名结构体
	San int
	Hunger int
}

func (a *Animal)Sleep() {
	fmt.Printf("%s在睡觉!\n", a.Name)
}

func (p *Player)Speak()  {
	fmt.Printf("%s在说话!\n", p.Name)
}

func (p *Player)Info() {
	fmt.Printf("玩家%s的血量:%v, San: %v,饱食度:%v\n", p.Name, p.Blood, p.San, p.Hunger)
}

func main()  {
	wendy := Player{
		Animal: Animal{Name:"wendy", Blood:150},
		San:    200,
		Hunger: 150,
	}
	wendy.Sleep()
	wendy.Speak()
	wendy.Info()
}
[root@heyingsheng studygo]# go run day06/02-inherit/mian.go
wendy在睡觉!
wendy在说话!
玩家wendy的血量:150, San: 200,饱食度:150

2.2. 继承的注意事项

2.2.1. 匿名结构体中字段和属性访问方式

在下面的案例中,Student继承了Person结构体,且Person和Student中有同名的字段Name,此时s.Name会指向Student中的Name,而Student中没有Age,s.Age会指向Person中的Age,这是继承中的就近原则!在方法中也是如此!
或者说,原本就是 s.Person.Age 和 s.Person.Name 才能访问到Person中的属性,Go为此做了简化,如果Student中没有的字段或方法,解释器会继续向父类寻找,直到顶层还没有就报错!

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

type Student struct {
	Person
	Name  string
	Class string
}

func (s *Student)String() string {
	return fmt.Sprintf("[s.name=%v; s.Class=%v: s.Age=%v, s.Person.Name=%v, s.Person.Age=%v]",
		s.Name, s.Class, s.Age, s.Person.Name, s.Person.Age)

}

func main() {
	s0 := Student{
		Person: Person{Name:"P-zhangsan", Age:18},
		Name:   "S-张三",
		Class:  "大一",
	}
	fmt.Println(&s0)
}
[root@heyingsheng studygo]# go run day06/02-inherit/no1.go
[s.name=S-张三; s.Class=大一: s.Age=18, s.Person.Name=P-zhangsan, s.Person.Age=18]

2.2.2. 多重继承中属性和方法的访问

在下面案例中,可以看到,在多重继承中,如果两个父类的属性有重名(Name, Age),且子类没有这个属性(Age),那么必须要明确指定是哪个父类的属性(Age): line26, line27 。方法也是如此!

package main

import "fmt"

type A struct {
	Name string
	Age  int
}

type B struct {
	Name string
	Age  int
	Sal  float64
}

type C struct {
	A
	B
	Name string
}

func main() {
	var c C
	c.Name = "张三" // c.Name
	c.Sal = 10000 // c.B.Sal
	//c.Age  = 18  // 报错 ambiguous selector c.Age 模糊的选择器 c.Age
	c.A.Age = 18
	fmt.Printf("%v\n", c)
}

2.2.3. 有名结构体(组合)

上面提到的继承都是继承自匿名结构体,如果是有名结构体,那么称为组合,其实是一个特殊的继承方式。这种继承方式下,想要访问父类的方法和属性,必须要明确指定哪个父类,不可以简写!如下案例对别上面的案例分析。

package main

import "fmt"

// 生物信息
type Animal struct {
	Name  string
	Blood int
}

// 玩家信息
type Player struct {
	A      Animal // 有名结构体
	San    int
	Hunger int
}

func (a *Animal) Sleep() {
	fmt.Printf("%s在睡觉!\n", a.Name)
}

func (p *Player) Speak() {
	//fmt.Printf("%s在说话!\n", p.Name) //p.Name undefined (type *Player has no field or method Name)
	fmt.Printf("%s在说话!\n", p.A.Name)
}

func (p *Player) Info() {
	fmt.Printf("玩家%s的血量:%v, San: %v,饱食度:%v\n", p.A.Name, p.A.Blood, p.San, p.Hunger)
}

func main() {
	wendy := Player{
		A: Animal{Name: "wendy", Blood: 150},
		San:    200,
		Hunger: 150,
	}
	wendy.A.Sleep()
	wendy.Speak()
	wendy.Info()
}

2.3. 方法继承和重写实现思路

2.3.1. 方法继承

package main

import "fmt"

type service struct {
	name string
	port uint16
}

type database struct {
	service
	dbType string
	dataDir []string
	logDir string
}

func (s service)manager(operate string)  {
	switch operate {
	case "start":
		fmt.Printf("%s start!\n", s.name)
	case "stop":
		fmt.Printf("%s stop!\n", s.name)
	}
}

func (d database)backup() {
	fmt.Printf("%s backup success!\n", d.name)
}

func main()  {
	mysql := database{
		service: service{
			name: "MySQL",
			port: 3306,
		},
		dbType:  "InnoDB",
		dataDir: []string{"/data/mysql/data"},
		logDir:  "/data/mysql/logs",
	}
	mysql.manager("start")  // 继承了嵌套结构体的方法
	mysql.backup()
}
[root@heyingsheng day04]# go run 09-struct/main.go
MySQL start!
MySQL backup success!

2.3.2. 方法重写

package main

import "fmt"

type service struct {
	name string
	port uint16
}

type database struct {
	service
	dbType string
	dataDir []string
	logDir string
}

func (s service)manager(operate string)  {
	switch operate {
	case "start":
		fmt.Printf("%s start!\n", s.name)
	case "stop":
		fmt.Printf("%s stop!\n", s.name)
	}
}

func (d database)manager(operate string)  {
	switch operate {
	case "start":
		fmt.Printf("%s start!\n", d.name)
	case "stop":
		fmt.Printf("%s stop!\n", d.name)
	case "restart":
		fmt.Printf("%s restart!\n", d.name)
	}
}

func main()  {
	mysql := database{
		service: service{
			name: "MySQL",
			port: 3306,
		},
		dbType:  "InnoDB",
		dataDir: []string{"/data/mysql/data"},
		logDir:  "/data/mysql/logs",
	}
	mysql.manager("restart")
}
[root@heyingsheng day04]# go run 09-struct/main.go
MySQL restart!

2.4. 继承和接口的区别

  • 接口和继承需要解决的问题不同:
    • 继承是将重复代码抽象出来,解决的是代码的复用性和可维护性
    • 接口在于定义需要实现的方法,让其它类型去实现这些方法,提高代码规范性
  • 接口和继承与数据类型的关系不同
    • 接口要满足的是 Like - a 的关系,不需要明确指定
    • 继承要满足的是 Is - a 的关系,需要明确指定

3. 多态

Golang中的多态是使用 Interface 来实现的,即变量或者实例具备多种形态。在Golang中多种数据类型实现了相同的接口,那么这个接口变量就具备了不同的数据形态,调用相同的方法也会因为数据类型不同就呈现了不同的结果!

3.1. 多态参数

定义一个形参为接口的函数,该函数就能接收所有实现该接口的实例,并根据实例所属类型不同,实现不同的逻辑,如下面这个简单案例所演示的,line 24 backup 函数即可接收mysql结构体实例,也可以接收redis结构体实例!再比如,一个数据查询函数接收一个接口变量完成数据查询,这个接口变量可以是 mysql ,也可以是 oracle,只要mysql和oracle实现了这个接口即可。

package main

import "fmt"

type redis struct {
	name    string
	dstPath string
}

func (r redis) backup() {
	fmt.Printf("%s 备份完毕,备份存在路径: %s\n", r.name, r.dstPath)
}

type mysql struct{ name string }

func (m mysql) backup() {
	fmt.Println(m.name, "备份完毕!")
}

type backuper interface {
	backup()
}

func backup(b backuper) {
	b.backup()
}

func main() {
	redis := redis{"Redis", "/opt/backup/redis"}
	mysql := mysql{"MySQL"}
	backup(redis)
	backup(mysql)
}

3.2. 多态数组

标题叫多态数组,其实可以是切片、数组、字段等。以多态数组为例:

package main

import "fmt"

type Student struct {
	Name  string
	Score int
}

type Worker struct {
	Name string
	sal  float64
}

type Person interface {
	info()
}

func (s Student)info()  {
	fmt.Printf("[name=%v; sal=%v]\n", s.Name, s.Score)
}

func (w Worker)info()  {
	fmt.Printf("[name=%v; sal=%v]\n", w.Name, w.sal)
}

func main()  {
	var s0 = [3]Person{}
	s0[0] = Student{"张三", 94}
	s0[1] = Worker{"李四", 15000}
	s0[2] = Student{"王五", 68}
	fmt.Println(s0) // [{张三 94} {李四 15000} {王五 68}]
}