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/

猜你喜欢

转载自www.cnblogs.com/huiyichanmian/p/12787788.html