Go语言基础之10--面向对象编程2之方法

一、方法的定义

之前我们学习了结构体(struct),其仅仅是对数据的封装,并没有行为方法,还不是一个完全的面向对象的思路,所以现在我们来学习在结构体的基础上如何去定义一个方法。结构体(类)+方法=完整的面向对象

1.1 定义与声明

1)和其他语言不一样, Go的方法采用另外一种方式实现。

2)Go的方法是在函数前面加上一个接受者,这样编译器就知道这个方法属于哪个类型了。接受者是值类型还是指针类型也就决定了该方法是属于值类型还是指针类型。

3)定义一个类(结构体)里面的方法有两种:

第一种:值类型形式

第二种:指针类型形式

声明:

解释:

一般传入参数分为2部分:

1、func (a A)这里要传入的是类型的参数

2、Test(s string)这里要传入的是方法的参数

1.2 基于已有类型定义另外一种类型

具体见如下实例:

package main

import (
    "fmt"
)

//用已经存在的一种类型,去定义另外一种类型//定义一个Integer 类型,是int64的别名(Integer是新定义的一个类型,其底层就是int64类型)
type Integer int64

//定义一个值类型为Integer的Print0方法
func (i Integer) Print0() {
    fmt.Printf("i=%d
", i)
}

//定义一个指针类型为Integer的Set方法
func (i *Integer) Set(b int64) {
    *i = Integer(b)
}

func main() {
    var a Integer
    a = 1000

    fmt.Printf("a=%v
", a)

    var b int64 = 500
    a = Integer(b) //a是Integer类型(新定义一个类型),虽然底层和b一样也是int64,但是此处还是需要强转一下,不然会报错。
    fmt.Printf("a=%v
", a)

    a.Print0()
    a.Set(100000)

    a.Print0()
}

 执行结果如下图所示:

注意:

计算机中内存地址用16进制来表示

1.3 函数和方法的区别

函数不属于任何类型,方法属于特定的类型

通过下面这个实例来更好理解:

 

解释:

我们定义一个int64类型的变量,但是int64并不能调用Print0方法,因为Print0方法是属于特定的Integer类型的。(如上图,已经标红,证明是有问题的。)

1.4 重点实例

代码结构如下:(包含main.go和util.go两个)

 

util.go的内容主要是为了掩饰同一个包不同文件去调取同一个类(结构体)。

main.go代码如下:

package main

import (
    "fmt"
)

//1. 定义个Student的类型
type Student struct {
    Name string
    Age  int
}

//下面定义了类型为student的两个方法:Getname方法和Setname方法
//定义了一个值类型(完全拷贝副本)为Student的GetName方法
func (s Student) GetName() string { //s为变量,student为类型
    return s.Name //return这里返回的是拷贝的那个副本的Name,其并没有修改,所以还是s5
}

//定义了一个指针类型(拷贝的是地址)为Student的SetName方法
func (s *Student) SetName(name string) {
    s.Name = name //在这里将s1.Name对应内存地址的值改为了s2,所以s1中的Name值由s5改为了s2
}

func main() {
    //2.定义一个类型为Student的s1变量
    var s1 Student = Student{ //s1是Student类型的实例,所以其也可以调用Getname方法和Setname方法。
        Name: "s5",
        Age:  18,
    }

    name := s1.GetName() //执行时,s1传递给s,Getname方法中的s就相当于s1的拷贝,如果函数Getname()中有参数,就和函数传参是一样的了。
    fmt.Printf("name=%s
", name)

    //(&s1).SetName("s2") //正规写法
    s1.SetName("s2") //这里直接写s1而不是&s1,是因为Setname方法发现student是一个指针类型,所以在s1这里,go语言就帮我们自动做了取址,这也就是为什么不会报错的原因。
    //此处SetName有参数,s2,所以其就传递给SetName(name string)的name,所以name就是s2了。
    //SetName方法的s相当于s1的拷贝,只不过这次传递的是内存地址
    name = s1.GetName()
    fmt.Printf("name=%s
", name)

    s1.Print0() //调用main包的Print0方法
}

util.go内容如下:

package main

import (
    "fmt"
)

//定义一个指针类型的Print0方法
func (s *Student) Print0() {
    fmt.Printf("student:%#v", *s)
}

执行结果如下图所示:

 

注意:写项目时尽量将同一个功能的类和方法放在一个文件中,这样比较好维护,如果都混搭在一块,不仅混乱还难维护。

二、 值类型和指针类型

2.1 指针类型和值类型作为接受者的区别?

值类型可以作为接受者,同样指针类型也可以作为接受者,如下图所示:

  

具体区别

1、值类型是要拷贝的是结构体字段值的副本,针对该副本值的修改,原有结构体里面字段的值的修改是不会生效的;

2、要修改结构体里面的字段,必须要用指针类型(指针类型拷贝的是内存地址)

2.2 什么时候用值类型/指针类型作为接受者?

A. 需要修改接受者中的值的时候

B. 接受者是大对象的时候,副本拷贝代价比较大

C. 一般来说,通常使用指针类型作为接受者

下面我们来通过一个实例来进行验证当值较大时,值类型和指针类型的耗时,进而验证上述理论

当接受者为值类型

package main

import (
    "fmt"
    "time"
)

type User struct {
    s1 [10000000]int64
    s2 [10000000]int64
    s3 [10000000]int64
    s4 [10000000]int64
}

func (u User) Set() {
    for i := 0; i < len(u.s1); i++ {
        u.s1[i] = 1
        u.s2[i] = 1
        u.s3[i] = 1
        u.s4[i] = 1
    }
}

func main() {
    var u *User = new(User) //结构体初始化

    start := time.Now().UnixNano()
    u.Set()
    end := time.Now().UnixNano()

    fmt.Printf("cost:%d ns", (end-start)/1000)
}

执行结果:

当接受者为指针类型

package main

import (
    "fmt"
    "time"
)

type User struct {
    s1 [10000000]int64
    s2 [10000000]int64
    s3 [10000000]int64
    s4 [10000000]int64
}

func (u *User) Set() {
    for i := 0; i < len(u.s1); i++ {
        u.s1[i] = 1
        u.s2[i] = 1
        u.s3[i] = 1
        u.s4[i] = 1
    }
}

func main() {
    var u *User = new(User) //结构体初始化

    start := time.Now().UnixNano()
    u.Set()
    end := time.Now().UnixNano()

    fmt.Printf("cost:%d ns", (end-start)/1000)
} 

执行结果如下:

  

结论:可以发现接受者作为指针类型耗时明显少于接受者作为值类型。

三、面向对象和继承

3.1 匿名结构体与继承

之前已经学会了数据的继承,接下来我们来学习一下方法的继承。

注意:继承并不是一个实例的继承,而是一个类型的继承。

正常的匿名结构体数据继承:

type Animal struct {
    Name string
}

type People struct {
    Sex string
    Age int
    Animal //or *Animal
}

方法的继承示例如下:

package main

import (
    "fmt"
)

type Animal struct {
    Name string
    Age  int
}

func (a *Animal) SetName(name string) {
    a.Name = name
}

func (a *Animal) SetAge(age int) {
    a.Age = age
}

func (a *Animal) Print() {
    fmt.Printf("a.name=%s a.age=%d
", a.Name, a.Age)
}

type Birds struct {
    *Animal //继承Animal,正常是a *Animal 那么a如果要用a里面的字段就需要初始化,现在我们是匿名字段,所以字段名和类型名就都是*Animal,所以如果下面要使用的话,必须要初始化
    //指针类型要使用必须要初始化
}

func (b *Birds) Fly() {
    fmt.Printf("name %s is fly
", b.Name) //使用继承的Animal中的Name字段
}

func main() {
    var b *Birds = &Birds{
        Animal: &Animal{}, //针对结构体指针进行初始化(分配内存),不初始化,就是一个空内存地址,程序就会崩溃,字段名是继承的Animal,初始化值为Animal类型所在的内存地址。
    }
    b.SetName("bird") //SetName是Animal的方法,Bird继承了Setname,所以其也有了其父类bird的方法
    b.SetAge(18)      //SetAge同理

    b.Fly() //SetFly同理
    b.Print()
}

执行结果如下图所示:

注意:继承并不是一个实例的继承,而是一个类型的继承

比如说如下这个例子,环境还是上述的例子:

    var a Animal
    var b Birds
    a.Name = "birds"

    fmt.Printf("b.Name:%s
",b)

解释:Birds继承Animal类型中的Name和Age字段,a和b是两个不同变量,继承仅仅是类型的继承,并不是实例的继承,a和b是两个相互独立的东西,相互不影响的不同实例,不能说b的Birds继承a的Animal,b就有了a中的东西,b只是有了a中的字段(Name和Age),也就是继承了a的类型,具体实例化时还要为其赋值,所以a.Name为birds,b.Name肯定不是birds了

3.2 多重继承与冲突解决

不推荐多重继承

type Mother struct {
    Name string
}

type Father struct {
    Name string
}

type People struct {
    Sex string
    Age int
    *Mother
    *Father
}

比如说对于继承来说,Mother和Father中都有Name字段,而People又继承了Mother和Father类型,所以要引用Mother中的Name字段就要写全了。比如:.Mother.Name.

多重继承的话,方法也是一样,调取路径写全即可调用,但是多重继承不推荐,建议少用

四、结构体和json序列化

4.1 序列化

结构体序列化:结构体转成json数据格式

package main

import (
    "encoding/json"
    "fmt"
)

type Animal struct {
    Name string
    Age  int
}

func (a *Animal) SetName(name string) {
    a.Name = name
}

func (a *Animal) SetAge(age int) {
    a.Age = age
}

func (a *Animal) Print() {
    fmt.Printf("a.name=%s a.age=%d
", a.Name, a.Age)
}

type Birds struct {
    *Animal //继承Animal,正常是a *Animal 那么a如果要用a里面的字段就需要初始化,现在我们是匿名字段,所以字段名和类型名就都是*Animal,所以如果下面要使用的话,必须要初始化
    //指针类型要使用必须要初始化
}

func (b *Birds) Fly() {
    fmt.Printf("name %s is fly
", b.Name) //使用继承的Animal中的Name字段
}

func main() {
    var b *Birds = &Birds{
        Animal: &Animal{}, //针对结构体指针进行初始化(分配内存),不初始化,就是一个空内存地址,程序就会崩溃,字段名是继承的Animal,初始化值为Animal类型所在的内存地址。
    }
    b.SetName("bird") //SetName是Animal的方法,Bird继承了Setname,所以其也有了其父类bird的方法
    b.SetAge(18)      //SetAge同理

    b.Fly() //SetFly同理
    b.Print()

     //json.Marshal是序列化的方法
    data, err := json.Marshal(b) //序列化成json时,会返回一个byte数组(其中值为json序列化后的值),和error,字节数组可以返回到一个文件中,或者返回给调用方,调用方在反序列化为其支持的数据类型,这样我们不同语言写的程序就可以通信了。
    fmt.Printf("marshal result:%s err:%v
", string(data), err)
}

执行结果如下图所示:

4.2 反序列化

结构体反序列化: json数据格式转成结构体

就是把上述例子中的序列化后的json数据序列化为结构体

package main

import (
    "encoding/json"
    "fmt"
)

type Animal struct {
    Name string
    Age  int
}

func (a *Animal) SetName(name string) {
    a.Name = name
}

func (a *Animal) SetAge(age int) {
    a.Age = age
}

func (a *Animal) Print() {
    fmt.Printf("a.name=%s a.age=%d
", a.Name, a.Age)
}

type Birds struct {
    *Animal //继承Animal,正常是a *Animal 那么a如果要用a里面的字段就需要初始化,现在我们是匿名字段,所以字段名和类型名就都是*Animal,所以如果下面要使用的话,必须要初始化
    //指针类型要使用必须要初始化
}

func (b *Birds) Fly() {
    fmt.Printf("name %s is fly
", b.Name) //使用继承的Animal中的Name字段
}

func main() {
    var b *Birds = &Birds{
        Animal: &Animal{}, //针对结构体指针进行初始化(分配内存),不初始化,就是一个空内存地址,程序就会崩溃,字段名是继承的Animal,初始化值为Animal类型所在的内存地址。
    }
    b.SetName("bird") //SetName是Animal的方法,Bird继承了Setname,所以其也有了其父类bird的方法
    b.SetAge(18)      //SetAge同理

    b.Fly() //SetFly同理
    b.Print()

    data, err := json.Marshal(b)
    fmt.Printf("marshal result:%s err:%v
", string(data), err)

    var c Birds //定义一个新结构体c,因为反序列后就是给c
    //反序列化用json.Unmarshal方法
    err = json.Unmarshal(data, &c) //需要传入第一个json字符数组,第二个是一个空的接口(就是反序列化后谁来接受),也就是任意类型都可以(因为我们之后要反序列化后的格式并不确定,各种类型都有可能),并且这里必须要传入地址&c,因为Birds结构体是值类型,要想真正修改c,必须要传地址。
    fmt.Printf("c:%#v, err:%v
", c.Animal, err)
}

 执行结果如下图所示:

 

原文地址:https://www.cnblogs.com/forever521Lee/p/9371304.html