Go中的结构体

本文参考:https://www.liwenzhou.com/posts/Go/10_struct/

结构体

Go语言中的基本数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或者部分属性时,这时候再用单一的基本数据类型明显就无法满足需求。Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,struct

Go语言中通过struct来实现面向对象。

结构体的定义

使用typestruct关键字来定义结构体

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

示例:

type person struct{   // 定义一个person的结构体
    name string
    age int8
    city string   
}

同类型的子弹也可以写在一行:

type person struct{
        name,city string
    age int8
}

这样就拥有一个person的自定义类型,它有name,age,city三个字段,分别标识姓名,年龄和城市。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人的信息。

语言内置的基本数据类型是用来描述一个值的,而及饿哦固体是用来描述一组值的。比如一个人的名字,年龄,城市等,本质上是一种聚合性的数据类型。

结构体实例化

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

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字来声明结构体类型

var 结构体实例 结构体类型

基本实例化

type person struct{
        name string
        age int8
        city string
}

func main(){
    var p person
    p.name = "Negan"
    p.age = 18
    p.city = "西安"
    fmt.Printf("p=%v\n",p)  // p={Negan 西安 18}
    fmt.Printf("p=%#v\n",p)  // p=main.person{name:"Negan",age:18,city:"西安"}
}

我们通过.来访问结构体字段(成员变量),例如p.namep.age等。

匿名结构体

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

func main(){
        var user struct{name string;age int}
        user.name = "Negan"
        user.age = 18
        fmt.Printf("%#v\n", user)
}

创建指针类型结构体

通过使用new关键字对结构体进行实例化,得到的是结构体地址。

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

从打印结果来看,此时p是一个结构体指针。

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

var p = new(person)
p.name = "Negan"
p.age = 68
p.city = "亚历山大"
fmt.Printf("p=%#v\n",p)  // p=&main.person{name:"Negan",age:68,city:"亚历山大"}

取结构体的地址实例化

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

p := &person{}
fmt.Printf("%T\n",p)  // *main.person
fmt.Printf("p=%v\n",p)  // p=&main.person{name:"",age:0,city:""}
p.name = "Negan"
p.age = 68
p.city = "救世堂"  
fmt.Printf("p=%#v\n",p)  // p=&main.person{name:"Negan",age:68,city:"救世堂"}

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

结构体初始化

没有初始化的结构体,其成员变量都是对应其类型的零值。

type person struct{
        name string
        age int8
        city string
}

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

使用键值对初始化

使用键值对对结构体进行初始化,键对应结构体的字段,值对应该字段的初始值。

p := person{
        name:"Negan",
        age:68,
        city:"亚历山大"
}
fmt.Printf("p=%#v\n",p)  // p=main.person{name:"Negan",age:68,city:"亚历山大"}

也可以使用结构体指针进行键值对初始化

p := &person{
        name:"Negan",
        age:68,
        city:"亚历山大"
}
fmt.Printf("p=%#v\n",p)  //p=&main.person{name:"Negan",age:68,city:"亚历山大"}

当某些字段没有初始值的时候,该字段可以不写,此时没有指定初始值的字段的值就是该字段类型的零值。

p := &person{
    city:"救世堂"
}
fmt.Printf("p=%#v\n",p)  // p=&main.person{name:"",age:0,city:"救世堂"}

使用值的列表初始化

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值

p := &person{
        "Negan",
        68,
        "救世堂"
}
fmt.Printf("p=%#v\n",p)  // p=&main.person{name:"Negan",age:68,city:"救世堂"}

使用这种格式初始化时,需要注意:

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

结构体内存布局

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

package main

import "fmt"

func main() {
        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)  // n.a 0xc0000140a8
        fmt.Printf("n.b %p\n",&n.b)  // n.b 0xc0000140a9
        fmt.Printf("n.c %p\n",&n.c)  // n.c 0xc0000140aa
        fmt.Printf("n.d %p\n",&n.d)  // n.d 0xc0000140ab
}

