7.Go语言-结构体

1.类型别名和自定义类型

1.自定义类型

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

  • 自定义类型定义一个全新的类型,我们可以基于类型定义,也可以通过struct定义,例如:

    package main
    
    import "fmt"
    
    //自定义类型和类型别名示例
    
    //1.自定义类型
    
    //Myint基于int类型的自定义类型
    type MyInt int
    type YouInt MyInt
    func main() {
            var i MyInt
            fmt.Printf("%T  %v\n", i, i)
    }
    //main.MyInt  0
    

2.类型别名

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

  • 类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias和Type数=是一个类型,就像一个孩子小时候有小明。。

    type TypeAlias Type
    

2.结构体

  • 因Go语言中没有"类"的概念,也不支持"类"的继承等面向对象的概念,Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

  • Go语言中基础数据类型可以表示一些事务的基本属性,但是当我们向表达一个事务的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称称为struct,也就是我们可以通过struct来定义自己的类型了。

  • 格式:

    type 类型名 struct{
            字段名 字段类型
            字段名 字段类型
            ...
    }
    类型名: 标识自定义结构体的名称,再同一个包内不能重复
    字段名: 标识结构体字段名,结构体中的字段名必须唯一
    字段类型:标识结构体字段的具体类型
    

0.类型别名和自定义类型

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

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

    type MyInt int
    

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

  • 类型别名

    • 类型别名是Go1.9版本添加的新功能。典型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
    type TypeAlias = Type
    
    • 我们之前见过的rune和byte就是类型别名,他们定义如下:
    type byte = uint8
    type rune = int32
    
  • 类型定义和类型别名的区别

    • 类型别名与类型定义表面上看是划等号,我们通过下面这段代码就理解它们之间区别。
    // 类型定义
    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
    }
    

    结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

1.结构体的实例化

  • 只有当结构体实例化时,才会真正地分配内存,也就是必须实例化后才能使用结构体的字段。

  • 结构体本身也是一种类型,我们可以像生命内置类型一样使用var关键字生命结构体类型。

  • 语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型

    package main
    
    import "fmt"
    
    //1.定义结构体:这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。
    type person struct {
            name, city string
            age        int
    }
    
    func main() {
            //2.结构体的实例化
            var p person
            p.name = "俊凯"
            p.city = "北京"
            p.age = 22
            fmt.Printf("p=%v\n", p)
            fmt.Println(p.name)
            fmt.Println(p.city)
            fmt.Println(p.age)
    }
    
  • 示例:

    //定义学生类
    type Student struct {
            id   int
            name string
            sex  byte
            age  int
            addr string
    }
    
    func main() {
        //1.顺序初始化
            var s1 Student = Student{1, "Jone", 'f', 20, "shahe"}
        //指定创建
            s2 := Student{id: 2, age: 20}
        //结构体作为指针变量初始化
            var s3 *Student = &Student{3, "Lucy", 'm', 25, "changping"}
    
            fmt.Println(s1, s2, *s3)// {1 Jone 102 20 shahe} {2  0 20 } {3 Lucy 109 25 changping}
            fmt.Println(s3.id)// 3
            }
    
  • 结构体参数:结构体可以作为函数参数传递

    type Student struct {
            id   int
            name string
            sex  byte
            age  int
            addr string
    }
    
    func temStudent(tmp Student) {
            tmp.id = 250
            fmt.Println(tmp)
    }
    func temStudent2(p *Student) {
            p.id = 300
            fmt.Println(p)
    }
    
    func main() {
            var s1 Student = Student{1, "Jone", 'f', 20, "shahe"}
            //传递非指针,不更改原对象
            temStudent(s1)
            fmt.Println(s1)
            //传递指针,更改原对象
            temStudent2(&s1)
            fmt.Println(s1)
            }
    

