go语言学习笔记之面向对象

go 究竟是不是面向对象语言,官方给出了这样的回答:

可以认为是,也可以认为否。虽然Go有类型和方法,并且允许面向对象的编程风格,但在Go中没有继承。Go中的接口提供了一种不同的方法,易于使用,在某些方面更为通用。此外,Go中缺少继承之类的层次结构,使得Go中的“对象”比C++或java语言中的对象更加轻量级。

我们熟知的面向对象的语言有 Java。在Java 中,头等公民是对象,一切都要封装在一个对象中。而在Go语言中,函数是头等公民,函数可以单独存在,可以作为参数或者返回值进行传递。

go封装数据和方法

数据封装

go 语言中,可以把 struct 当作 Java 中的类,使用 struct 封装数据。

结构体定义,比如如下的定义相当于是封装了一个 Employee 对象。

type Employee struct {
	Id string
	Name string
	Age int
}

实例创建及初始化

e := Employee{"0", "Bob", 20}
e1 := Employee{Name: "Mike", Age: 30}
e2 := new(Employee) //注意这⾥返回的引⽤(指针),相当于 e := &Employee{}
e2.Id =2" //与其他主要编程语⾔的差异:通过实例的指针访问成员不需要使⽤->
e2.Age = 22
e2.Name = “Rose

示例

type Employee struct {
	Id   string
	Name string
	Age  int
}

func Test(t *testing.T) {
	e0 := Employee{Id: "1000", Name: "Lily", Age: 20}
	e1 := Employee{"1001", "Bob", 20}
	e2 := new(Employee) //返回指针
	// 虽然e2是指针,但访问field时,不需要使用 -> 符号,直接用.符号就可以进行访问
	e2.Id = "1002"
	e2.Name = "Simon"
	e2.Age = 22
	t.Log("e0", e0)
	t.Log("e1", e1)
	t.Log("e2", e2)
	// 返回 e1 和 e2的创建类型
	t.Logf("e1 is %T", e1)
	t.Logf("e2 is %T", e2)
}

输出

=== RUN   Test
    object_oriented_test.go:19: e0 {1000 Lily 20}
    object_oriented_test.go:20: e1 {1001 Bob 20}
    object_oriented_test.go:21: e2 &{1002 Simon 22}
    object_oriented_test.go:23: e1 is object_oriented.Employee
    object_oriented_test.go:24: e2 is *object_oriented.Employee
--- PASS: Test (0.00s)
PASS

成员方法

定义对象的成员方法主要有如下两种方法。需要注意的是,在Java 中成员方法都是定义在一个类中的,但是go语言中,定义方法并不是在 struct 中进行定义,只需新建一个方法,传入的 什么类型的指针,就表示这个方法归属于什么指针类型的对象。

type Employee struct {
	Id   string
	Name string
	Age  int
}

//第⼀种定义⽅式在实例对应⽅法被调⽤时,实例的成员会进⾏值复制
func (e Employee) String() string {
	return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
} 

//第二种方法,复制的仅仅是指针,指针指向的对象不会发生复制。通常情况下为了避免内存拷⻉我们使⽤第⼆种定义⽅式
func (e *Employee) String() string {
	return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}

示例

type Employee struct {
	Id   string
	Name string
	Age  int
}

//func (e Employee) String() string {
//	return fmt.Sprintf("ID:%s Name:%s Age:%d", e.Id, e.Name, e.Age)
//}

// 成员方法的写法 (e *Employee) 对应着 Employee 这个对象,使用这种方法,不用发生值复制
func (e *Employee) String() string {
	// %x 打印地址
	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
	fmt.Println()
	return fmt.Sprintf("ID:%s Name:%s Age:%d", e.Id, e.Name, e.Age)
}

func TestObjectMethod(t *testing.T) {
	e := Employee{Id: "1000", Name: "Lily", Age: 20}
	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
	fmt.println()
	fmt.Println(e.String())
}

从输出中可以发现使用 e *Employee 时,Employee 对象的name的地址都是同样的,说明参数传递时,没有发生值复制。

=== RUN   TestObjectMethod
Address is c00006e5b0
Address is c00006e5b0
ID:1000 Name:Lily Age:20
--- PASS: TestObjectMethod (0.00s)
PASS