空结构体

空结构体是不占内存的。

var v struct{}
fmt.Println(unsafe.Sizeof(v))  // 0

面试题

请问下面代码执行的结构是什么?

type student struct {
        name string
        age int
}

func main() {
        m := make(map[string]*student)
        stus := []student{
                {name:"Negan",age: 68},
                {name:"Alice",age:29},
                {name:"小王八",age:10000},
        }
        for _,s := range stus{
                m[s.name] = &s
        }
        fmt.Println(m)

        for k,v := range m{
                fmt.Println(k,"->",v.name)
        }
}

输出结果

map[Alice:0xc0000044a0 Negan:0xc0000044a0 小王八:0xc0000044a0]
Negan -> 小王八
Alice -> 小王八
小王八 -> 小王八

说明:通过打印m我们可以知道,map的值都是同一个地址。所以导致所有的值都相同。for range在遍历切片的时候,创建了每个元素的副本,而不是直接返回每个元素的引用,如果使用该值变量的地址作为指向每个元素的指针,就会导致错误,在迭代时,返回的变量是迭代过程中根据切片依次赋值的新变量,所以值的地址总是相同。

type student struct {
        name string
        age int
}

func main() {
        m := make(map[string]*student)
        stus := []student{
                {name:"Negan",age: 68},
                {name:"Alice",age:29},
                {name:"小王八",age:10000},
        }
        for _,s := range stus{
                name := s
                m[s.name] = &name
        }
        fmt.Println(m)

        for k,v := range m{
                fmt.Println(k,"->",v.name)
        }
}

输出结果

map[Alice:0xc0000044c0 Negan:0xc0000044a0 小王八:0xc0000044e0]
小王八 -> 小王八
Negan -> Negan
Alice -> Alice

构造函数

Go语言的结构体没有构造函数,我们可以自己实现。

下方的代码实现了一个person的构造函数,因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大。所以构造函数返回的是结构体的指针类型。

type person struct {
        name string
        age int8
        city string
}

func newPerson(name,city string, age int8) *person{
        return &person{
                name:name,
                age: age,
                city:city,

        }
}

func main() {
        p := newPerson("Negan", "亚历山大",68)   // 调用构造函数
        fmt.Printf("%#v\n",p)  // &main.person{name:"Negan", age:68, city:"亚历山大"}

}

方法和接收者

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

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数){
        函数体
}
  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如Person类型的接收者变量应该命名为p
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
  • 方法名、参数列表、返回参数:具体格式与函数定义相同
// person结构体
type person struct {
        name string
        age int8
        city string
}

// newPerson 构造函数
func newPerson(name,city string, age int8) *person{
        return &person{
                name:name,
                age: age,
                city:city,

        }
}

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

func main() {
        p := newPerson("Negan", "亚历山大",68)
        fmt.Printf("%#v\n",p)  // &main.person{name:"Negan", age:68, city:"亚历山大"}
        p.Dream()  //Negan的梦想是学好Go语言
}

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

指针类型的接收者

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

// setAge 设置p的年龄
// 使用指针接收者
func (p *person) setAge(newAge int8){
    p.age = newAge
}

调用该方法:

