Bootstrap

(六)Go------自定义类型type,结构体struct

一. 自定义类型 type

1. 自定义类型

在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

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

通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

2. 类型别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。最终指向的是本人

type TypeAlias = Type

rune和byte就是类型别名

type byte = uint8
type rune = int32

3. 类型定义和类型别名的区别

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf("type of a : %T\n", a) //type of a:main.NewInt
    fmt.Printf("type of b : %T\n", b) //type of b:int
}

//结果
type of a : main.NewInt
type of b : int

a的类型是main.NewInt,表示main包下定义的NewInt类型。
b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
也就是自定义类型最终输出的类型为自定义的类型,而别名最终为原始类型

二. 结构体

什么是结构体?

数组与切片,只能存储同一类型的变量
若要存储多个类型的变量,就需要用到结构体,它是将多个任意类型的变量组合在一起的聚合数据类型

每个变量都成为该结构体的成员变量。

可以理解为 Go语言 的结构体struct和其他语言的class有相等的地位,但是Go语言放弃大量面向对象的特性,所有的Go语言类型除了指针类型外,都可以有自己的方法,提高了可扩展性

Go 语言没有 class 类的概念,只有 struct 结构体的概念,因此也没有继承

1. 定义结构体

声明结构体

type 结构体名 struct {
    属性名   属性类型
    属性名   属性类型
    ...
}

比如我要定义一个可以存储个人资料名为 Profile 的结构体,可以这么写

type Profile struct {
    name   string
    age    int
    gender string
    mother *Profile // 指针
    father *Profile // 指针
}

若相邻的属性(字段)是相同类型,可以合并写在一起

type Profile struct {
    name,gender   string
    age    int
    mother *Profile // 指针
    father *Profile // 指针
}

或者

 var user struct{Name string; Age int}

结构体初始化

最常用的方式

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p1 person
    p1.name = "qcq.cn"
    p1.city = "北京"
    p1.age = 18
    fmt.Printf("p1=%v\n", p1)  //p1={qcq.cn 北京 18}
    fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"qcq.cn", city:"北京", age:18}
}

1. 直接定义

通过结构体可以定义一个组合字面量,有几规则

规则一:当最后一个字段和结果不在同一行时 不可省略。

p1 := person{
    name: "qcq.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"qcq.cn", city:"北京", age:18}

反之,不在同一行,就可以省略。

xm := Profile{
    name: "小明",
    age: 18,
    gender: "male"}

规则二:字段名要么全写,要么全不写,不能有的写,有的不写。

例如下面这种写法是会报 mixture of field:value and value initializers

错误

xm := Profile{
    name: "小明",
    18,
    "male",
}

2 . 使用值的列表初始化

字段名 全都 不写

p8 := &person{
    "qcq.cn",
    "北京",
    18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"qcq.cn", city:"北京", age:18}

需要注意

1.必须初始化结构体的所有字段
2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用

3. 结构体指针初始化

全部写 对结构体指针进行键值对初始化,例如:

p6 := &person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18}

规则三:初始化结构体,并不一定要所有字段都赋值,未被赋值的字段,会自动赋值为其类型的零值
必须指定字段名才可以赋值部分字段。

xm := Profile{name: "小明"}
fmt.Println(xm.age)
// output: 0

否则会报错

# command-line-arguments
./demo.go:19:51: too few values in Profile literal

4. 结构体初始化

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p4 person
    fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}

5. 结构体内存布局

可以看出结构体再内存中是连续的

