LOADING

正在加载

Go基础学习(捌)——假“类”

壹 介绍

在结构体中我们说过,在Go语言中没有类的概念,也不支持类的继承等面向对象的概念,那为什么这里会又有个类呢?因为在Go语言中,我们可以通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性,所以这里说的类其实是Go语言的结构体的内嵌和接口。

注意:Go语言没有类这个概念,只是因为这部分内容有点像其他编程语言的类概念而已。

贰 构造函数

那么既然有这个假“类”,那么就要有构造函数,但事实是Go语言是没有面向对象这个概念的,也就没有真正的构造函数,不过这时候我们可以自己实现一个构造函数。

在定义结构体的时候,我们要对结构体进行初始化,如果不初始化的话,那么结构体存储的都是空值,一个方法是直接赋值,另一个方法是使用构造函数,当然这不是让我们决定使用构造函数的原因。使用构造函数的根本原因是因为它可以确保必须执行的任何初始化逻辑均在使用该假”类“之前执行

那么如何定义一个构造方法?例如,下方有一个student的结构体,然后分别实现一个普通结构体类型的构造函数newStudent1和实现一个结构体指针类型的构造函数newStudent2,其逻辑就是直接返回一个结构体的类型出去。

func 构造函数名(初始化的数据 初始化数据的类型)返回值类型{
    return 结构体类型{
        结构体属性1: 初始化数据1,
        结构体属性2: 初始化数据2,
        ...
    }
}
// 这里需要说明,返回值类型一定是结构体类型,而且不会定义变量名,否则初始化不了结构体的属性
// 其实就和普通的函数定义一样,不过返回的是结构体类型

接着我们可看到下面的例子,结构体指针类型构造函数占用的内存比普通结构体类型构造函数占用的内存小,这是因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以在某些情况下建议构造函数返回的是结构体指针类型。

package main
import (
    "fmt"
    "unsafe"
)
// 定义一个student结构体
type Student struct {
    name string
    age  int
}
// 定义一个newStudent1的普通结构体类型构造函数
func NewStudent1(name string, age int) Student {
    return Student{
        name: name,
        age:  age,
    }
}
// 定义一个newStudent2的结构体指针类型构造函数
func NewStudent2(name string, age int) *Student {
    return &Student{
        name: name,
        age:  age,
    }
}
func main() {
    // 实例化newStudent1
    stu1 := NewStudent1("A7", 10)
    fmt.Printf("stu1数据:%#v\tstu1占的内存大小:%v\n", stu1, unsafe.Sizeof(stu1)) //stu1数据:main.Student{name:"A7", age:10}       stu1占的内存大小:24
    // 实例化NewStudent2
    stu2 := NewStudent2("A7cc", 20)
    fmt.Printf("stu2数据:%#v\tstu2占的内存大小:%v\n", stu2, unsafe.Sizeof(stu2)) //stu2数据:&main.Student{name:"A7cc", age:20}    stu2占的内存大小:8
}

在定义结构体类型时,尽量第一个字母大写,而构造函数的话,尽量以New+结构体名进行命名,如:NewText

叁 方法和接收者

3.1 初识方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}
// 接收者变量:接收者中的参数变量名在命名时
// 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
// 方法名、参数列表、返回参数:具体格式与函数定义相同。

个人感觉普通函数前面可能是默认前面有接收者变量,方法就是特殊的函数!!!(这是个人见解,这样好理解,但不代表官方)

下面是官方的:官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,student类型的接收者变量应该命名为 sConnector类型的接收者变量应该命名为c等。方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

举个例子:

package main

import (
    "fmt"
)

