详解 Go之面向对象

面向对象的三大特点是:封装、继承、多态,那么Go是面向对象的语言吗?

一、传统面向对象语言的基本特征
  • 封装

封装,是面向对象方法的重要原则,就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。

  • 继承

继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

  • 多态

多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作

以Java语言为例,Java语言是妥妥的面向对象语言,他有 class 、implement、 extend 等关键字,也支持封装、继承、多态三个基本特征。

举个例子,假设有一个人的基类。人的吃、住是这个人的基本能力,那么我们就可以把 吃、住分别封装成两个方法,只对外暴露 “吃”,“住”,至于怎么吃的,怎么住的,这些细节我们隐藏起来。这就是实现了封装。

我们基于人的类,创建一个老师的类,并继承人类,那么老师也拥有了人类的吃住能力。这就是继承。

因为老师是一个比较特殊的群体,可能住的地方是教师宿舍,和一般的人住的地方不大一样,那么老师的 “住能力” 就可以重新实现细节,这就是多态的实现之一。

面向对象的写法,从某种意义上来说,是更加工程化的,与之相比的面向过程写法,相对来说更加的随意,代码复用性更低。

如果你写过PHP,你可能就深有感触。 PHP是一门即可以用面向过程,也支持面向对象的语言。

二、Go是面向对象的吗?

又回到了我们标题的提问了,首先,我们先看下官方是如何解释的。

Is Go an object-oriented language?
Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).
Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

官方认为是或者不是,go是明显允许面向对象的编程风格的,但同时又缺乏一些Java和C++中的常见类型或者说是关键字。Go的interface也和Java中的用法十分不同, 官方觉得这一套挺好的,更加的容易使用且通用性更强。大多数情况下我们都是使用OOP 的思想来组织 go 项目的,但是如果你要完全按照 Java 这种思想去思考 go,会发现十分的别扭。

参考PHP这种语言,PHP 其实也是有面向对象和面向过程的写法,但是PHP有着和 Java 很类似的组织形式,例如都有类、继承、重载、接口。相对来说学习成本更加低。

OOP 的项目结构,确实可以对代码有着较好的组织形式,可以帮助我们更好的去工程化我们的项目。

三、如何写出面向对象的GO

很多新手都觉得。GO没有 class、extend 怎么才能实现面向对象呢?
不要急,我们下面一点点学习Go的OOP是怎么写的。

1、类
type Person struct {
	name string
	age int32
}

GO 没有 class 关键字,实际上,会以结构体的方式去定义一个所谓的类,将相关的方法绑定在这个结构体上,作为一个整体的集合。也就是所谓的类。

类的实例化有两种。

p1 := new(Persion)
p2 := &Person{
	name : "MClink"
	age : 25
}
p3 := &Person{}
p4 := &Person{"MClink", 25}
var p5  Person{}

其中 p1 和 p3 的效果是一样的,但是分配的内存大小是不同,类的属性在不主动初始化赋值的情况下都是默认的零值。字符串的零值是 “”, int32 的零值是 0。则 p2 、p4 则是在实例化的同时,对属性也做了相应的赋值。

所谓的类,实际上是一种类型。因此 new 函数实际上的形参是一种类型,而不是值,返回的是这个类型的零值的指针。

2、方法
func (p *Person) GetName(name string) error {
	p.name = name 
	return nil
}

func (p Person) SetName() (string, error) {
	return p.name, nil
}

Go 中所谓的方法。实际上是依附于某个类(结构体类型,或者说是接收者)的 func。
这里比较特别的是。go 的 func 是支持多返回值的。这是十分特别的。以前,我们如果要返回多个返回值,一般通过使用数组或者集合的方式进行返回。

眼尖的你会发现,两个方法的接收者有点不同,一个是 p *T ,一个是 p T。我们分别称之为 指针方法和值方法。两个方法集合是有所差别的,我们用官方的话来看下。

Why do T and *T have different method sets?
As the Go specification says, the method set of a type T consists of all methods with receiver type T, while that of the corresponding pointer type *T consists of all methods with receiver *T or T. That means the method set of *T includes that of T, but not the reverse.
This distinction arises because if an interface value contains a pointer *T, a method call can obtain a value by dereferencing the pointer, but if an interface value contains a value T, there is no safe way for a method call to obtain a pointer. (Doing so would allow a method to modify the contents of the value inside the interface, which is not permitted by the language specification.)
Even in cases where the compiler could take the address of a value to pass to the method, if the method modifies the value the changes will be lost in the caller. As an example, if the Write method of bytes.Buffer used a value receiver rather than a pointer, this code:
var buf bytes.Buffer
io.Copy(buf, os.Stdin)
would copy standard input into a copy of buf, not into buf itself. This is almost never the desired behavior.

大概的意思就是指针方法集合包含了值方法的集合。有些人会纠结到底要用指针方法还是值方法。从本质上来说,就看你的接收者是否需要改动,需要改就用指针,不需要可以用指针也可以用值方法,所以,不要纠结了,统一用指针方法即可。

Go 语言没有 public、protected、private 三种范围修饰词。
采用的是首字母大小写的方式区分是否是包外可见的方法。大写说明是包外可见的,可以在包外使用。

3、继承

Java 或者 PHP 使用的是 extend 关键字来进行父类的继承。而 Go 则是使用组合的方式来实现继承。

举个例子

type Person struct {
	name string
	age int32
}

func (p *Person) GetName(name string) error {
	p.name = name 
	return nil
}

func (p Person) SetName() (string, error) {
	return p.name, nil
}

type Man struct {
	Person // 继承了Person类的所有方法和成员属性
	phone string
}

type WoMan struct {
	*Person // 以指针继承了 Person类的所有方法和成员属性
	phone string
}

采用组合的方式,可以让一些公共的部分单独抽离出来,像搭积木那样,将多个积木进行组合,达到自己所需的功能集合。组合内外如果出现名字重复问题,只会访问到最外层,内层会被隐藏,不会报错,即类似java中方法覆盖/重写。

4. 接口

Go 保留了接口。学过其他语言的应该知道,接口是一堆方法的集合,定义了对象的一组行为,主要是用来规范行为的。但是其他语言的接口实现是显式的,一般使用 implement 显式实现。而Go是隐式的,也就是说,你只要实现了 接口内的所有方法,便是继承了这个接口。
接口的作用主要是将定义与实现分离,降低耦合度。

4.1 接口的实现

举个例子

func (p *Man) Say() string {
	return ""
}

func (p *Man) Eat(something string) error {
	fmt.Print("man eat " +  something)
	return nil
}

func (w *WoMan) Say() string {
	return ""
}

func (w *WoMan) Eat(something string) error {
	fmt.Print("woman eat " +  something)
	return nil
}

type ManI interface {
	Say() string
	Eat(something string) error
}

这样 Man 类就实现了 ManI 接口。只要类实现了该接口的所有方法,即可将该类赋值给这个接口,接口主要用于多态化方法。即对接口定义的方法,不同的实现方式。
如:

...
	var mi ManI = new(Man)
	mi.Say()
	var mi2 ManI = new(Woman)
	mi2.Eat("好吃的")
...
4.2 接口的赋值

也就是说 ManI接口类型的变量可以用来承载所有实现了该接口的类。这样我们就可以通过传递类来调用不同类的相同方法了。例如

 func Call (mi ManI) {
		mi.Eat("牛肉粿条")
 }

	var mi ManI
	t := "woman"
	switch t {
	case "woman":
		mi = new(Woman)
	case "man":
		mi = new(Man)
	}
	Call(mi)

我们也可以将一个接口赋值给另一个接口,需要满足以下的条件

  • 两个接口拥有相同的方法列表(与次序无关),即使是两个相同的接口,也可以相互赋值

  • 接口赋值只需要接口A的方法列表是接口B的子集(即假设接口A中定义的所有方法,都在接口B中有定义),那么B接口的实例可以赋值给A的对象。反之不成立。
    例如:

type ManI interface {
	Say() string
	Eat(something string) error
}

type ManI2 interface {
	Say() string
}

func (p *Man) Say() string {
	return ""
}

func (p *Man) Eat(something string) error {
	return nil
}

	var mi ManI
	var mi2 ManI2
	mi = mi2 // 会报错
	mi2 = mi // 可以

即大集合接口可以赋值给小集合接口。

4.3 接口的判断

我们可以使用类型断言的方式,判断一个接口类型是否实现了某个接口。

func (p *Man) Say() string {
	return ""
}

func (p *Man) Eat(something string) error {
	return nil
}

type Man struct {
	Person // 继承了 Person类的所有方法
	phone  string
}

type ManI interface {
	Say() string
	Eat(something string) error
}

func main() {
	var mi ManI = &Man{
  		Person: Person{},
  		phone:  "",
	 }
	if _, ok := mi.(*Man); ok {
		fmt.Print("true") //结果是true
	} else {
		fmt.Println("false")
	}
	return
}

如果你对类型断言还不够熟悉,可以先读读这篇文章 传送门

我们也可以知道某个接口类型的值存的是哪个类型的。

var mi ManI = &Man{
		Person: Person{},
		phone:  "",
	}
	switch mi.(type) {
	case *Man:
		fmt.Println("man") // hint
	case *Woman:
		fmt.Println("woman")
	default:
		fmt.Println("none")
	}

x.(type) 是 go 的一种语法糖,用来识别接口类型值是实现该接口的哪个类型。只能配合switch 使用,其中 case 是枚举实现了该接口的类型。

4.4 接口的组合

接口和类型一样,也是可以组合的。几个小接口可以组合成一个大接口

type Human interface {
	ManI
	WoManI
}

type ManI interface {
	Say() string
	Eat(something string) error
}

type WoManI interface {
	Born() error
}
4.5 万能黑洞

空接口 interface{} 是一个没有方法的接口,因此任何类型都是他的实现,当你的参数无法确定类型时,interface{} 就是一个很好的容器。

5、一个完整的面向对象栗子
package main

import "fmt"

// 定义接口
type Human interface {
	Say(string)
	Eat(string)
}

// 定义类
type Man struct {
}
// 实例化方法,一般是 New+类名作为实例化的方法
func NewMan() *Man {
 return &Man{}
}
// 实现Human接口的Say
func (m *Man) Say(something string) {
	fmt.Println(something)
}
// 实现Human接口的Eat
func (m *Man) Eat(something string) {
	fmt.Println(something)
}

// 定义员工类,其中包含 Human 接口的值
type employees struct {
	man Human
}
// 实现实例化方法
func newEmployees() *employees {
 return &employees{
		man: NewMan(), // 实现该类型的接口值可以赋值该类型的对象
	}
}

func main() {
 // 获取员工类实例化对象
 emp := newEmployees()
 // 通过员工类中包含的 Human 接口值间接调用 Man 类的 Say 方法
 emp.man.Say("EAT")
}

你可能感兴趣的:(Go大法,golang,开发语言)