10-Go语言结构体

结构体

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

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

结构体定义

使用typestruct关键字来定义结构体,具体代码格式如下:

type 类型名 struct{
  	字段名 字段类型
  	字段名 字段类型
  	...
}

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。

举个例子,我们定一个Person的结构体,代码如下:

type Person struct{
  name string
  city string
  age int8
}

//同样的类型可以写在一行:
type Person1 struct{
  name,city string
  age 
}

这样我们就有了一个preson的自定义类型,他有name city age 三个字段。这样我们就可以使用这个peeson结构体就能方便的在程序中表示和存储人信息了。

//定义一个Student的结构体
type Student struct {
	name     string
	age      int8
	married  bool
	mapScore map[string]int
}

//定义一个order的结构体
type Order struct {
	id         int64
	proID      int64
	userId     int64
	createTime int64
}
结构体实例化

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

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

var 结构体实例 结构体类型
基本实例化:
type Person struct {
	name string
	city string
	age  int8
}

func struDemo1() {
	var p1 Person
	p1.name = "沙河娜扎"
	p1.city = "北京"
	p1.age = 18

	fmt.Printf("p1=%+v\n", p1)
	fmt.Printf("p1=%#v\n", p1)
}

//结果
p1={name:沙河娜扎 city:北京 age:18}
p1=main.Person{name:"沙河娜扎", city:"北京", age:18}

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

匿名结构体

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

//匿名结构体
func struDemo2() {
	var user struct {
		name string
		age  int
	}
	user.name = "小王子"
	user.age = 18
	fmt.Printf("%#v\n", user)
}

//结果:
struct { name string; age int }{name:"小王子", age:18}
创建指针类型结构体

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

//创建指针类型结构体
func struDemo3() {
	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语言中支持对结构体指针直接使用.来访问

func struDemo3() {
	var p2 = new(Person)

	p2.name = "小孩子"   //等价于(*p2).name
	p2.age = 28
	p2.city = "上海"
	fmt.Printf("%T\n", p2)
	fmt.Printf("p2=%#v\n", p2)
}
取结构体地址实例化
func struDemo4() {
	p3 := &Person{} //等价于 var p3 = new(Person)
	fmt.Printf("%T\n", p3)
	fmt.Printf("p3=%#v\n", p3)

	p3.name = "黑煤球"
	p3.city = "北京"
	p3.age = 27
	fmt.Printf("p3=%+v\n", p3)
}

//结果:
*main.Person
p3=&main.Person{name:"", city:"", age:0}
p3=&{name:黑煤球 city:北京 age:27}

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

结构体初始化

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

func struDemo5() {
	var p4 Person

	fmt.Printf("p4=%#v\n", p4)
}

//运行结果:
p4=main.Person{name:"", city:"", age:0}
使用键值对初始化

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

unc struDemo6() {
	p5 := Person{
		name: "不小孩",
		city: "北京",
		age:  28,
	}

	fmt.Printf("p5=%#v\n", p5) //p5=main.Person{name:"不小孩", city:"北京", age:28}
}

也可以对结构体指针进行键值对初始化,例如:

func struDemo7() {
	p6 := &Person{
		name: "小王子",
		city: "北京",
		age:  18,
	}

	fmt.Printf("p6=%#v\n", p6) // p6=&main.Person{name:"小王子", city:"北京", age:18}
}

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

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

eg:

//结构体字面量初始化
func struDemo8() {
	stu1 := Student{
		name: "王磊",
		age:  26,
		mapScore: map[string]int{
			"语文": 6,
			"数学": 7,
		},
	}

	fmt.Printf("%+v\n", stu1)

	stu2 := Student{} // map[string]int{}
	//stu2.mapScore["英语"] = 6 //会报错,因为map没有make
	fmt.Printf("%#v\n", stu2)

	stu3 := &Student{} //取地址  --》 new(Student) --> 结构体指针
	stu3.name = "玩"     Go语言中提供的语法糖,支持 结构体指针类型.属性 简写
	(*stu3).age = 34
	fmt.Printf("%+v\n", stu3)

	var stu4 = &Student{}
	stu4.name = "json"
	fmt.Printf("%+v\n", stu4)	
}

//结果:
{name:王磊 age:26 married:false mapScore:map[数学:7 语文:6]}
main.Student{name:"", age:0, married:false, mapScore:map[string]int(nil)}
&{name:玩 age:34 married:false mapScore:map[]}
&{name:json age:0 married:false mapScore:map[]}
值列表初始化

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

	// 列表初始化
	// 必须按结构体定义时候的属性顺序依次赋值
func struDemo9() {
	var stu6 = Student{
		"胡子",
		24,
		false,
		map[string]int{"语文": 100},
	}

	fmt.Printf("%+v\n", stu6)

}

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

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。
结构体指针初始化
// 结构体字面量初始化
func demo5() {
	stu4 := &Student{} // 取地址  --》 new(Student) --> 结构体指针
	(*stu4).name = "李硕"
	stu4.age = 18 // Go语言中提供的语法糖,支持 结构体指针类型.属性 简写
	fmt.Printf("%+v\n", stu4)

	// var stu5 *Student // nil
	// var stu5 = new(Student)
	var stu5 = &Student{}
	stu5.name = "jade" // (*nil).name =
	fmt.Printf("%+v\n", stu5)
	stu5 = &Student{
		name: "大都督",
	}
	stu5 = new(Student)

	// var x *int       // nil
	var x = new(int)
	*x = 100 // (*nil) = 100
	fmt.Println(x)
}
空结构体

空结构体不占用空间.

//空结构体
func struDemo10() {
	var v struct{}
	fmt.Println(unsafe.Sizeof(v)) //0
}

结构体的内存布局(进阶)
结构体大小

结构体占用连续的内存空间。

结构体占用的内存大小是由每个属性的大小和内存对齐决定的。

type Foo struct {
	a int8 //1byte
	b int8 //1byte
	c int8 //1byte
}

//结构体大小
func struDemo11() {
	var f Foo
	fmt.Println(unsafe.Sizeof(f)) //3byte

}
//结果
3
内存对齐

内存对齐的原理:CPU读取内存是以**word size(字长)**为单位,避免出现一个属性CPU分多次读取的问题。

内存对齐是编译器帮我们根据CPU和平台来自动处理的。

//内存对齐
type Bar struct {
	a int32 //4
	b int64 //8
	c bool  //1
}

func struDemo12() {
	var b1 Bar
	fmt.Println(unsafe.Sizeof(b1)) //24
}

有的同学可能会认为结构体变量b1的内存布局如下图所示,那么问题来了,结构体变量b1的大小怎么会是24呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7UGudskD-1656580917572)(/Users/alblue/Library/Application Support/typora-user-images/image-20220127093929478.png)]