// Student 结构体
type Student struct {
    name string
    age  int
}
//NewStudent 构造函数
func NewStudent(name string, age int) *Student {
    return &Student{
        name: name,
        age:  age,
    }
}
// Student的Dream方法
func (s Student) Dream() {
    fmt.Printf("s的地址是:%p\n", &s)           // s的地址是:0xc000098078
    fmt.Printf("s.name的地址是:%p\n", &s.name) // s.name的地址是:0xc000098078
    fmt.Printf("s.age的地址是:%p\n", &s.age)   // s.age的地址是:0xc000098088
    fmt.Printf("%s的梦想是学好Go语言!\n", s.name)  // A7cc的梦想是学好Go语言!
    s.age = 19
}
func main() {
    stu1 := NewStudent("A7cc", 18)
    stu1.Dream()
    fmt.Printf("stu1的地址是:%p\n", &stu1)           // stu1的地址是:0xc0000d8018
    fmt.Printf("stu1.name的地址是:%p\n", &stu1.name) // stu1.name的地址是:0xc000098060
    fmt.Printf("stu1.age的地址是:%p\n", &stu1.age)   // stu1.age的地址是:0xc000098070
    fmt.Printf("stu1.age=%v", stu1.age)          // stu1.age=18
}

根据上面的结果可以看到,传递的接收者变量地址与传入的值的地址不一样。这是因为,传递的接收者变量的类型不是一个指针类型,而是非指针类型或者说是值类型,我们都知道值类型是直接复制一份一模一样的值到另一个内存中,所以此时地址也就不一样了,具体指针类型接收者与值类型接收者介绍在下面小节。

3.2 指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Student添加一个SetAge方法,来修改实例变量的年龄。

// SetAge1 设置s的年龄
// 使用指针接收者
//Dream Student做梦的方法
func (s *Student) SetAge1(newAge int) {
    fmt.Printf("s的地址是:%p\n", &s)           // s的地址是:0xc000006038
    fmt.Printf("s.name的地址是:%p\n", &s.name) // s.name的地址是:0xc000004078
    fmt.Printf("s.age的地址是:%p\n", &s.age)   // s.age的地址是:0xc000004088
    s.age = newAge
}

func main() {
    stu1 := NewStudent("A7cc", 18)
    fmt.Println(stu1.age)
    stu1.SetAge1(26)
    fmt.Printf("stu1的地址是:%p\n", &stu1)           // stu1的地址是:0xc000006028
    fmt.Printf("stu1.name的地址是:%p\n", &stu1.name) // stu1.name的地址是:0xc000004078
    fmt.Printf("stu1.age的地址是:%p\n", &stu1.age)   // stu1.age的地址是:0xc000004088
    fmt.Printf("stu1.age=%v", stu1.age)          // stu1.age=26
}

可以看到,其结构体的属性地址在传入方法后是一样的,所以SetAge1方法最后是将age=18改变成了age=26,(以下是个人理解)那么为什么结构体的地址却不一样?因为在方法中并没有进行结构体本身的指针传递,那么就只能将其复制一份属性地址相同但本身地址不同的副本给方法了。

3.3 值类型的接收者

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

// SetAge2 设置p的年龄
// 使用值接收者
//Dream Student做梦的方法
func (s Student) SetAge2(newAge int) {
    fmt.Printf("s的地址是:%p\n", &s)           // s的地址是:0xc000098078
    fmt.Printf("s.name的地址是:%p\n", &s.name) // s.name的地址是:0xc000098078
    fmt.Printf("s.age的地址是:%p\n", &s.age)   // s.age的地址是:0xc000098088
    s.age = newAge
}
func main() {
    stu1 := NewStudent("A7cc", 18)
    fmt.Println(stu1.age)
    stu1.SetAge2(26)
    fmt.Printf("stu1的地址是:%p\n", &stu1)           // stu1的地址是:0xc0000d8018
    fmt.Printf("stu1.name的地址是:%p\n", &stu1.name) // stu1.name的地址是:0xc000098060
    fmt.Printf("stu1.age的地址是:%p\n", &stu1.age)   // stu1.age的地址是:0xc000098070
    fmt.Printf("stu1.age=%v", stu1.age)          // stu1.age=18
}

由于是值传递,是复制一份副本,所以使用SetAge2方法不能改变age的值。

3.4 什么时候应该使用指针类型接收者

  • a.需要修改接收者中的值
  • b.接收者是拷贝代价比较大的大对象
  • c.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

肆 任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。就是说像intfloat32boolstring这些在别的包已经定义的类型不可以定义方法,但是我们可以使用type对其进行创建一个新的类型,再添加方法。

//MyInt 将int定义为自定义MyInt类型
type MyInt int

