17

Go语言基础(三)—— 面向对象编程

 4 years ago
source link: https://studygolang.com/articles/26394
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言:

647

目录如下:

Go语言基础(一)—— 简介、环境配置、HelloWorld

Go语言基础(二)—— 基本常用语法

Go语言基础(三)—— 面向对象编程

Go语言基础(四)—— 优质的容错处理

Go语言基础(五)—— 并发编程

Go语言基础(六)—— 测试、反射、Unsafe

Go语言基础(七)—— 架构 & 常见任务

Go语言基础(八)—— 性能调优

本篇将介绍如下内容:

1.Go是面向对象的语言么?

2.结构体与行为(方法)的定义

3.接口(协议)

我们先来看个引子:“Is Go an object-oriented language?”

ZVnaye3.png!web

在Go的官方论坛里有世界各地的开发者讨论,

问:“Go是一种面向对象的语言吗?”

答案是:是也不是。

因为我们都知道,面向对象的三大特性是:

  1. 封装
    隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
  2. 继承
    提高代码复用性;继承是多态的前提。
  3. 多态
    父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。

然而, Go语言并不支持继承 。提倡使用组合(has-a)而不是继承(is-a)。

具体内容可查看第一篇: 《Go语言基础(一)—— 简介、环境配置、Hello World》因此,我们说Go不是标准的“面向对象”语言。

那为什么又说也是“面向对象”语言呢?

虽然Go语言中无法继承,但它依然允许面向对象的编码风格。

Go中提供了接口(协议)的概念,提供了一种面向的新思路,且在某些方面更通用,更高效。因此,对比C++/Java等语言,Go变的更加轻量级。

一、结构体 / 行为(方法)的定义

1.结构体的定义

type Employee struct {
	Id   int
	Name string
	Age  int
}
复制代码

2.结构体的三种初始化方式:

func TestCreateEmployeeObj(t *testing.T) {
	/* 第一种构造方式,返回对象 */
	e1 := Employee{1, "647", 23}

	/* 第二种构造方式,返回对象 */
	e2 := Employee{Id: 2, Name: "647", Age: 23}

	/* 第三种构造方式,返回指针 */
	e3 := new(Employee) // 返回指针
	e3.Id = 3
	e3.Age = 23
	e3.Name = "647"

	t.Log("e1 = ", e1)
	t.Log("e2 = ", e2)
	t.Log("e3 = ", *e3)
	t.Logf("e1 is %T", e1) // %T打印类型
	t.Logf("e2 is %T", e2)
	t.Logf("e3 is %T", *e3)
}
复制代码

3.行为(方法)的定义:

  • 拷贝对象的方法定义:
func (e Employee) String1() string {
	fmt.Printf("String1's e address is %x\n", unsafe.Pointer(&e.Name))
	return fmt.Sprintf("Id:%d, Name:%s, Age:%d", e.Id, e.Name, e.Age)
}
复制代码
  • 非拷贝行为(方法)的定义:
func (e *Employee) String2() string {
	fmt.Printf("String2's e address is %x\n", unsafe.Pointer(&e.Name))
	return fmt.Sprintf("Id:%d, Name:%s, Age:%d", e.Id, e.Name, e.Age)
}
复制代码

这两种有什么区别呢?

其实从返回的结果来看是一致的,但“非拷贝行为(方法)的定义”会复用原来的对象,而拷贝行为(方法)的定义会复制出一个对象出来。(通过打印对象的内存地址可以发现。)

demo:

func TestStructOperations(t *testing.T) {
	e := Employee{1, "647", 23}
	fmt.Printf("Origin's e address is %x\n", unsafe.Pointer(&e.Name))
	t.Log("拷贝对象:", e.String1())
	t.Log("非拷贝对象:", e.String2())
}
复制代码

打印结果:

rey2qiv.png!web

二、接口(协议)

与其他编程语言的区别:

非侵入性:实现不依赖与接口的定义。 因此,接口的定义可以包含在接口的使用者package内。