很显然结构体变量b1的内存布局和上图中的并不一致,实际上的布局应该如下图所示,灰色虚线的部分就是内存对齐时的填充(padding)部分。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yeG0Gyx9-1656580917573)(/Users/alblue/Library/Application Support/typora-user-images/image-20220127094031194.png)]

因为 CPU 访问内存时,并不是逐个字节访问,而是以字(word)为单位访问。比如 64位CPU的字长(word size)为8bytes,那么CPU访问内存的单位也是8字节,每次加载的内存数据也是固定的若干字长,如8words(64bytes)、16words(128bytes)等。

对齐保证

我们利用对齐的规则合理的减小结构体的体积。

对齐系数:对于 struct 类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f)unsafe.Alignof(x) 等于其中的最大值。

我们可以通过内置的unsafe包的sizeof函数获取一个变量的大小,此外我们可以通过内置的unsafeAlignof函数获取一个变量的对齐系数,例如:

//结构体变量b1的对齐系数
	fmt.Println(unsafe.Alignof(b1)) //8
	//b1每个字段的对齐系数
	fmt.Println(unsafe.Alignof(b1.a)) //4: 表示此字段按4的倍数对齐
	fmt.Println(unsafe.Alignof(b1.b)) //8:表示此字段按8的倍数对齐
	fmt.Println(unsafe.Alignof(b1.c)) //1:表示此字段按1的倍数对齐unsafe.Alignof()的规则如下:

unsafe.Alignof()的规则如下:

  • 对于任意类型的变量 x ,unsafe.Alignof(x) 至少为 1。
  • 对于 struct 类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f)unsafe.Alignof(x) 等于其中的最大值。
  • 对于 array 类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐倍数。

在了解了上面的规则之后,就可以调整结构体字段来减小结构体大小。

//对齐保证
type Bar2 struct {
	x int32 //4
	z bool  //1
	y int64 //8
}

func struDemo13() {
	var b2 Bar2
	fmt.Println(unsafe.Sizeof(b2)) //16
}

此时结构体 Bar2 变量的内存布局示意图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DDzLYvId-1656580917573)(/Users/alblue/Library/Application Support/typora-user-images/image-20220127095343474.png)]

总结一下:在了解了Go的内存对齐规则之后,我们在日常的编码过程中,完全可以通过合理地调整结构体的字段顺序,从而优化结构体的大小。

方法

构造函数

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

type Person struct {
	name string
	city string
	age  int8
}


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


//调用构造函数
newPerson := struDemo14("王磊", "北京", 28)
	fmt.Printf("%#v\n", newPerson) //&main.Person{name:"王磊", city:"北京", age:28}
方法的定义

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

方法定义的格式如下:

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

其中:

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

举个例子:

//方法
//People结构体
type People struct {
	name string
	age  int8
}

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