2.匿名结构体

  • 在定义一些临时数据结构等场景下还可以使用匿名结构体,可能用一次就不用了。

    package main
    import "fmt"
    
    func main() {
            var user struct{Name string;Age int}
            user.Name = "xjk"
            user.Age = 19
            fmt.Printf("%v\n",user)//{xjk 19}
            fmt.Printf("%#v\n",user)//struct { Name string; Age int }{Name:"xjk", Age:19}
    }
    
  • 指针类型的结构体

    • 我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。
    package main
    
    import "fmt"
    
    type person struct {
            name, city string
            age        int8
    }
    
    func main() {
            var p2 = new(person) //此时p2为一个指针
            /*
                    (*p2).name = "xjk"
                    (*p2).age = 22
                    (*p2).city = "北京"
            */
            //但是这样写过于麻烦,可以这么写
            //在struct体中,可以不用进行指针的操作,它会自动帮我们找到变量
            p2.name = "xjk"
            p2.age = 22
            p2.city = "北京"
            fmt.Printf("%v\n", p2)
    
    }
    
    
  • 取结构体的地址进行实例化

package main

import "fmt"

type person struct {
        name, city string
        age        int8
}

func main() {
        //取结构体的地址进行实例化
        p3 := &person{}
        p3.name = "xjk"
        p3.age = 22
        p3.city = "北京"

        fmt.Printf("%v\n", p3)

}
//&{xjk 北京 22}

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

3.结构体初始化

  • 键值对初始化

    package main
    
    import "fmt"
    
    //结构体的初始化
    type person struct {
            name, city string
            age        int8
    }
    
    func main() {
            //1.键值对初始化
            p4 := person{
                    name: "xujunkai",
                    age:  18,
                    city: "上海",
            }
            fmt.Printf("%v\n", p4) //{xujunkai 上海 18}
            // 对结构体指针进行键值对初始化
            p5 := &person{
                    name: "jk",
                    city: "北京",
                    age:  19,
            }
            fmt.Printf("%v\n", p5) //&{jk 北京 19}
    }
    
    
  • 使用值的列表初始化

    package main
    
    import "fmt"
    
    //结构体的初始化
    type person struct {
            name, city string
            age        int8
    }
    
    func main() {
            //2.值的列表进行初始化
            //注意struct初始化的字段与填写值对应好
            p6 := person{
                    "xjk",
                    "青岛",
                    21,
            }
            fmt.Printf("%v\n", p6) //{xjk 青岛 21}
    }
    
    
    • 列表初始化注意
      • 必须要初始化结构体所有字段
      • 初始值的填充顺序必须与字段在结构体中的声明顺序一致
      • 该方式不能和键值初始化方式混用

4.结构体内存布局

  • 结构体占用的一块内存是连续的

    package main
    
    import "fmt"
    
    type test struct {
            a int8
            b int8
            c int8
            d int8
    }
    
    func main() {
            n := test{
                    1, 2, 3, 4,
            }
            fmt.Printf("%p\n", &n.a)
            fmt.Printf("%p\n", &n.b)
            fmt.Printf("%p\n", &n.c)
            fmt.Printf("%p\n", &n.d)
        //连续的
            /*
                    0x11010080
                    0x11010081
                    0x11010082
                    0x11010083
            */
    
    }
    
    

5.构造函数

  • Go语言中没有构造函数,但是我们可以自己实现,看下面代码实现一个person的构造函数,因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以这构造函数返回的是结构体指针类型。
package main

import "fmt"

//构造结构体构造函数:构造一个结构体实例的函数
//结构体是值类型
type person struct {
        name, city string
        age        int8
}

//构造函数
// 定义返回为person内存地址
func newPerson(name, city string, age int8) *person {//为什么用*person节约内存
    // 返回person内存地址
        return &person{
        //&person 同上
                name: name, //前面nane为结构体字段,后面name为初始化函数传入变量
                city: city,
                age:  age,
        }
}

func main() {
        p1 := newPerson("大老王", "洛杉矶", int8(19))
        fmt.Printf("type:%T value:%v\n", p1, p1)
        //type:main.person value:{大老王 洛杉矶 19}
}

  • 面试题:
type student struct {
        name string
        age  int
}