举个例子:

iIN7zy3.png!web

实例代码:

// 接口(协议)的定义
type Programmer interface {
	WriteHelloWorld() string
}

// 接口(协议)的类型
type GoProgrammer struct {
}

// 接口(协议)的实现
func (_ *GoProgrammer) WriteHelloWorld() string {
	return "fmt.Println(\"Hello World\")"
}

// 测试demo:
func TestClient(t *testing.T) {
	var coder = new(GoProgrammer)
	t.Log(coder.WriteHelloWorld())
}
复制代码

自定义类型:

我们可以把常用的一些类型做一些简化自定义,抽出来,便于代码的简洁和可读。

例如,我们想监测方法的耗时,用自定义类型简化就变成了这样:

type IntConv func(op int) int // 自定义类型

func timeSpent(inner IntConv) IntConv {
	return func(n int) int {
		start := time.Now()
		ret := inner(n)
		fmt.Println("time spent:", time.Since(start).Seconds())
		return ret
	}
}

func slowFunc(op int) int {
	time.Sleep(time.Second * 1)
	return op
}

func TestFunc(t *testing.T) {
	t.Log(timeSpent(slowFunc)(1))
}
复制代码

其中: type IntConv func(op int) int 就是自定义类型。

三、扩展(复用)

在本篇的开始,已经提到过 —— Go本身是不支持继承的

因此,我们只能通过组合(has-a)的方式,来达到类似的效果。

我们举一个Pet与Dog的例子,直接上代码:

// Pet结构体
type Pet struct {
	id      int
	name    string
	variety string
}

func (p *Pet) Speak() {
	fmt.Print("Pet")
}

func (p *Pet) SpeakTo(host string) {
	p.Speak()
	fmt.Println(" speak to", host)
}

// Dog结构体
type Dog struct {
	Pet // 组合的方式(相当于拥有了所有Pet的方法与属性)
}

func (d *Dog) Speak() {
	fmt.Println("wang wang wang.") // 但并不支持重载
}

// 测试结果发现,依然是Pet,说明不支持重载(因为不是继承)
func TestDog(t *testing.T) {
	dog := new(Dog)
	dog.SpeakTo("647")
}
复制代码

但是我们打印一下结果:

mauqEb2.png!web

发现并不是 Dog speak to 647?

为什么呢?因为Go本身不支持继承,所以也不支持方法重载。 因此,我们在调用 dog.SpeakTo 的时候依然调用的是Pet的方法。 由此可见,Go确实不支持多继承。

四、空接口 & 断言

空接口:表示各种类型。 断言:将空接口转换成指定类型。

  • 空接口:
func DoSomething(p interface{}) {...}
复制代码
  • 断言:
v, ok := p.(int) // ok = true时,代表转换成功。
复制代码

举个例子,根据数据类型,打印不同的数据:

func DoSomething(p interface{}) {
	switch v := p.(type) {
	case int:
		fmt.Println("Integer:", v)
	case string:
		fmt.Println("String:", v)
	default:
		fmt.Println("Unknown Type.")
	}
}

func TestEmptyInterfaceAssertion(t *testing.T) {
	DoSomething(10)
	DoSomething("10")
}
复制代码

那么问题来了,如何设计一个好的Go接口?

主要有以下几点:

  1. 使用小接口定义,甚至有的接口可以只定义一个方法。(这样,业务方在接入的时候,可以根据自己的需求,只实现个别接口)

  2. 可以采用接口套接口的形式,大接口由多个小接口组成。

  3. 业务方在实现功能时,只依赖于最小的接口。

举个例子:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type ReadWriter interface {
	Reader
	Writer
}

func DoSomething(reader Reader) error {
	// ...
}
复制代码

最后,本系列我是在蔡超老师的 技术分享 下总结、实战完成的, 感谢蔡超老师的 技术分享

PS:另附上,分享链接: 《Go语言从入门到实战》 祝大家学有所成,工作顺利。谢谢!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK