天天看点

九、Go结构体

Go语言结构体(Struct)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、char、float 等基本类型组成的。你可以认为结构体是一种聚合类型。

在实际开发中,我们可以将一组类型不同的、但是用来描述同一件事物的变量放到结构体中。例如,在校学生有姓名、年龄、身高、成绩等属性,学了结构体后,我们就不需要再定义多个变量了,将它们都放到结构体中即可。

type Student struct {
    sid   int
    name string
    sex  byte
    age  int
    addr string
}
           

Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性。结构体成员,也可称之为成员变量,字段,属性。属性要满足唯一性。

在Go语言中,结构体承担着面向对象语言中类的作用。Go语言中,结构体本身仅用来定义属性。还可以通过接收器函数来定义方法,使用内嵌结构体来定义继承。这样使用结构体相关操作Go语言就可以实现OOP面向对象编程了。

9.1、声明结构体

Go语言通过type关键字声明结构体,格式如下:

type 类型名 struct {   // 标识结构体的类型名,在同一个包内不能重复
    字段1 字段1类型    // 字段名必须唯一
    字段2 字段2类型
    …
}
           

同类型的变量也可以写在一行,用逗号隔开

type Book struct {
   title,author string
   price int
}
           

9.2、结构体的实例化

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。

实例化方式包括如下几种。

9.2.1、实例化之 var 变量名 结构体

结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。

package main

import "fmt"

type Student struct {
    sid    int
    name   string
    course []string           //  报名课程
    score  map[string]float64 // 各科成绩
}

func main() {

    // 声明一个结构体对象 ,值类型,默认开辟空间,字段赋予零值
    var s Student
    fmt.Println("s:", s)
    // 要访问结构体成员,需要使用点号 . 操作符
    fmt.Println(s.name)
    // 更改成员变量的值
    s.name = "yuan"
    fmt.Println(s.name)
    // s.course[0] = "chinese"   // 结果,如何调整

}
           

1、结构体属于值类型,即var声明后会像整形字符串一样创建内存空间。