go语言的接口

接口的定义与使用

go 语言中没有 implements 这样的关键字来显示声明某个类实现了某个接口。在 go 语言中只要定义的方法签名与接口中定义的方法的方法签名相同,且传入入了某个类型的对象,那么就可以认为这个对象所在的类 implements 了某个接口。

这就是所谓的鸭子理论:一个东西看起来像鸭子,走起来也像鸭子,叫起来也像鸭子,那我们就认为这个东西就是个鸭子。

示例

import "testing"

// 定义接口
type Programmer interface {
	// 需要注意的是必须实现 interface 中的所有方法,才会有继承关系
	WriteHelloWorld()
}

// 定义实现类
type GoProgrammer struct {
}

// 没有显示指定某个类 implement某个接口,新建一个方法与 interface 中的方法签名相同
// 且传入相应的 struct ,即可认为实现了某个接口中的方法
func (p *GoProgrammer) WriteHelloWorld() {
	fmt.Println("Hello World")
}

func TestInterface(t *testing.T) {
	var p Programmer
	p = new(GoProgrammer)
	p.WriteHelloWorld()
}

我的开发环境是 Goland。
可以发现,当具有继承关系时,IDE 中会出现一些向上向下的箭头
在这里插入图片描述

如下图所示,当我在接口中再定义一个方法,却没有实现该方法,此时 WriteHelloWorld 这个方法归属于 GoProgrammer 这个对象,并没有实现 Programmer 这个接口,可以发现 IDE 中也没有提示任何的继承关系。
在这里插入图片描述

接口定义好,实现类也有的了,就可以这样调用了。

func TestInterface(t *testing.T) {
	var p Programmer
	p = new(GoProgrammer)
	p.WriteHelloWorld()
}

输出

=== RUN   TestInterface
Hello World
--- PASS: TestInterface (0.00s)
PASS

综合上面的示例,我们可以看出 go 语言中的接口更加灵活。接口为非入侵性,实现不依赖于接口定义。所以接口的定义可以包含在接口使用者(客户端)包内,不用放在实现类的包中。

在 Java 中使用接口,可以达到定义与实现分离的目的。这样,使用者就可以为接口选择不同的实现。但在开发的过程中,我们会发现,Java 中定义的接口与实现类通常都是放在一个包下的,通常都将接口的定义与实现都打包放在服务端的代码中,这在一定程度上造成了耦合。

List<String> list1 = new ArrayList<>();
List<String> list2 = new LinkedList<>();
List<String> list3 = new CopyOnWriteArrayList<>();

自定义类型(取别名)

在 Go 语言中,函数可以作为参数也可以作为返回值,在方法中定义的时候,函数的签名太长,定义的方法就看起来不美观,这个时候就可以使用自定义的类型。

// 自定义类型
type InnerFuncType func(param int) int

// 代码1 代码1的方法定义等效于方法2的定义
func calculateSpendTime(innerFunc InnerFuncType) InnerFuncType {
	return func(param int) int {
		startTime := time.Now()
		// 计算返回值
		ret := innerFunc(param)
		fmt.Printf("spend time = %f", time.Since(startTime).Seconds())
		fmt.Println()
		return ret
	}
}

// 代码2
//func calculateSpendTime(innerFunc func(param int) int) func(param int) int{
//	return func(param int) int {
//		startTime := time.Now()
//		// 计算返回值
//		ret := innerFunc(param)
//		fmt.Printf("spend time = %f", time.Since(startTime).Seconds())
//		fmt.Println()
//		return ret
//	}
//}

go 接口的最佳实践

  • 倾向于使用小的接口定义,接口中尽量只定义一个方法
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
}
  • 只依赖于必要功能的最小接口(便于复用)
    比如仅仅只需要 Reader 中的 Read 方法,那么传入的参数使用 Reader 接口即可,不要使用 ReadWriter 接口
func StoreData(reader Reader) error {}

通过组合实现扩展

组合

go 中没有继承,不能通过继承实现扩展,可以通过组合的方式实现扩展。

// 定义了 Pet
type Pet struct {

}

// Pet 的成员方法
func (p *Pet) Speak(voice string) {
	fmt.Println(voice)
}

