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)引用了相同的地址。
}