func main() {
        m := make(map[string]*student)
        stus := []student{
                {name: "小王子", age: 18},
                {name: "娜扎", age: 23},
                {name: "大王八", age: 9000},
        }
        fmt.Println(stus)
        for _, stu := range stus {
                fmt.Println(&stu)
                fmt.Printf("---->%v\n", stu)
                m[stu.name] = &stu
        }
        for k, v := range m {
                fmt.Println(k, "=>", v.name)
        }
}
//小王子 => 大王八
//娜扎 => 大王八
//大王八 => 大王八

3.方法的定义和接收者

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

  • 方法的定义格式如下:

    func (接收者变量 接受者类型)方法名(参数列表)(返回参数){
            函数体
    }
    
  • 其中:

    • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self,this之类的命名,例如,Person烈性的接收者变量应该命名为p,Connector类型的接收者变量应该命名为c等。
    • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
    • 方法名,参数列表,返回参数:具体格式与函数定义相同。
  • 示例:

    package main
    
    import "fmt"
    
    //方法的定义实例
    
    //Person 是一个结构体
    type Person struct {
            name string
            age  int8
    }
    
    //NewPerson  是一个Person类型的构造函数
    func NewPerson(name string, age int8) *Person {
            return &Person{
                    name: name,
                    age:  age,
            }
    }
    
    //Dream 是为Person类型定义方法
    func (p Person) Dream() {
            fmt.Printf("%s的梦想是学好GoLang", p.name)
    }
    
    func main() {
            p1 := NewPerson("老王", int8(20))
            // (*p1).Dream()
            p1.Dream()
    }
    

    方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

1.指针类型接收者

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

  • 指的是接收者的类型是指针

    package main
    
    import "fmt"
    
    //方法的定义实例
    
    //Person 是一个结构体
    type Person struct {
            name string
            age  int8
    }
    
    //NewPerson  是一个Person类型的构造函数
    func NewPerson(name string, age int8) *Person {
            return &Person{
                    name: name,
                    age:  age,
            }
    }
    
    //Dream 是为Person类型定义方法
    func (p Person) Dream() {
            fmt.Printf("%s的梦想是学好GoLang", p.name)
    }
    
    // SetAge 是一个修改年龄方法(指针类型接收者)
    func (p *Person) SetAge(newAge int8) {
            p.age = newAge
    }
    
    func main() {
            p1 := NewPerson("老王", int8(20))
    
            fmt.Println(p1.age)//20
            p1.SetAge(int8(33))
            fmt.Println(p1.age)//33
    }
    
    

2.值类型接收者

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

import "fmt"

//方法的定义实例

//Person 是一个结构体
type Person struct {
        name string
        age  int8
}

//NewPerson  是一个Person类型的构造函数
func NewPerson(name string, age int8) *Person {
        return &Person{
                name: name,
                age:  age,
        }
}

//Dream 是为Person类型定义方法
func (p Person) Dream() {
        fmt.Printf("%s的梦想是学好GoLang", p.name)
}

// SetAge 是一个修改年龄方法(指针类型接收者)
func (p *Person) SetAge(newAge int8) {
        p.age = newAge
}

// SetAge2 修改年龄方法(值类型接收者)
func (p Person) SetAge2(newAge int8) {
        p.age = newAge
}

func main() {
        p1 := NewPerson("老王", int8(20))
        // (*p1).Dream()
        p1.Dream()

        fmt.Println(p1.age)
        p1.SetAge(int8(33))
        fmt.Println(p1.age)

        p1.SetAge2(int8(30))
        fmt.Println(p1.age) //还是33
        /*
                发现执行SetAge2方法,打印结果还是33,原因是:
                在Go语言中函数传参时,都是值拷贝,如果要想要在函数
                修改变量时,需要传入变量指针,同样在方法一样,想要
                在方法中修改变量的属性(字段),如果传入是值类型,只
                会进行值拷贝,此时方法中p已经不是之前调用的p1了,所
                以一定要使用指针接收者

        */

}