// 我们可以看到通过type可以自定义一个类型
func (a MyInt) text1(top1 int) {
    fmt.Printf("MyInt本质是一个int类型,我们传递了参数:%v", top1)
}
// 如果将下面注释的方法定义使用,会报错,原因就是因为非本地类型不能定义方法
// func (b int) text2(top2 int) {
// 	fmt.Printf("int类型不能添加方法,我们传递了参数:%v", top2)
// }

func main() {
    var val1 MyInt
    // 注意需要定义一个MyInt型变量才能调用该类型变量的方法
    val1.text1(1)
    // var val2 int
    // val2.text2(2)
}

注意:需要定义一个MyInt型变量才能调用该类型变量的方法。

伍 结构体的匿名字段

结构体允许其成员字段在声明时没有字段名(属性名)而只有类型,这种没有名字的字段就称为匿名字段。个人不建议使用,增加了代码阅读量,也容易导致代码重复。

注意:这里匿名字段的说法并不代表没有字段名(属性名),而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个,而其他同类型必须要有字段名(属性名)进行区分。

// Student 结构体
type Student struct {
    string
    class  string
    number int
    int
}
func main() {
    stu1 := Student{
        "A7cc",
        "六年三班",
        12,
        18,
    }
    fmt.Printf("%#v\n", stu1) // main.Student{string:"A7cc", class:"六年三班", number:12, int:18}
    fmt.Printf("名字是:%v,学号是%v,班级是:%v,年龄是%v", stu1.string, stu1.number, stu1.class, stu1.int) // 名字是:A7cc,学号是12,班级是:六年三班,年龄是18
}

陆 嵌套结构体

6.1 基础的嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针,就像下面的示例代码那样。

package main

import (
    "fmt"
)

//Address 地址结构体
type Address struct {
    Province string // 省份
    City     string // 城市
}

//User 用户结构体
type User struct {
    Name   string  // 名字
    Gender string  // 性别
    Addres Address // 地址,引用了Address结构体,也可以引用结构体指针
}

func main() {
    user1 := User{
        Name:   "A7cc",
        Gender: "男",
        Addres: Address{
            Province: "陕西",
            City:     "西安",
        },
    }
    fmt.Printf("user1=%#v\n", user1) //user1=main.User{Name:"A7cc", Gender:"男", Address:main.Addres{Province:"陕西", City:"西安"}}
    user1.Addres.Province = "四川"     // 访问Addres的字段时需要加上Addres
    user1.Addres.City = "成都"
    fmt.Printf("user1=%#v\n", user1) // user1=main.User{Name:"A7cc", Gender:"男", Address:main.Address{Province:"四川", City:"成都"}}
}

6.2 嵌套匿名字段

上面user结构体中嵌套的Address结构体也可以采用匿名字段的方式,例如:

//Address 地址结构体
type Address struct {
    Province string // 省份
    City     string // 城市
}

//User 用户结构体
type User struct {
    Name    string // 名字
    Gender  string // 性别
    Address        // 地址,引用了Address结构体,这里也可以是结构体指针,范围的话,直接用.即可,但是匿名只能是一个结构体或者是结构体指针,因为类型名只能给其中一个
}