func main() {
        p := newPerson("Negan", "亚历山大",68)
        fmt.Println(p.age)  // 68
        p.setAge(30)
        fmt.Println(p.age)  // 30

值类型的接收者

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

// setAge 设置p的年龄
// 使用值接收者
// setAge设置p的年龄,使用指针接收者
func (p person) setAge(newAge int8){
        p.age = newAge
}

func main() {
        p := newPerson("Negan", "亚历山大",68)
        fmt.Println(p.age)  // 68
        p.setAge(30)
        fmt.Println(p.age)  // 68
}

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

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

任意类型添加方法

在Go语言中,接收者的类型可以是任意类型,不仅仅是结构体,任何类型都可以拥有方法。

type MyInt int

func (m MyInt) sayHello(){
        fmt.Println("hello,我是一个int")
}

func main() {
        var m1 MyInt
        m1.sayHello()   // hello,我是一个int
        m1 = 100
        fmt.Printf("%#v %T\n",m1,m1)  // 100 main.MyInt
}

注意事项:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

结构体的匿名字段

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

// 结构体的匿名字段
// 结构体Person类型
type Person struct{
        string
        int
}

func main() {
        p1 := Person{
                "Negan",
                68,
        }
        fmt.Printf("%#v\n", p1)   // main.Person{string:"Negan", int:68}
        fmt.Println(p1.string, p1.int)  // Negan 68
}

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

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或者结构体指针。

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

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

func main() {
        user := User{
                Name: "Negan",
                Gender:"男",
                Address: Address{
                        Province: "陕西",
                        City: "西安",
                },
        }
        fmt.Printf("user=%#v\n", user) 
    // user=main.User{Name:"Negan", Gender:"男", Address:main.Address{Province:"陕西", City:"西安"}}
}

嵌套匿名结构体

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

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

func main() {
        var user User
        user.Name = "Negan"
        user.Gender = "男"
        user.Address.Province = "陕西"  // 通过匿名结构体.字段名访问
        user.City = "西安"   // 直接访问匿名结构体的字段名
        fmt.Printf("user=%#v\n", user)  
        // user=main.User{Name:"Negan", Gender:"男", Address:main.Address{Province:"陕西", City:"西安"}}
}

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

嵌套结构体的字段名冲突

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

// 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 user User
        user.Name = "Negan"
        user.Gender = "男"
        user.Address.CreateTime = "2020"
        user.Email.CreateTime = "2020"
        fmt.Printf("%#v\n", user)
}

结构体的“继承”

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

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

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

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

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

func main() {
        d := &Dog{
                Feet: 4,
                Animal:&Animal{
                        name: "旺财",
                },
        }
        d.wang()  // 旺财会汪汪汪~
        d.move()  // 旺财会动
}

结构体字段的可见性

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

结构体与JSON序列化

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对嘴和中的简明卸载前面并用双引号“”包裹,使用:分割,然后紧接着值,多个键值对之间使用,分割。

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

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

func main() {
        c := &Class{
                Title: "101",
                Student: make([]*Student,0,200),
        }
        for i:=0;i<10;i++{
                stu := &Student{
                        Name: fmt.Sprintf("stu%02d",i),
                        Gender: "男",
                        ID:i,
                }
                c.Student = append(c.Student, stu)
        }

        // Json序列化:结构体--> Json格式的字符串
        data, err := json.Marshal(c)
        if err != nil{
                fmt.Println("json marshal failed")
        }
        fmt.Printf("json:%s\n",data)

        // Json反序列化:Json格式的字符串-->结构体
        c1 := &Class{}
        err = json.Unmarshal([]byte(data),c1)
        if err != nil{
                fmt.Println("json unmarshal failed!")
                return
        }
        fmt.Printf("%#v\n", c1)
}

结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义,由一对反引号“``”包裹起来。