3.什么时候应该使用指针类型

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

4.任意类型添加方法

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

    package main
    
    import "fmt"
    
    //为任意类型添加方法
    
    //基于内置的基本类型造一个自己类型
    type myInt int
    
    func (m myInt) SayHi() {
            fmt.Println("hi")
    }
    
    func main() {
            var m1 myInt
            m1 = 100
            m1.SayHi()
    
    }
    // 注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
    

5.结构体的匿名字段

  • 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

    type Person struct {
            string
            int
    }
    
    func main() {
            p := Person{"xjk",19}
            fmt.Printf("%#v\n",p)// main.Person{string:"xjk", int:19}
            fmt.Println(p.string,p.int)// xjk 19
    }
    

    匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

4.结构体的嵌套

1嵌套结构

  • 结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体包中可访问)

    package main
    
    import "fmt"
    
    type Address struct {
            Province string
            City string
    }
    
    type User struct {
            Name string
            Gender string
            Address Address
    }
    
    func main() {
            user1 := User{
                    Name: "xjk",
                    Gender: "male",
            // Address 是嵌套的结构体
                    Address: Address{
                            Province: "zhejiang",
                            City: "hangzhou",
                    },
            }
            // user1=main.User{Name:"xjk", Gender:"male", Address:main.Address{Province:"zhejiang", City:"hangzhou"}}
            fmt.Printf("user1=%#v\n",user1)
    }
    

2.嵌套匿名结构体

  • 嵌套匿名结构体

    package main
    
    import "fmt"
    
    type Address struct {
            Province string
            City string
    }
    
    type User struct {
            Name string
            Gender string
            Address //为匿名结构体
    }
    func main () {
            var user2 User
            user2.Name = "Tom"
            user2.Gender = "male"
        //通过匿名结构体.字段名访问
            user2.Address.Province = "shandong"
        //直接访问匿名结构体的字段名
            user2.City = "qingdao"
            // user2=main.User{Name:"Tom", Gender:"male", Address:main.Address{Province:"shandong", City:"qingdao"}}
            fmt.Printf("user2=%#v\n",user2)
    
    }
    

    当访问结构体成员时,会先在结构体中查找该字段,找不到再去匿名结构体中查找

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

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

    package main
    
    import "fmt"
    
    type Address struct {
            Province string
            City string
            Createtime string
    }
    
    type Email struct {
            Account string
            Createtime string
    }
    
    type User struct {
            Name string
            Gender string
            Address
            Email
    }
    
    func main() {
            var user3 User
            user3.Name = "Tom"
            user3.Gender = "male"
            user3.Address.Province = "shandong"
            user3.City = "qingdao"
            user3.Account = "1111@163.com"
            // 如果指定 user3.Createtime = "2012",这样会产生歧义,因为Email和Address都有CreateTime字段,无法确认是哪个。
            //指定Address结构体中的CreateTime
            user3.Address.Createtime = "2010"
            //指定Email结构体中的CreateTime
            user3.Email.Createtime = "2010"
            fmt.Printf("user3=%#v\n",user3)
    }
    

4结构体的继承

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

    package main
    
    
    import "fmt"
    
    type Animal struct {
            name string
    }
    
    func (a *Animal) move() {
            fmt.Printf("%s会动!\n",a.name)
    }
    
    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.move()
            d1.wang()
    }
    // 二狗子会动!
    // 二狗子 会汪汪汪~
    

5结构体字段的可见性

  • 结构体字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