func main() {
    user1 := User{
        Name:   "A7cc",
        Gender: "男",
        Address: Address{ // 由于省略了类型,使用字段名就是类型名
            Province: "陕西",
            City:     "西安",
        },
    }
    fmt.Printf("user1=%#v\n", user1) //user1=main.User{Name:"A7cc", Gender:"男", Address:main.Address{Province:"陕西", City:"西安"}}
    user1.Address.Province = "四川"    // 匿名字段默认使用类型名作为字段名
    user1.City = "成都"                // 可以看到再访问匿名字段时可以省略结构体类型,但是字段名有名字时不可以省略,如上面那个例子,这是因为当访问结构体成员时会先在结构体中查找该字段名,如果找不到就会去嵌套的匿名字段中查找
    fmt.Printf("user1=%#v\n", user1) // user1=main.User{Name:"A7cc", Gender:"男", Address:main.Address{Province:"四川", City:"成都"}}

当访问结构体成员时会先在结构体中查找该字段名,如果找不到就会去嵌套的匿名字段中查找,所以建议使用具体的内嵌结构体字段名。

6.3 嵌套结构体的字段名冲突

那么现在有个问题如果一个结构体嵌套了两个结构体,并且这两个结构体的字段名存在相同的名字,即嵌套结构体内部存在相同的字段名。所以在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。

//Address 地址结构体
type Address struct {
    Province   string
    City       string
    CreateTime string
}

//Email 邮箱结构体
type Email struct {
    Account    string
    CreateTime string
}

//User 用户结构体
type User struct {
    Name   string
    Gender string
    Address
    Email
}

func main() {
    var user1 User
    user1.Name = "A7cc"
    user1.Gender = "男"
    // user1.CreateTime = "2019"
    user1.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
    user1.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
}

柒 结构体的“继承”

Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

//Animal 动物
type Animal struct {
    name string
}

// 定义一个Animal的独有的方法move
func (a *Animal) move() {
    fmt.Printf("%s是动物,会动!\n", a.name)
}

// 定义一个Animal的方法dream
func (a *Animal) dream() {
    fmt.Printf("%s是动物,梦想是成为百兽之王!\n", a.name)
}

//Dog 狗
type Dog struct {
    Feet    int8
    *Animal //通过嵌套匿名结构体实现继承
}

// 定义一个Dog的独有的方法wang
func (d *Dog) wang() {
    fmt.Printf("%s是狗,会汪汪汪~\n", d.name)
}

// 定义一个Dog的方法dream,方法名与前面的Animal的dream重名
func (d *Dog) dream() {
    fmt.Printf("%s是狗,梦想是成为人类忠诚的朋友!\n", d.name)
}
func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{ //注意嵌套的是结构体指针
            name: "狗狗",
        },
    }
    d1.wang() // 狗狗是狗,会汪汪汪~
    d1.move() // 狗狗是动物,会动!
    d1.dream() // 狗狗是狗,梦想是成为人类忠诚的朋友!
    d1.Animal.dream() // 狗狗是动物,梦想是成为百兽之王!
}

可以看到结构体通过嵌套匿名结构体的方法实现继承,然后可以直接调用继承的结构体的方法,但是,当结构体A本身与结构体A继承的结构体B存在相同方法名时,会先调用A本身的方法,而不是B的方法。由此更验证了,当访问结构体成员或者方法时会先在结构体中查找该字段或者该方法,找不到再去嵌套的匿名字段中查找。

捌 结构体字段的可见性(需要注意)

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问,其他的包不可以访问)。可以看出,结构体真的很像类。

package main

import (
    // 这个是Go语言的序列化库
    "encoding/json"
    "fmt"
)

//Student 学生
type Student struct {
    ID     int
    Gender string
    name   string	// 私有不能被json包访问
}

func main() {
    stu1 := Student{
        ID:     1,
        Gender: "六年级三班",
        name:   "A7cc",
    }
    // 将stu1进行序列化
    date, _ := json.Marshal(stu1)
    fmt.Printf("%s", date)	// {"ID":1,"Gender":"六年级三班"}
}

可以看到,name字段没有被json包访问成功。

玖 结构体和方法补充知识点(注意)

因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意,使用=复制的时候就是指针的传递。我们来看下面的例子:

type Person struct {
    name   string
    age    int8
    dreams []string
}

func (p *Person) SetDreams(dreams []string) {
    // 这里就是将dreams的切片地址赋值给p.dreams
    p.dreams = dreams
    // 真正的做法应该是像切片操作一样使用copy函数,即下面操作
    // 	p.dreams = make([]string, len(dreams))
    // 	copy(p.dreams, dreams)
}

func main() {
    p1 := Person{name: "小王子", age: 18}
    data := []string{"吃饭", "睡觉", "打豆豆"}
    p1.SetDreams(data)

    // 你真的想要修改 p1.dreams 吗?
    data[1] = "不睡觉"
    fmt.Println(p1.dreams)  // 会发现不但修改了data的值而且还修改了p1.dreams的值
}

同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。

avatar
小C&天天

修学储能 先博后渊


今日诗句