`key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成,键与值使用冒号分隔,值使用双引号括起来。同一个结构体字段可以这只多个键值对tag,不同的键值对之间使用空格分隔。

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

// 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:"Negan",
        }
        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":"男"}
}

结构体和方法补充知识点

因为slice和map这两种数据类型都包含了指向底层数据的指针,因此在需要复制的时候特别注意

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

func (p *Person) SetDreams(dreams []string){
        p.dreams = dreams
}

func main() {
        p1 := Person{
                name: "Negan",
                age:68,
        }
        data := []string{"吃饭","睡觉","打豆豆"}
        p1.SetDreams(data)
        fmt.Println(p1.dreams)   // [吃饭 睡觉 打豆豆]

        data[1] = "不睡觉"
        fmt.Println(p1.dreams)   // [吃饭 不睡觉 打豆豆]
}

正确的做法是在方法中使用传入slice的拷贝进行结构体赋值。

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

func (p *Person) SetDreams(dreams []string){
        p.dreams = make([]string,len(dreams))
        copy(p.dreams,dreams)
}

func main() {
        p1 := Person{
                name: "Negan",
                age:68,
        }
        data := []string{"吃饭","睡觉","打豆豆"}
        p1.SetDreams(data)
        fmt.Println(p1.dreams)   // [吃饭 睡觉 打豆豆]

        data[1] = "不睡觉"
        fmt.Println(p1.dreams)   // [吃饭 睡觉 打豆豆]
}

练习题

使用“面向对象”的思维方式编写一个学生信息管理系统

  • 学生有id、姓名、年龄、分数等信息
  • 程序提供展示学生列表,添加学生,编辑学生信息,删除学生等功能。
type student struct{
    id int64
    name string
}

// 造一个学生的管理者
type studentMgr struct{
    allStudent map[int64]student
}

// 查看学生
func (s studentMgr) showStudents(){
    // 从s.allStudent这个map中将所有的学生逐个拿出来
    for _,stu := range s.allStudent{
        fmt.Printf("学号:%d,姓名:%s\n",stu.id,stu.name)
    }
}

// 增加学生
func (s studentMgr) addStudents(){
    // 根据用户输入的内容创建一个新的学生
    var (
        stuId int64
        stuName string
    )
    // 获取用户输入
    fmt.Print("请输入学号")
    fmt.Scanln(&stuId)
    fmt.Print("请输入姓名")
    fmt.Scanln(&stuName)
    // 把新的学生放到s.allStudent这个map中
    newStu := student{
        id:stuId,
        name:stuName
    }
    s.allStudent[newStu.id] = newStu
}

// 修改学生
func (s studentMgr) editStudents(){
    // 获取用户输入学号
    var stuId int64
    fmt.Print("请输入要修改学生的学号")
    fmt.Scanln(&stuId)
    // 展示该学号对应的学生信息,如果没有则提示查无此人
    stuObj, ok := s.allStudent[stuId]
    if !ok{
        fmt.Println("查无此人")
        return
    }
    fmt.Printf("要修改的学生信息如下:学号:%d,姓名:%s",stuObj.id,stuObj.name)
    fmt.Println("请输入学生新名字")
    var newName string
    fmt.Scanln(&newName)
    // 更新学生的姓名
    stuObj.name = newName
    s.allStudent[stuId] = stuObj
}

// 删除学生
func (s studentMgr) deleteStudents(){
    // 请用户输入要删除学生的id
    var stuId int64
    fmt.Println("请输入要删除学生的学号")
    fmt.Scanln(&stuId)
    // 在map中查找这个学生
    _,ok := s.allStudent[stuId]
    if !ok{
        fmt.Println("查无此人")
        return
    }
    // 删除,如何从map中删除键值对
    delete(s.allStudent, stuId)
    fmt.Println("删除成功")
}

var smr studentMgr  // 声明一个全局变量学生管理对象smr

// 菜单函数
func showMenu(){
    fmt.Println("welcome sms")
    fmt.Println(`
        1、查看所有学生
        2、添加学生
        3、修改学生
        4、删除学生
        5、退出
    `)
}

func main(){
    smr = studentMgr{
        allStudent:make(map[int64]student,100)
    }
    for{
        showMenu()  // 给用户展示菜单
        // 等待用户输入
        fmt.Print("请输入菜单序号:")
        var choice int
        fmt.Scanln(&choice)
        fmt.Println("您输入的是:",choice)
        
        switch choice{
        case 1:
            smr.showStudents()
        case 2:
            smr.addStudents()
        case 3:
            smr.editStudents()
        case 4:
            smr.deleteStudents()
        case 5:
            os.Exit(1)   // 退出    
        default:
            fmt.Println("请滚")    
        }
    }
}

本文参考:https://www.liwenzhou.com/posts/Go/10_struct/