6结构体与JSON序列化

  • JSON(JavaScript Object N)是一种轻量级的数据交换模式,易于人阅读和编写,同时也易于机器解析和生成,JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名卸载前面并用双引号""包裹,使用冒号:分割,然后紧接着值:多个键值之间使用英文,分隔。

    package main
    
    import (
            "encoding/json"
            "fmt"
    )
    
    //结构体字段的可见性与JSON序列化
    
    // 如果一个Go语言包中定义的标识符是首字母大写的,那么就是对外可见的
    // 如果一个结构体中字段名首字母大写,那么该字段就是对外可见的
    type student struct {
            ID   int
            Name string
    }
    
    type class struct {
            Title    string    `json:"title"` //当使用json,处理将引用小写title,必须这么写,如果有多个Tag值,用空格分隔
            Students []student `json:"stu_list" db:"db_stu" xml:"html"`
    }
    
    //student的构造函数
    func newStudent(id int, name string) student {
            return student{
                    ID:   id,
                    Name: name,
            }
    }
    
    func main() {
            //创建一个班级变量c1
            c1 := class{
                    Title:    "火箭101",
                    Students: make([]student, 0, 20),
            }
            //往班级c1中添加学生
            for i := 0; i < 10; i++ {
                    //造10个学生
                    tempStu := newStudent(i, fmt.Sprintf("stu%02d", i))
                    c1.Students = append(c1.Students, tempStu)
    
            }
            fmt.Printf("%#v\n", c1)
            /*
                    main.class{Title:"火箭101", Students:[]main.student{main.student{ID:0, Name:"stu00"}, main.student{ID:1, Name:"stu01"}, main.student{ID:2, Name:"stu02"}, main.student{ID:3, Name:"stu03"}, main.student{ID:4, Name:"stu04"}, main.student{ID:5, Name:"stu05"}, main.student{ID:6, Name:"stu06"}, main.student{ID:7, Name:"stu07"}, main.student{ID:8, Name:"stu08"}, main.student{ID:9, Name:"stu09"}}}
            */
    
            //json序列化:Go语言中数据->Json格式的字符串
            data, err := json.Marshal(c1)
            if err != nil {
                    fmt.Println("json marshal failed,error", err)
                    return
            }
            fmt.Printf("%T\n", data)
            fmt.Printf("%s\n", data)
            //json反序列化,JSON格式的字符串-->Go语言中数据
            jsonStr := `{"title": "火箭101","Students": [
                            {
                                    "ID": 0,
                                    "Name": "stu00"
                            },
                            {
                                    "ID": 1,
                                    "Name": "stu01"
                            }
                    ]
                    }`
            var c2 class
            err = json.Unmarshal([]byte(jsonStr), &c2)
            if err != nil {
                    fmt.Println("json unmarshal failed err", err)
                    return
            }
            fmt.Printf("%#v\n", c2)
    
  • 结构体标签(Tag)

    • tag是结构体的元信息,可以在运行时候通过反射的机制读取出来,tag在结构体字段的后方定义,由一对反引号包裹起来,具体格式如下:

      key1:"value1"  key2:"value"
      
    • 结构体标签由一个或多个键值对组成,键与值使用冒号分隔,值用双引号括起来,键值对之间使用一个空格分隔。注意事项:为结构体编写Tag时,必须严格遵守键值对规则,结构提标前的解析代码的容错能力很差,一旦格式写错,编译和运行都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

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

package main

import (
        "encoding/json"
        "fmt"
)
type Student struct {
        ID int  `json:"id"`////通过指定tag实现json序列化该字段时的key
        Gender string//json序列化是默认使用字段名作为key
        name string//私有不能被json包访问
}
func main() {
        s1 := Student{
                ID:1,
                Gender:"female",
                name:"Lily",
        }
        data,err := json.Marshal(s1)
        if err != nil {
                fmt.Println("json marshal failed!")
                return
        }
        // json str:{"id":1,"Gender":"female"}
        fmt.Printf("json str:%s\n",data)
}
  • 练习
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},
    }
    // [{1 xiaoming 22} {2 xiaozhang 33}]
    fmt.Println(ce)
    demo(ce)
    // [{1 xiaoming 22} {2 xiaozhang 999}]
    fmt.Println(ce)
}
  • 删除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}
    // map[1:{1 xiaolizi 22} 2:{2 wang 23}]
    fmt.Println(ce)
    delete(ce, 2)
    // map[1:{1 xiaolizi 22}]
    fmt.Println(ce)
}