type test struct {
    a int8
    b int8
    c int8
    d int8
}
n := test{
    1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
输出:

    n.a 0xc0000a0060
    n.b 0xc0000a0061
    n.c 0xc0000a0062
    n.d 0xc0000a0063

面试题

type student struct {
    name string
    age  int
}

func main() {
	//这个字典的,值为结构体student
    m := make(map[string]*student)
    stus := []student{
        {name: "qcq", age: 22},
        {name: "测试", age: 30},
        {name: "博客", age: 38},
    }
//map无序,所以结果有多种可能
    for _, stu := range stus {
    //相当于qcq=qcq,22
    //&stu是将 该地址的内容赋值
        m[stu.name] = &stu
    }
    
    for k, v := range m {
        fmt.Println(k, "=>", v.name)
    }
}

二. 结构体方法

1. 如何定义一个结构体方法

不能在结构体内定义方法
可以用组合函数的方式来定义结构体方法。

type Profile struct {
    name   string
    age    int
    gender string
    mother *Profile // 指针
    father *Profile // 指针
}

//方法
func (person Profile) FmtProfile() {
    fmt.Printf("名字:%s\n", person.name)
    fmt.Printf("年龄:%d\n", person.age)
    fmt.Printf("性别:%s\n", person.gender)
}

其中FmtProfile 是方法名,而(person Profile) :表示将 FmtProfile 方法与 Profile 的实例绑定。
我们把 Profile 称为方法的接收者,而 person 表示实例本身
,在方法内可以使用 person.属性名 的方法来访问实例属性。

package main

import "fmt"

// 定义一个名为Profile 的结构体
type Profile struct {
    name   string
    age    int
    gender string
    mother *Profile // 指针
    father *Profile // 指针
}

// 定义一个与 Profile 的绑定的方法
func (person Profile) FmtProfile() {
    fmt.Printf("名字:%s\n", person.name)
    fmt.Printf("年龄:%d\n", person.age)
    fmt.Printf("性别:%s\n", person.gender)
}

func main() {
    // 实例化
    myself := Profile{name: "小明", age: 24, gender: "male"}
    // 调用函数
    myself.FmtProfile()
}

//输出如下
名字:小明
年龄:24
性别:male
//Person 结构体
type Person struct {
    name string
    age  int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
    return &Person{
        name: name,
        age:  age,
    }
}

//Dream Person做梦的方法
func (p Person) Dream() {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
    p1 := NewPerson("测试", 25)
    p1.Dream()
}

2. 方法的参数传递方式(重点)

当你想要在方法内改变实例的属性的时候,必须使用指针做为方法的接收者

package main

import "fmt"

// 声明一个 Profile 的结构体
type Profile struct {
    name   string
    age    int
    gender string
    mother *Profile // 指针
    father *Profile // 指针
}

// 重点在于这个星号: *, 说明已经有一个struct对象实例化了
//是 再次基础上做修改
func (person *Profile) increase_age() {
    person.age += 1
}

func main() {
    myself := Profile{name: "小明", age: 24, gender: "male"}
    fmt.Printf("当前年龄:%d\n", myself.age)
    myself.increase_age()
    fmt.Printf("当前年龄:%d", myself.age)
}
//结果
当前年龄:24
当前年龄:25

可以看到在方法内部对 age 的修改已经生效。你可以尝试去掉 *,使用值做为方法接收者,看看age是否会发生改变(答案是:不会改变

至此,我们知道了两种定义方法的方式:

  • 做为方法接收者

  • 指针做为方法接收者

那我们如何进行选择呢?

  • 直接使用指针做为方法的接收者。
  1. 你需要在方法内部改变结构体内容的时候
  2. 出于性能的问题,当结构体过大的时候(相当于,组合)
  • 有些情况下,以值或指针做为接收者都可以,但是考虑到代码一致性,建议都使用指针做为接收者

指针类型的接收者

    // SetAge 设置p的年龄
    // 使用指针接收者
    func (p *Person) SetAge(newAge int8) {
        p.age = newAge
    }
调用该方法:

func main() {
    p1 := NewPerson("测试", 25)
    fmt.Println(p1.age) // 25
    p1.SetAge(30)
    fmt.Println(p1.age) // 30
}

4. 结构体实现 “继承”

Go 语言本身并不支持继承

可以使用组合的方法,实现类似继承的效果。

组合:比如一台电脑,是由机身外壳,主板,CPU,内存等零部件组合在一起,最后才有了我们用的电脑。

在 Go 语言中,把一个结构体嵌入到另一个结构体的方法,称之为组合。

现在这里有一个表示公司(company)的结构体,还有一个表示公司职员(staff)的结构体。

type company struct {
    companyName string
    companyAddr string
}

type staff struct {
    name string
    age int
    gender string
    position string
}

若要将公司信息与公司职员关联起来,一般都会想到将 company 结构体的内容照抄到 staff 里。

借鉴继承的思想,我们可以将公司的属性都“继承”过来。

但是在 Go 中没有类的概念,只有组合
可以将 company 这个 结构体嵌入 staff 中,做为 staff 的一个匿名字段,staff 就直接拥有了 company 的所有属性了。

type staff struct {
    name string
    age int
    gender string
    position string
    company   // 匿名字段
//  cpmpay  compay // 嵌套
}

验证一下。

package main

import "fmt"

type company struct {
    companyName string
    companyAddr string
}

type staff struct {
    name string
    age int
    gender string
    position string
    company
}

func main()  {
    myCom := company{
        companyName: "Tencent",
        companyAddr: "北京市",
    }
    staffInfo := staff{
        name:     "小明",
        age:      28,
        gender:   "男",
        position: "云计算工程师",
        company: myCom,
    }
/*
	 user1 := User{
        Name:   "pprof",
        Gender: "女",
        Address: Address{
            Province: "陕西",
            City:     "西安",
        },
    }

*/

    fmt.Printf("%s 在 %s 工作\n", staffInfo.name, staffInfo.companyName)
    fmt.Printf("%s 在 %s 工作\n", staffInfo.name, staffInfo.company.companyName)
}
结果,可见staffInfo.companyName 和 staffInfo.company.companyName 的效果是一样的。

小明 在 Tencent 工作
小明 在 Tencent 工作

嵌套类型
嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段

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

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

func main() {
    var user2 User
    user2.Name = "pprof"
    user2.Gender = "女"
    user2.Address.Province = "陕西"    //通过匿名结构体.字段名访问
    user2.City = "西安"                //直接访问匿名结构体的字段名
    fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}

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

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

func (a *Animal) move() {
    fmt.Printf("%s会动!\n", a.name)
}

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

func (d *Dog) wang() {
    fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{ //注意嵌套的是结构体指针
            name: "乐乐",
        },
    }
    d1.wang() //乐乐会汪汪汪~
    d1.move() //乐乐会动!
}

5. 内部方法与外部方法(大小写)

函数名的首字母大小写非常重要,它被来实现控制对方法的访问权限。

  • 当方法的首字母为大写时,这个方法对于所有包都是Public,其他包可以随意调用

  • 当方法的首字母为小写时,这个方法是Private,其他包是无法访问

6. 三种实例化方法

第一种:正常实例化

func main() {
    xm := Profile{
        name: "小明",
        age: 18,
        gender: "male",
    }
}

第二种:使用 new

func main() {
    xm := new(Profile)
    // 等价于: var xm *Profile = new(Profile)
    fmt.Println(xm)
    // output: &{ 0 }

    xm.name = "iswbm"   // 或者 (*xm).name = "iswbm"
    xm.age = 18     //  或者 (*xm).age = 18
    xm.gender = "male" // 或者 (*xm).gender = "male"
    fmt.Println(xm)
    //output: &{iswbm 18 male}
}

第三种:使用 &

func main() {
    var xm *Profile = &Profile{}
    fmt.Println(xm)
    // output: &{ 0 }

    xm.name = "iswbm"   // 或者 (*xm).name = "iswbm"
    xm.age = 18     //  或者 (*xm).age = 18
    xm.gender = "male" // 或者 (*xm).gender = "male"
    fmt.Println(xm)
     //output: &{iswbm 18 male}
}

7. 选择器的冷知识

从一个结构体实例对象中获取字段的值,通常都是使用 . 这个操作符,该操作符叫做 选择器

当你对象是结构体对象的指针时,你想要获取字段属性时,按照常规理解应该这么做

type Profile struct {
    Name string
}

func main() {
	//这里直接将p1定义为指针类型
    p1 := &Profile{"iswbm"}
  	fmt.Println((*p1).Name)  // output: iswbm
}

有一个更简洁的做法,可以直接省去 * 取值的操作,选择器 . 会直接解引用,示例如下

type Profile struct {
    Name string
}

func main() {
    p1 := &Profile{"iswbm"}
    fmt.Println(p1.Name)  // output: iswbm
}

8. 结构体与JSON序列化

JSON是一种轻量级的数据交换格式
JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

//Student 学生
type Student struct {
    ID     int
    Gender string
    Name   string
}

//Class 班级
type Class struct {
    Title    string
    Students []*Student
}

func main() {
    c := &Class{
        Title:    "101",
        Students: make([]*Student, 0, 200),
    }
    for i := 0; i < 10; i++ {
        stu := &Student{
            Name:   fmt.Sprintf("stu%02d", i),
            Gender: "男",
            ID:     i,
        }
        c.Students = append(c.Students, stu)
    }
    //JSON序列化:结构体-->JSON格式的字符串
    data, err := json.Marshal(c)
    if err != nil {
        fmt.Println("json marshal failed")
        return
    }
    fmt.Printf("json:%s\n", data)
    //JSON反序列化:JSON格式的字符串-->结构体
    str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
    c1 := &Class{}
    err = json.Unmarshal([]byte(str), c1)
    if err != nil {
        fmt.Println("json unmarshal failed!")
        return
    }
    fmt.Printf("%#v\n", c1)
}

9. 结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

key1:"value1" key2:"value2"

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,双引号括起来。键值对之间使用一个空格分隔

注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

//Student 学生
type Student struct {
    ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
    Gender string //json序列化是默认使用字段名作为key
    name   string //私有不能被json包访问
}

func main() {
    s1 := Student{
        ID:     1,
        Gender: "女",
        name:   "pprof",
    }
    data, err := json.Marshal(s1)
    if err != nil {
        fmt.Println("json marshal failed!")
        return
    }
    fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
}

10. map类型的结构体

package main

import "fmt"

type student struct {
    id   int
    name string
    age  int
}

func main() {
    ce := make(map[int]student)
    ce[1] = student{1, "xiaolizi", 22}
    ce[2] = student{2, "wang", 23}
    fmt.Println(ce)
    //如何删除
    delete(ce, 2)
    fmt.Println(ce)
}

实现map有序输出

(面试经常问到)

package main

import (
    "fmt"
    "sort"
)

func main() {
    map1 := make(map[int]string, 5) 
    map1[1] = "www.topgoer.com"
    map1[2] = "rpc.topgoer.com"
    map1[0] = "xiaohuang"
    map1[5] = "qcqly"
    map1[3] = "xiaohong"
 
    sli := []int{}
    for k, _ := range map1 {
        sli = append(sli, k)
    }
    sort.Ints(sli)
    for i := 0; i < len(map1); i++ {
        fmt.Println(map1[sli[i]])
    }
}

思考



package main

import "fmt"

type student struct {
    id   int
    name string
    age  int
}

func demo(ce []student) {
    //切片是引用传递,是可以改变值的
    ce[1].age = 999
    // ce = append(ce, student{3, "xiaowang", 56})
    // return ce
}
func main() {
    var ce []student  //定义一个切片类型的结构体
    ce = []student{
        student{1, "xiaoming", 22},
        student{2, "xiaozhang", 33},
    }
    fmt.Println(ce)
    demo(ce)
    fmt.Println(ce)
}

匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体

package main

import (
    "fmt"
)

func main() {
    var user struct{Name string; Age int}
    user.Name = "qcq.cn"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}

1. 创建指针类型结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

var p2 = new(person)
fmt.Printf("%T\n", p2)     //*main.person
fmt.Printf("p2=%#v\n", p2) 
//结果
//p2=&main.person{name:"", city:"", age:0}

从打印的结果中我们可以看出p2是一个结构体指针

在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

var p2 = new(person)
p2.name = "测试"
p2.age = 18
p2.city = "北京"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18}

2. 取结构体的地址实例化

使用&对结构体进行取地址操作相当于**对该结构体类型进行了一次new实例化**操作。

p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "qcq"
p3.age = 21
p3.city = "西安"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"qcq", city:"西安", age:21}

p3.name = "qcq"其实在底层是(*p3).name = "qcq"
这是Go语言帮我们实现的语法糖。

;