//People的Dream方法
func (p People) Dream() {
	fmt.Printf("%s的梦想是在%v学好GO语言\n", p.name, p.age)
}

//调用
p1 := newPeople("不小孩", 25)
	p1.Dream()  // 不小孩的梦想是在25学好GO语言
接收者
  1. 值接收者

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

    //值类型接受者
    //定义值类型接受的方法
    func (p People) SetAge2(newage int8) {
    	p.age = newage
    	fmt.Printf("副本的年龄:%v\n", p.age)
    }
    
    //调用方法:
    p1 := newPeople("不小孩", 25)
    	fmt.Println(p1.age) //25
    	p1.SetAge2(30)      //副本的年龄:30
    	fmt.Println(p1.age) //25,原来变量的值仍旧25
    
  2. 指针接收者

    指针类型接受者由一个结构体的指针组成。由于指针的特性,调用方法时修改接受者指针的任意成员变量,在方法结束后,修改都是有效的。

    //指针类型的接受者
    //定义方法
    func (p *People) SetAge(newage int8) {
    	p.age = newage
    }
    
    //调用
    p1 := newPeople("不小孩", 25)
    	fmt.Println(p1.age) //25
    	p1.SetAge(30)       //30
    	fmt.Println(p1.age) //30 原来的变量已经被更改了
    
  3. 什么时候该使用指针类型的接受者

    1. 需要修改接收者中的值
    2. 接收者是拷贝代价比较大的大对象
    3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法

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

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

//SayHello 为MyInt添加一个SayHello的方法
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
}
结构体的匿名字段

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

**注意:**这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

//结构体的匿名字段
type Teacher struct {
	string
	int
}

func teaDemo() {
	t := Teacher{
		"小王子",
		30,
	}
	fmt.Printf("%#v\n", t)       //main.Teacher{string:"小王子", int:30}
	fmt.Println(t.string, t.int) //小王子 30
}
嵌套结构体

一个结构体可以嵌套包含一个结构体或结构体指针,就像下面的示例代码那样。

//嵌套结构体

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

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

func strudemo15() {
	user := User{
		Name:   "小王子",
		Gender: "男",
		Address: Address{
			Province: "北京",
			City:     "北京",
		},
	}
	fmt.Println(user)  //{小王子 男 {北京 北京}}
}
嵌套匿名字段

上面的user结构体嵌套的address结构体,也可以采取匿名字段的方式,例如:

//嵌套匿名字段
type User struct {
	Name    string
	Gender  string
	Address //匿名字段
}

func studemo16() {
	var user1 User
	user1.Name = "小昂子"
	user1.Gender = "女"
	user1.Address.Province = "山西" // 匿名字段默认使用类型名作为字段名
	user1.City = "大同"             // 匿名字段可以省略
	fmt.Println(user1)            //{小昂子 女 {山西 大同}}
}

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

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。

//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 user3 User
	user3.Name = "沙河娜扎"
	user3.Gender = "男"
	// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
	user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
	user3.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
结构体的继承

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

//结构体继承

//Anmial 结构体
type Animal struct {
	name string
}

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

//Dog

type Dog struct {
	Feet    int8
	*Animal //继承Anmial结构体
}

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

//调用
func Anmil() {
	d1 := Dog{
		Feet: 4,
		Animal: &Animal{
			name: "小强",
		},
	}
	d1.wang() //小强会汪汪
	d1.move() //强会动
}
结构体字段的可见性

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

结构体与JSON序列化

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

//JSON与结构体序列化和反序列化

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

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

func Json() {
	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("fail")
		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)
}

结构体标签(Tag)

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

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

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

注意事项: 为结构体编写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:   "沙河娜扎",
	}
	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":"男"}
}

补充

切片的源码

在这里插入图片描述

