Go 其五 到底是不是面向对象语言 -- 封装数据和行为, 接口, 自定义类型

  关于Go是不是面向对象语言其实有很多争论,关于给出的解释是:Yes and no.

 

封装数据和行为
  结构体定义

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"

  

行为(方法)的定义

//第一种定义方式在实例对应方法被调用时,实例的成员会进行值复制
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)
}

  


Duck Type式接口实现
  接口定义

type Programmer interface {
  WriteHelloWorld() Code
}

  

  接口实现

type GoProgrammer struct {
}

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

  

 

Go接口
  与其他主要编程语言的差异

  1. 接口为非入侵性的,实现不依赖于接口的定义
  2. 所以接口的定义可以包含在接口使用者包内


  接口变量

var prog Coder = &GoProgrammer{}
// 以上prog是接口Coder的一个变量

  

当prog被初始化之后它有两部分

prog

type GoProgrammer stuct { //类型
类型 --> }

数据 --> &GoProgrammer{} //具体的实现

  


自定义类型

  举例:
1. type IntConvertionFn func(n int) int
2. type MyPoint int

Go语言中接口的实现是不依赖于接口的定义的,是采用DockType的方式

 

  接口

  以上是笔者在进行Go语言面向对象方面知识所记录的笔记,可能会有一些凌乱。如果你和我的技术栈类似,可能会对Go中的接口定义以及实现,Duck Type等部分内容感到新奇。可以看看接口这部分的代码:

package interface_test

import (
	"testing"
)

type Programmer interface{
	WriteHelloWorld() string
}

type GoProgrammer struct{

}

func (g *GoProgrammer) WriteHelloWorld() string{
	return "fmt.Println(\"Hello World!\")"
}

/*注意这里,其实p定义的是接口Programmer,而p = new(GoProgrammer)
  是将接口Programmer的具体实现'GoProgrammer'作为p的实例
  这里没有使用传统的【接口定义,接口实现继承自接口定义,具体使用的地方利用接口定义通过容器或别的方式获取接口实现】的传统方法
  而是使用了DuckType.所以DuckType就是指,这个鸟虽然我不知道是什么,但是看起来脚上有蹼,扁嘴,像是鸭子,那么将当它是鸭子。(2333,这是老师的原话)
  对应上面就是接口GoProgrammer所对应的方法‘WriteHelloWorld’与Programmer中定义的方法看起来是一样的,那么我们就当GoProgrammer是Programmer的具体实现
*/

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

  如上述代码,如果是以"C#"之类的语言来看,其实我们在TestClient中是定义了一个Programmer接口的变量p,而p的具体实现则是 “类” GoProgrammer 。但我们观察 “类” GoProgrammer,其实并不像C#中那样要继承自Programmer接口,只是这个方法定义的相同。这就是所谓的“Duck Type”.

 

  自定义类型

  而关于自定义类型,其实是可以自定义出一些“复杂”的类型,并利用这写类型来简化一些功能的实现.

package customer_type

import (
	"testing"
	"fmt"
	"time"
)

type IntConv func(op int) int

//通过自定义类型,让程序有更好的可读性,这里自定义了IntCov类型,这个类型是入参为一个int,返回一个int的函数
func timeSpent(inner func(op int) int) IntConv {
	return func(n int) int {
		start := time.Now()
		ret := inner(n)
		fmt.Println("time spent:", time.Since(start).Seconds())
		return ret
	}
}

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

func TestFn(t *testing.T) {
	// 计算函数运行时间
	tsSF := timeSpent(slowFun)
	t.Log(tsSF(10))
}

  如上述代码,我们的自定义类型IntConv其实是一个参数为int,返回值为int的函数。而timeSpent方法的返回值是IntConv类型,作用是计算某个函数的运行时间。如果你仔细观察,其实会发现timeSpent不只返回值,参数其实也是IntConv类型,因此这个方法改写成如下是完全OK的。

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
	}
}

  

  而关于行为方法的定义,其实没什么好说的,只是要注意两种方式的不同,为了避免内存复制,尽量使用 " Employee"这种方式.

package encapsulation

import (
	"testing"
	"fmt"
	"unsafe"
)

type Employee struct {
	Id string
	Name string
	Age int
}

func TestCreateEmployeeObj(t *testing.T){
	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"
	t.Log(e)
	t.Log(e1)
	t.Log(e1.Id)
	t.Log(e2)
    //%T 代表输出类型
	t.Logf("e is %T", e) 	//输出 e is encapsulation.Employee
	t.Logf("e2 is %T", e2)  //输出 e2 is *encapsulation.Employee
}

//第一种定义方式在市里对应方法被调用时,实例的成员会进行值复制
func (e Employee) String() string {
	fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
	return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}

//第二种通常情况下为了避免内存拷贝我们使用第二种定义方式
func (e *Employee) String2() string {
	fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
	return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}

func TestStructOperations(t *testing.T) {
	e := Employee{"0", "Bob", 20}
	t.Log(e.String())
	fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
	/*
		输出为:
		Address is c000064520
			TestStructOperations: encap_test.go:45: ID:0-Name:Bob-Age:20
		Address is c0000644f0
	*/

	t.Log(e.String2())
	fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
	/*
		输出为:
		Address is c0000644f0
			TestStructOperations: encap_test.go:48: ID:0/Name:Bob/Age:20
		Address is c0000644f0
	*/

	//结论,第一种方式(func (e Employee) String() string)会有更大的内存开销,因为是把结构的数据copy了一份,而第二种方式(func (e *Employee) String2() string)引用了相同的地址。
}

  

 

posted @ 2020-07-12 20:22  DogTwo  阅读(187)  评论(0编辑  收藏  举报