2、创建结构体对象如果没有给字段赋值,则默认零值(字符串默认 “",数值默认0,布尔默认false,切片和map默认nil对象)

结构体的内存存储:

package main

import "fmt"

type Person struct {
    name      string
    age       int
    isMarried bool
}

func main() {
    var p Person
    p.name = "yuan"
    p.age = 23
    p.isMarried = false
    fmt.Printf("%p\n", &p)
    fmt.Printf("%p\n", &(p.name))
    fmt.Printf("%p\n", &(p.age))
    fmt.Printf("%p\n", &(p.isMarried))
}

           

之前我们学习过值类型和引用类型,知道值类型是变量对应的地址直接存储值,而引用类型是变量对应地址存储的是地址。因为结构体因为是值类型,所以p的地址与存储的第一个值的地址是相同的,而后面每一个成员变量的地址是连续的。

9.2.2、实例化之 结构体{}

// (1) 方式1
var s2 =Student{}
s2.name = "alvin"
// (2) 方式2:键值对赋值
s2 = Student{sid:1001,name: "alvin",course: []string{"chinese","math"}}
fmt.Println(s2.name)
fmt.Println(s2.score)  // 默认值
// (3) 方式3:多值赋值
s2 = Student{1001,"alvin",[]string{"chinese","math"},nil}
fmt.Println(s2.name)
fmt.Println(s2.course[1])
           

1、结构体可以使用“键值对”(Key value pair)初始化字段,每个“键”(Key)对应结构体中的一个字段,键的“值”(Value)对应字段需要初始化的值。键值对的填充是可选的,不需要初始化的字段可以不填入初始化列表中。

2、多值初始化方式必须初始化结构体的所有字段且每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。

3、键值对与值列表的初始化形式不能混用。

9.2.3、实例化之 &结构体{}

package main

import "fmt"

type Student struct {
    sid    int
    name   string
    course []string           //  报名课程
    score  map[string]float64 // 各科成绩
}

func CourseInit(stu *Student) {
    (*stu).course = make([]string, 5)
    // stu.course = make([]string, 5)
}

func main() {

    // 案例1
    s2 := Student{sid: 1001, name: "alvin", course: []string{"chinese", "math"}}
    s3 := s2
    fmt.Println(s3)
    s2.name = "rain"
    fmt.Println(s2.name)
    fmt.Println(s3.name)
    // 如果希望s3的值跟随s2保持一致怎么实现
    s4 := &s2 // var s4 *Student = &s2
    s2.name = "rain"
    fmt.Println(s4.name)

    // 案例2
    //var s = Student{sid: 1001, name: "alvin", course: []string{"chinese", "math"}}
    //CourseInit(s)
    //fmt.Println("s报的课程:",s.course)

    var s = &Student{sid: 1001, name: "alvin", course: []string{"chinese", "math"}}
    CourseInit(s)
    fmt.Println("s报的课程:",(*s).course)  // *s.course的写法是错误的
    fmt.Println("s报的课程:",s.course)

}
           

在Go语言中,访问结构体指针的成员变量时可以继续使用.,这是因为Go语言为了方便开发者访问结构体指针的成员变量可以像访问结构体的成员变量一样简单,使用了语法糖(Syntactic sugar)技术,将 instance.Name 形式转换为 (*instance).Name。

9.2.4、实例化之 new(结构体)

Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。使用 new 的格式如下:其中:

instance := new(T)
           

其中:

T 为类型,可以是结构体、整型、字符串等。

instance:T 类型被实例化后保存到 instance 变量中,instance的类型为 *T,属于指针。

s := new(Student)  // &Student{}
fmt.Println(reflect.TypeOf(s)) // *Student
fmt.Println(s) // *Student
s.name = "yuan"
fmt.Println((*s).name)
           

9.4、模拟构造函数

Go语言没有构造函数,但是我们可以使用结构体初始化的过程来模拟实现构造函数。

func NewStudent(sid int,name string,sex byte,age int,addr string)*Student{

    return &Student{
        sid:sid,
        name:name,
        sex:sex,
        age:age,
        addr:addr,
    }
}
func main() {

    s:=NewStudent(1001,"yuan",'m',23,"beijing")  
    fmt.Println(s.name)
    fmt.Println(s.age)

}
           

9.5、方法接收器

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。

方法的定义格式如下:

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

其中,

接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。

接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。

方法名、参数列表、返回参数:具体格式与函数定义相同。

package main

import "fmt"

type Player struct{
    Name string
    HealthPoint int
    Magic int
}

// Player结构类的构造函数
func NewPlayer(name string,hp int,mp int) *Player{

    return &Player{
        name,hp,mp,
    }
}

// Player结构的方法
func (p Player) attack()  {
    fmt.Printf("%s发起攻击!\n",p.Name)
}
func (p *Player) attacked()  {
    fmt.Printf("%s被攻击!\n",p.Name)
    p.HealthPoint -= 10
    fmt.Println(p.HealthPoint)
}

func (p Player) buy_prop()  {
    fmt.Printf("%s购买道具!\n",p.Name)
}

func main() {
    player := NewPlayer("yuan",100,100)
    player.attack()
    player.attacked()
    fmt.Println(player.HealthPoint)
    player.buy_prop()
}
           

1、官方定义:Methods are not mixed with the data definition (the structs): they are orthogonal to types; representation(data) and behavior (methods) are independent

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

9.6、匿名字段

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

type Person struct {
    string
    int
}

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

结构体也可以作为匿名字段使用

package main

import "fmt"

type Addr struct {
    country string
    province string
    city string
}

type Person struct {
    string
    int
    Addr
}


func main() {
    p1 := Person{
        "yuan",
        18,
        Addr{"中国","广东省","深圳"},
    }
    fmt.Printf("%#v\n", p1)        //main.Person{string:"北京", int:18}
    fmt.Println(p1.string, p1.int) // yuan 18
    fmt.Println(p1.Addr) 
    fmt.Println(p1.Addr.country) // 中国
    fmt.Println(p1.city) // 深圳
}
           

当结构体中有和匿名字段相同的字段时,采用外层优先访问原则

9.7、结构体的"继承”

package main

import "fmt"

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

func (a *Animal) eat() {
    fmt.Printf("%s is eating!\n", a.name)
}
func (a *Animal) sleep() {
    fmt.Printf("%s is sleeping!\n", a.name)
}

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

func (d *Dog) bark () {
    fmt.Printf("%s is barking ~\n", d.name)
}

func main() {
    d1 := &Dog{
        Kind: "金毛",
        Animal: &Animal{ //注意嵌套的是结构体指针
            name: "路易十六",
        },
    }
    d1.bark()
    d1.eat()
}
           

9.8、序列化

序列化: 通过某种方式把数据结构或对象写入到磁盘文件中或通过网络传到其他节点的过程。

反序列化:把磁盘中对象或者把网络节点中传输的数据恢复为python的数据对象的过程。

9.8.1、json初识

序列化最重要的就是json序列化。

JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

package main

import (
    "encoding/json"
    "fmt"
)

type Addr struct {
    Province string
    City     string
}
type Stu struct {
    Name string `json:"name"`
    Age  int `json:"-"`   // 表示不参与序列化
    Addr Addr
}

func main() {

    var map01 = map[int]string{1: "111", 2: "222", 3: "333"}
    var stu01 = Stu{Name: "yuan", Age: 18, Addr: Addr{Province: "Hebei", City: "beijing"}}

    // 序列化
    jsonMap01, _ := json.Marshal(map01)
    jsonStu01, _ := json.Marshal(stu01)

    fmt.Println(string(jsonMap01))
    fmt.Println(string(jsonStu01))

    // 反序列化
    // var x  = make(map[int]string)
    var map02 map[int]string
    json.Unmarshal([]byte(jsonMap01), &map02)
    fmt.Println("map02", map02)

    // var data = `{"name":"yuan","Age":18,"Addr":{"Province":"Hebei","City":"beijing"}}`
    var stu02 Stu

    json.Unmarshal([]byte(jsonStu01), &stu02)
    fmt.Println(stu02)
    fmt.Println(stu02.Name)
    fmt.Println(stu02.Addr.City)

}
           
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "os"
)