练习

  1. 下面的代码执行结果为?为什么?

    type student struct {  //定义一个名为student的结构体
    	name string
    	age  int
    }
    
    func main() {
    	m := make(map[string]*student) //定义一个map:key是string,value为student结构体指针类型
    	stus := []student{						//定义一个元素类型为student结构体类型的切片,并且初始化了3个元素在里面
    		{name: "小王子", age: 18},
    		{name: "娜扎", age: 23},
    		{name: "大王八", age: 9000},
    	}
    
    	for _, stu := range stus {//在每次循环的过程中操作map,添加新的键值对key为切片元素的name,value为当次循环中for range的内部变量的地址
        
    		m[stu.name] = &stu //stu地址不变,但是值会不断的变化,循环到最后一个值为{name: "大王八", age: 9000},所以内存地址指的就是这个值
    	}
    	for k, v := range m {
    		fmt.Println(k, "=>", v.name)
    	}
    }
    
    //运行结果:
    小王子 => 大王八
    娜扎 => 大王八
    大王八 => 大王八
    
    //原因分析:
    1、新键值对的value是存储内部变量stu的指针,那么就意味着,每次循环所创建的心键值对的value都指向了同一块内存地址&stu
    2、那么就知道为啥输出这个样子的,因为stu的指针指向内存地址,每次循环的时候,值都是会变的。循环到最后一项,内部value为最后一项的元素
    
    //变行1:
    
    type student struct {
    	name string
    	age  int
    }
    
    func ex() {
    	m := make(map[string]*student)
    	stus := []student{
    		{name: "小王子", age: 18},
    		{name: "娜扎", age: 23},
    		{name: "大王八", age: 9000},
    	}
    
    	for i, stu := range stus {
    		m[stu.name] = &stus[i]  //这样每次对应的内存地址就不一样了
    		// fmt.Println(m)
    	}
    	for k, v := range m {
    		fmt.Println(k, "=>", v.name)
    	}
    }
    
    
  2. 编写学生管理系统

    1. 获取用户输入:
    2. 使用“面向对象”的思维方式编写一个学生信息管理系统。
      1. 学生有id、姓名、年龄、分数等信息
      2. 程序提供展示学生列表、添加学生、编辑学生信息、删除学生等功能
    package main
    
    import (
    	"fmt"
    	"os"
    )
    
    /*使用“面向对象”的思维方式编写一个学生信息管理系统。
    1、学生有id、姓名、年龄、分数等信息
    2、程序提供展示学生列表、添加学生、编辑学生信息、删除学生等功能
    */
    
    //1、Student结构体
    type Student struct {
    	ID    int
    	Name  string
    	Age   int
    	Score int
    }
    
    //考虑有添加和编辑,考虑map
    type Class struct {
    	Stulist map[int]*Student
    }
    
    //2、提供展示学生列表、添加学生、编辑学生信息、删除学生等方法
    
    //查看学生列表方法
    func (c *Class) showList() {
    	if len(c.Stulist) == 0 {
    		fmt.Println("学生列表为空哦~")
    	} else {
    		for k, v := range c.Stulist {
    			fmt.Printf("学生id:%d 学生名字:%s 学生年龄: %d 学生分数:%d\n", k, v.Name, v.Age, v.Score)
    		}
    	}
    
    }
    
    //添加学生
    func (c *Class) addStudent() {
    	var (
    		id    int
    		name  string
    		age   int
    		score int
    	)
    
    	fmt.Print("输入学生ID:")
    	fmt.Scan(&id)
    	_, ok := (c.Stulist)[id]
    	if ok {
    		fmt.Println("该学生已经存在,不能重复添加")
    		return
    	}
    	fmt.Print("输入学生名字:")
    	fmt.Scan(&name)
    	fmt.Print("输入学生年龄:")
    	fmt.Scan(&age)
    	fmt.Print("输入学生分数:")
    	fmt.Scan(&score)
    	stu := &Student{
    		ID:    id,
    		Name:  name,
    		Age:   age,
    		Score: score,
    	}
    	c.Stulist[id] = stu
    	fmt.Printf("%s同学添加成功~\n", stu.Name)
    
    }
    
    //编辑学生信息
    func (c *Class) editStudent() {
    	var (
    		id    int
    		name  string
    		age   int
    		score int
    	)
    	fmt.Print("请输入修改学生的id:")
    	fmt.Scan(&id)
    	_, ok := c.Stulist[id]
    	if !ok {
    		fmt.Println("该学生id无效,请重新输入有效id")
    		return
    	}
    	fmt.Print("请输入编辑后的学生名字,年龄,分数")
    	fmt.Scan(&name, &age, &score)
    	stu := &Student{
    		Name:  name,
    		Age:   age,
    		Score: score,
    	}
    	c.Stulist[id] = stu
    
    }
    
    //删除学生
    func (c *Class) deleteStudent() {
    	var id int
    	fmt.Print("请输入删除学生id:")
    	fmt.Scan(&id)
    	_, ok := c.Stulist[id]
    	if !ok {
    		fmt.Println("输入学生id不存在,请重新输入")
    		return
    	}
    	delete(c.Stulist, id)
    	fmt.Println("删除成功")
    }
    
    //3、写主执行函数
    func main() {
    	c := &Class{
    		Stulist: make(map[int]*Student),
    	}
    	for {
    		var input int
    		fmt.Print(`
    		欢迎访问学生管理系统!
    		1、查看所有学生列表 2、添加学生 3、编辑学生信息 4、删除学生 5、退出
    	
    		宝子们,请选择你要操作编号:`)
    		fmt.Scan(&input)
    		switch input {
    		case 1:
    			c.showList()
    		case 2:
    			c.addStudent()
    		case 3:
    			c.editStudent()
    		case 4:
    			c.deleteStudent()
    		case 5:
    			os.Exit(0)
    		}
    	}
    
    }
    
  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值