func (p *Pet) SpeakToHost(voice string, host string) {
	p.Speak(voice)
	fmt.Println(host)
}

// 定义了 Dog
type Dog struct {
	// 定义了Pet类型的指针
	p *Pet
}

func TestExtension(t *testing.T) {
	d := &Dog{}
	// 通过使用 Pet 类型的指针就能调用到 Speak 方法
	d.p.Speak("Wang Wang Wang")
	d.p.SpeakToHost("Wang Wang Wang", "Simon")
}

输出

=== RUN   TestExtension
Wang Wang Wang
Wang Wang Wang
Simon
--- PASS: TestExtension (0.00s)
PASS

匿名类型组合

在进行组合的时候,Go 提供了一个简化机制,不需要在定义结构体时再定义需要组合的对象的指针,如下所示:

// 定义了 Dog
type Dog struct {
	// 在这里只需要写上需要组合的对象
	Pet
}

然后调用的时候,就不用通过定义到 指针来进行调用了。

func TestExtension(t *testing.T) {
	d := &Dog{}
	d.Speak("Wang Wang Wang")
	d.SpeakToHost("Wang Wang Wang", "Simon")
}

完整代码如下所示:

// 定义了 Pet
type Pet struct {

}

func (p *Pet) Speak(voice string) {
	fmt.Println(voice)
}

func (p *Pet) SpeakToHost(voice string, host string) {
	fmt.Println(voice, host)
}

// 定义了 Dog
type Dog struct {
	// 在这里只需要写上需要组合的对象
	Pet
}

func TestExtension(t *testing.T) {
	d := &Dog{}
	d.Speak("Wang Wang Wang")
	d.SpeakToHost("Wang Wang Wang", "Simon")
}

输出与结构体中定义指针,然后通过指针调用方法的结果相同。

=== RUN   TestExtension
Wang Wang Wang
Wang Wang Wang
Simon
--- PASS: TestExtension (0.00s)
PASS

接着看,如果我们为 Dog 这个对象也增加了一个 Speak 这个方法,那么通过 d.Speak 进行调用时,会调用哪个方法呢?

func (d *Dog) Speak(voice string) {
	fmt.Println("I am a dog ==>", voice)
}

完整代码

// 定义了 Pet
type Pet struct {

}

func (p *Pet) Speak(voice string) {
	fmt.Println(voice)
}

func (p *Pet) SpeakToHost(voice string, host string) {
	p.Speak(voice)
	fmt.Println(host)
}

// 定义了 Dog
type Dog struct {
	Pet
}

func (d *Dog) Speak(voice string) {
	fmt.Println("I am a dog ==>", voice)
}

func TestExtension(t *testing.T) {
	d := &Dog{}
	fmt.Println("调用 Speak 方法")
	d.Speak("Wang Wang Wang")
	fmt.Println("调用 SpeakToHost 方法")
	d.SpeakToHost("Wang Wang Wang", "Simon")
}

从调用 Speak 方法的输出中我们可以发现调用的是Dog类对应的 Speak 方法,而不是 Pet 类中的 Speak 方法,这点和 Java 继承中的 Override 很类似。但是再看调用 SpeakToHost 方法的结果可以发现,这又并不是 Java 中的继承,因为子类一旦重写了父类的某个方法,那么子类对这个方法的调用会替换为调用子类的方法

=== RUN   TestExtension
调用 Speak 方法
I am a dog ==> Wang Wang Wang
调用 SpeakToHost 方法
Wang Wang Wang
Simon
--- PASS: TestExtension (0.00s)
PASS

上面的说法比较晦涩,可以写一段 Java 代码来看看。

定义 Pet 类

public class Pet {

    public void Speak(String voice) {
        System.out.println(voice);
    }

    public void speakToHost(String voice, String host) {
        this.Speak(voice);
        System.out.println(voice + " " + host);
    }
}

定义 Dog 类

public class Dog extends Pet{
    @Override
    public void Speak(String voice) {
        System.out.println("I am a dog I can " + voice);
    }
}

测试类

public class Main {