// 学生类
type Student struct {
    Sid   int
    Name  string
    Score map[string]float64
}

// 学生类的构造函数
func NewStudent(sid int, name string, score map[string]float64) Student {
    return Student{
        Sid:   sid,
        Name:  name,
        Score: score,
    }
}

// 学生管理类
type StuScoreManager struct {
    stus map[int]Student
}

// 学生管理类的构造函数
func NewStuScoreManager() *StuScoreManager {
    return &StuScoreManager{
        stus: make(map[int]Student),
    }
}

// 添加学生成绩
func (ssm StuScoreManager) addScore() {
    // 输入学号和姓名
    var sid int
    var name string
    fmt.Println("请分别输入学号,姓名,以空格隔开.如:1001 yuan")
    fmt.Scan(&sid, &name)

    _, isExist := ssm.stus[sid]
    if isExist {
        fmt.Println("该学生已经存在!")
    } else {
        // 输入成绩
        var chinese, math, english float64
        fmt.Println("请分别输入语数英成绩,以空格隔开.如:100 90 80")
        fmt.Scan(&chinese, &math, &english)
        // fmt.Println(chinese, math, english)

        // 添加学生到data中
        ssm.stus[sid] = NewStudent(sid, name, map[string]float64{
            "chinese": chinese,
            "math":    math,
            "english": english,
        })
        fmt.Println("添加成功")
    }
}

// 查看学生成绩
func (ssm StuScoreManager) checkScore() {
    fmt.Println("-----------------------------------------------------------------------------")
    for _, stu := range ssm.stus {
        fmt.Printf("学号:%-8d 姓名:%-8s 语文成绩:%-8.2f 数学成绩:%-8.2f 英语成绩:%-8.2f",
            stu.Sid, stu.Name, stu.Score["chinese"], stu.Score["math"], stu.Score["english"])
        fmt.Println()
    }
    fmt.Println("-----------------------------------------------------------------------------")
}

// 更新学生成绩
func (ssm StuScoreManager) updateScore() {
    var updateSid int
    fmt.Println("请输入更新学生成绩学号")
    fmt.Scan(&updateSid)
    updateStu, isExist := ssm.stus[updateSid]
    if isExist {
        // 输入成绩
        var chinese, math, english float64
        fmt.Println("请分别输入语数英成绩,以空格隔开.如:100 90 80")
        fmt.Scan(&chinese, &math, &english)
        // 更新data中的学生
        updateStu.Score["chinese"] = chinese
        updateStu.Score["math"] = math
        updateStu.Score["english"] = english
        fmt.Println("更新成功!")
    } else {
        fmt.Println("不存在该学生!")
    }
}

// 删除学生成绩
func (ssm StuScoreManager) deleteScore() {
    var delSid int
    fmt.Println("请输入删除学生成绩学号")
    fmt.Scan(&delSid)
    delete(ssm.stus, delSid)
    fmt.Println("删除成功!")
}

// 获取成绩
func (ssm StuScoreManager) getScore() map[int]Student {

    // 读文件
    content, err := ioutil.ReadFile("score.json")
    if err != nil {
        return nil
    }
    // 反序列化
    data := make(map[int]Student)
    json.Unmarshal(content, &data)  // 也可以直接写进ssm.stus中:json.Unmarshal(content, &ssm.stus)
    return data

}

// 保存成绩
func (ssm StuScoreManager) keepScore() {

    // json序列化
    jsonData, _ := json.Marshal(ssm.stus)
    // 写文件
    err := ioutil.WriteFile("score.json", []byte(jsonData), 0666)
    if err != nil {
        fmt.Println("write file failed, err:", err)
        return
    }
    fmt.Println("保存成绩成功!")

}

func showMenu() {
    fmt.Print(`


**************************************
  1 添加学生成绩  
  2 查看学生成绩  
  3 更新学生成绩  
  4 删除学生成绩
  5 保存
  6 退出程序    
**************************************
`)
}

func main() {

    ssm := NewStuScoreManager()
    ret := ssm.getScore()
    if ret != nil {
        ssm.stus = ret
    }

    for true {
        // 展示菜单
        showMenu()
        // 引导用户做出选择
        var choice int
        fmt.Print("请输入选择:")
        //stdin := bufio.NewReader(os.Stdin)
        //fmt.Fscan(stdin,&choice)
        fmt.Scanln(&choice)

        switch choice {
        case 1:
            ssm.addScore()
        case 2:
            ssm.checkScore()
        case 3:
            ssm.updateScore()
        case 4:
            ssm.deleteScore()
        case 5:
            ssm.keepScore()
        case 6:
            os.Exit(0)
        default:
            fmt.Println("非法输入!")
        }
    }

}