    public static void main(String[] args) {
        Pet pet = new Pet();
        System.out.println("pet 调用 Speak 方法");
        pet.Speak("...");
        System.out.println("pet 调用 SpeakToHost 方法");
        pet.speakToHost("...", "Simon");
        Pet dog = new Dog();
        System.out.println("dog 调用 Speak 方法");
        dog.Speak("Wang Wang Wang");
        System.out.println("dog 调用 SpeakToHost 方法");
        dog.speakToHost("Wang Wang Wang", "Simon");
    }
}

输出,输出中可以发现子类有两个行为。子类没有 Override 的方法,就调用父类的方法,子类 Override 的方法,就调用子类 Override 的方法。

pet 调用 Speak 方法
...
pet 调用 SpeakToHost 方法
...
... Simon
dog 调用 Speak 方法
I am a dog I can Wang Wang Wang
dog 调用 SpeakToHost 方法
I am a dog I can Wang Wang Wang
Wang Wang Wang Simon

最后再总结一下 go 语言中的组合,虽然可以通过匿名类型组合的方式进行简写,某些地方与 Java 的继承相似,但是 go 中的组合并不是 Java 中的继承,不能使用 LSP(里氏替换原则,凡是可以用父类或者基类的地方,都可以用子类替换),也不存在子类,父类的说法,因此它们之间的引用并不能互相转换。

如下所示:即便进行了强制类型转换,也会报错,因此两者根本无法转换。
在这里插入图片描述

多态与空接口

多态

示例代码

package polymorphic

import (
	"fmt"
	"testing"
)

// 自定义类型 Code 实际上就是 string 类型
type Code string

// 定义接口
type Programmer interface {
	// 需要注意的是必须实现 interface 中的所有方法
	WriteHelloWorld() Code
}

// GoProgrammer 
type GoProgrammer struct {
}

func (p *GoProgrammer) WriteHelloWorld() Code {
	return "fmt.Println(\"Hello World\")"
}

// JavaProgrammer 
type JavaProgrammer struct {
}

func (p *JavaProgrammer) WriteHelloWorld() Code {
	return "System.out.println(\"Hello World\");"
}

// 传入的参数只能为引用类型
func writeFistProgram(p Programmer) {
	// %T 输出实例或者是参数的类型
	fmt.Printf("p的类型为:%T p的值为:%v\n", p, p.WriteHelloWorld())
}

// 多态
func TestPolymorphic(t *testing.T) {
	// interface 不能实例化,对应的类型只能是一个指针
	goProg := &GoProgrammer{}     //等效为 goProg := new(GoProgrammer),不能写作 goProg := GoProgrammer{}
	javaProg := &JavaProgrammer{} //等效为 javaProg := new(JavaProgrammer),不能写作 javaProg := JavaProgrammer{}
	writeFistProgram(goProg)
	writeFistProgram(javaProg)
}

输出

=== RUN   TestPolymorphic
p的类型为:*polymorphic.GoProgrammer p的值为:fmt.Println("Hello World")
p的类型为:*polymorphic.JavaProgrammer p的值为:System.out.println("Hello World");
--- PASS: TestPolymorphic (0.00s)
PASS

空接口与断言

  1. 空接口可以表示任何类型(类似于 Java 中的 Object 类型)
  2. 通过空接口转化为具体的类型时,不是使用强制类型转换,而是通过断言。通过断言来将空接⼝转换为指定类型。
    v, ok := p.(int) //ok=true 时为转换成功
func DoSomething(p interface{}) {
	// 代码1 等效于代码2
	// p.(int) 表示断言,如果能断言为 int,则 ok 为true
	if i, ok := p.(int); ok {
		fmt.Println("integer", i)
		return
	}
	if s, ok := p.(string); ok {
		fmt.Println("string", s)
		return
	}
	fmt.Println("Unknown Type")

	// 代码2
	//switch t := p.(type) {
	//case int:
	//	fmt.Println("integer", t)
	//case string:
	//	fmt.Println("string", t)
	//default:
	//	fmt.Println("Unknown Type")
	//}
}

func TestEmptyInterface(t *testing.T) {
	DoSomething("1000")
}

输出

=== RUN   TestEmptyInterface
string 1000
--- PASS: TestEmptyInterface (0.00s)
PASS

参考
Go语言从入门到实战 蔡超

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值