结构体初始化为0_Go语言学习笔记(第九章) 结构体

e546d27afca6b0c74633a1df39736f7f.png


Go语言基础之结构体

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。试图表示一个现实世界中的实体。

类型别名和自定义类型

自定义类型

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

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

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

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

类型别名

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

类型别名规定:TypeAlias只是Type的别名,本质上TypeAliasType是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

type TypeAlias = Type

我们之前见过的 runebyte 就是类型别名,他们的定义如下:

type byte = uint8type rune = int32

类型定义和类型别名的区别

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

// 类型定义type NewInt int// 类型别名type MyInt = intfunc main() {  var a NewInt  var b MyInt    fmt.Printf("type of a:%T\n", a)  fmt.Printf("type of b:%T\n", b)}

输出:

type of a:main.NewInttype of b:int

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

结构体

结构体由一系列命名的元素组成,这些元素又被称为字段,每个字段都有一个名称和一个类型。

结构体的目的就是把数据聚集在一起,以便能够更加便捷地操作这些数据。结构体的概念在 C 语言里很常见,被称为 struct。Go 中的结构体也是 struct。Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 new 函数来创建。

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

结构体的定义

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

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

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。

  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。

  • 字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个 person(人)结构体,代码如下:

type person struct {  name string  city string  age  int8}

同样类型的字段也可以写在一行:

type person1 struct {  name, city string  age        int8}

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

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

结构体实例化

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

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

var 结构体实例 结构体类型
基本实例化

举个例子:

package mainimport (  "fmt")// 结构体type person struct {  name, city string  age        int  hobby      []string}func main() {  var p person  p.name = "蜘蛛侠"  p.age = 18  p.hobby = []string{"打怪兽", "沟女"}  p.city = "纽约"  fmt.Println(p)}

运行:

{蜘蛛侠 纽约 18 [打怪兽 沟女]}

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

匿名结构体

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

package main     import (    "fmt")     func main() {    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 关键字对结构体进行实例化,得到的是结构体的地址。格式如下:

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

从打印的结果中我们可以看出 p2 是一个结构体指针。

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

var p2 = new(person)p2.name = "蜘蛛侠"p2.age = 28p2.city = "纽约"fmt.Printf("p2=%#v\n", p2) // p2=&main.person{name:"蜘蛛侠", city:"纽约", age:28}
取结构体的地址实例化

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

结构体实例 := &结构体类型{}

取地址实例化是最广泛的一种结构体实例化方式:

p3 := &person{}fmt.Printf("%T\n", p3)     //*main.personfmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}p3.name = "浩克"p3.age = 30p3.city = "华盛顿"fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"浩克", city:"华盛顿", age:30}

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

Go语言和C/C++

在 C/C++ 语言中,使用 new 实例化类型后,访问其成员变量时必须使用->操作符。
在Go语言中,访问结构体指针的成员变量时可以继续使用.,这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术。

结构体初始化

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

结构体初始化的几种方式:

方式一:通过 var 声明结构体

type person struct {  name string  city string  age  int8}func main() {  var p4 person  fmt.Printf("p4=%#v\n", p4) // p4=main.person{name:"", city:"", age:0}}

在 Go 语言中当一个变量被声明的时候,系统会自动初始化它的默认值,比如 int 被初始化为 0,指针为 nil。
var 声明同样也会为结构体类型的数据分配内存,所以我们才能像上一段代码中那样,在声明了 var p4 person 之后就能直接给他的字段进行赋值。

方式二:使用 new

使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T)

type struct1 struct {    i1 int    f1 float32    str string} func main() {    ms := new(struct1)    ms.i1 = 10    ms.f1 = 15.5    ms.str= "Chris"     fmt.Printf("The int is: %d\n", ms.i1)    fmt.Printf("The float is: %f\n", ms.f1)    fmt.Printf("The string is: %s\n", ms.str)    fmt.Println(ms)}

与面向对象语言相同,使用点操作符可以给字段赋值:structname.fieldname = value。同样的,使用点操作符可以获取结构体字段的值:structname.fieldname

方式三:使用字面量

type Person struct {    name string    age int    address string} func main() {    var p1 Person    p1 = Person{"lisi", 30, "shanghai"}   //方式A    p2 := Person{address:"beijing", age:25, name:"wangwu"} //方式B    p3 := Person{address:"NewYork"} //方式C}

在(方式A)中,值必须以字段在结构体定义时的顺序给出。(方式B)是在值前面加上了字段名和冒号,这种方式下值的顺序不必一致,并且某些字段还可以被忽略掉,就想(方式C)那样。除了上面这三种方式外,还有一种初始化结构体实体更简短和常用的方式,如下:

ms := &Person{"name", 20, "bj"}ms2 := &Person{name:"zhangsan"}

&Person{a, b, c} 是一种简写,底层仍会调用 new(),这里值的顺序必须按照字段顺序来写,同样它也可以使用在值前面加上字段名和冒号的写法。

表达式 new(Type)和 &Type{} 是等价的。

几种初始化方式之间的区别

到目前为止,我们已经了解了三种初始化结构体的方式:

//第一种,在Go语言中,可以直接以 var 的方式声明结构体即可完成实例化var t Tt.a = 1t.b = 2 //第二种,使用 new() 实例化t := new(T) //第三种,使用字面量初始化t := T{a, b}t := &T{} //等效于 new(T)

使用 var t T 会给 t 分配内存,并零值化内存,但是这个时候的 t 的类型是 T
使用 new 关键字时 t := new(T),变量 t 则是一个指向 T 的指针。
从内存布局上来看,我们就能看出这三种初始化方式的区别:
使用 var 声明:

0deb05f46e36e0fa5eae9e7a5676ca20.png

使用 new 初始化:

1240a4a69822861793cca42f6ac97a33.png

使用结构体字面量初始化:

3036ad248752a7dcaa4d9b5fe6b1720b.png

下面来看一个具体的例子:

package mainimport "fmt" type Person struct { name string age int} func main() { var p1 Person p1.name = "zhangsan" p1.age = 18 fmt.Printf("This is %s, %d years old\n", p1.name, p1.age)  p2 := new(Person) p2.name = "lisi" p2.age = 20 (*p2).age = 23 //这种写法也是合法的 fmt.Printf("This is %s, %d years old\n", p2.name, p2.age)  p3 := Person{"wangwu", 25} fmt.Printf("This is %s, %d years old\n", p3.name, p3.age)}

输出:

This is zhangsan, 18 years oldThis is lisi, 23 years oldThis is wangwu, 25 years old

上面例子的第二种情况,虽然 p2 是指针类型,但我们仍然可以像 p2.age = 23 这样赋值,不需要像 C++ 中那样使用 -> 操作符,Go 会自动进行转换。
注意也可以先通过 * 操作符来获取指针所指向的内容,再进行赋值:(*p2).age = 23

结构体内存布局

Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go 语言中的指针很像。下面的例子清晰地说明了这些情况:

type Rect1 struct {Min, Max Point }type Rect2 struct {Min, Max *Point }

3764173e7b8a09f8d223351d45e40859.png

空结构体

空结构体是不占用空间的。

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

构造函数

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

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

调用构造函数:

p9 := newPerson("蜘蛛侠", "纽约", 18)fmt.Printf("%#v\n", p9) //&main.person{name:"蜘蛛侠", city:"纽约", age:18}

方法和接收者

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

方法的定义格式如下:

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

其中,

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

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

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

举个例子:

// Person 结构体type Person struct {  name string  age  int8}// NewPerson 构造函数func NewPerson(name string, age int8) *Person {  return &Person{    name: name,    age:  age,  }}// Dream Person做梦的方法func (p Person) Dream() {  fmt.Printf("%s的梦想是学好Go语言!\n", p.name)}func main() {  p1 := NewPerson("奇异博士", 25)  p1.Dream()}

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

指针类型的接收者

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

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

调用该方法:

func main() {  p1 := NewPerson("蜘蛛侠", 25)  fmt.Println(p1.age) // 25  p1.SetAge(30)  fmt.Println(p1.age) // 30}
值类型的接收者

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

// SetAge2 设置p的年龄// 使用值接收者func (p Person) SetAge2(newAge int8) {  p.age = newAge}func main() {  p1 := NewPerson("蜘蛛侠", 25)  p1.Dream()  fmt.Println(p1.age) // 25  p1.SetAge2(30) // (*p1).SetAge2(30)  fmt.Println(p1.age) // 25}
什么时候应该使用指针类型接收者
  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}

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

结构体的匿名字段

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

// Person 结构体Person类型type Person struct {  string  int}func main() {  p1 := Person{    "蜘蛛侠",    18,  }  fmt.Printf("%#v\n", p1)        //main.Person{string:"纽约", int:18}  fmt.Println(p1.string, p1.int) //纽约 18}

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

嵌套结构体

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

//Address 地址结构体type Address struct {  Province string  City     string}//User 用户结构体type User struct {  Name    string  Gender  string  Address Address}func main() {  user1 := User{    Name:   "蜘蛛侠",    Gender: "男",    Address: Address{      Province: "纽约",      City:     "华盛顿",    },  }  fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"蜘蛛侠", Gender:"男", Address:main.Address{Province:"纽约", City:"华盛顿"}}}
嵌套匿名字段

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

// Address 地址结构体type Address struct {  Province string  City     string}//User 用户结构体type User struct {  Name    string  Gender  string  Address //匿名字段}func main() {  var user2 User  user2.Name = "蜘蛛侠"  user2.Gender = "男"  user2.Address.Province = "纽约"    // 匿名字段默认使用类型名作为字段名  user2.City = "华盛顿"                // 匿名字段可以省略  fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"蜘蛛侠", 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 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语言中使用结构体也可以实现其他编程语言中面向对象的继承。

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

结构体字段的可见性

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

结构体与JSON序列化

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

结构体标签(Tag)

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

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

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

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

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

package mainimport (  "encoding/json"  "fmt")/*  结构体与 json  Go 语言中结构体的变量 -> json 格式的字符串(序列化)  json 格式的字符串 -> Go 语言中结构体的变量(反序列化)*/type person struct {  // 结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。  // 此处如果把结构体 person 传到 json.Marshal(p1) 方法中去,该方法无法获取到字段,因为是私有的。  // name string  // age  int  // 如果别人接收我们发送的 json 中,别人就是不想要我们字段的首字母大写,我们可以给其制定一下Tag。  Name string `json:"name"`  Age  int    `json:"age"`}func main() {  p1 := person{    Name: "蜘蛛侠",    Age:  18,  }  // 序列化方法:json.Marshal(value)  result, err := json.Marshal(p1)  if err != nil {    fmt.Printf("Marshal fail, err: %v\n", err)    return  }  fmt.Printf("%v\n", string(result))  // 反序列化方法:json.Unmarshal([]byte, value)  // 第一个参数:[]byte - 传进来的 json 字符串  // 第二个参数:value - 要传到哪里去,用一个结构体接收(注意此处要传递结构体的指针)  str := `{"name":"蜘蛛侠","age":18}`  var p2 person  json.Unmarshal([]byte(str), &p2) // 传指针是为了能在 json.Unmarshal() 方法内部去修改 p2 的值  fmt.Printf("%#v\n", p2)}

结构体和方法补充知识点

因为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: "钢铁侠", age: 18}  data := []string{"吃饭", "睡觉", "打灭霸"}  p1.SetDreams(data)  // 你真的想要修改 p1.dreams 吗?  data[1] = "不睡觉"  fmt.Println(p1.dreams)  // ?}

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

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

同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。

总结

这个程序将包含所有结构体的知识点:

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

  1. 学生有id、姓名、年龄、分数等信息

  2. 程序提供展示学生列表、添加学生、编辑学生信息、删除学生等功能

该实现有两种版本,我们先来看看函数版的解法:

package mainimport (  "fmt"  "os")/*  函数版学生管理系统  功能:查看、新增、删除学生*/var (  allStudents map[int64]*student // 声明一个存放 student 的变量)type student struct {  id   int64  name string}// student 构造体的构造函数func newStudent(id int64, name string) *student {  return &student{    id:   id,    name: name,  }}func showAllStudents() {  // 把所有的学生打印出来  for k, v := range allStudents {    fmt.Printf("学号:%d | 姓名:%s\n", k, v.name)  }}func addStudent() {  // 向 allStudents 中添加一个学生  // 1.创建一个新学生  var (    id   int64    name string  )  fmt.Print("请输入学生学号:")  fmt.Scanln(&id)  fmt.Print("请输入学生姓名:")  fmt.Scanln(&name)  // 调用 student 的构造函数  newStu := newStudent(id, name)  // 2.追加到 allStudents 中  allStudents[id] = newStu}func delStudent() {  // 根据学号删除 allStudents 中的一个学生  var (    deleteID int64  )  fmt.Print("请输入要删除的学生的学号:")  fmt.Scanln(&deleteID)  delete(allStudents, deleteID)}func main() {  allStudents = make(map[int64]*student, 50) // 初始化  for {    // 1.打印菜单    fmt.Println("欢迎光临学生管理系统")    fmt.Println(`      1.查看所有学生      2.新增学生      3.删除学生      4.退出    `)    fmt.Print("请输入你的操作:")    // 2.等待用户选择要做什么    var choice int    fmt.Scanln(&choice)    fmt.Printf("你选择了第 %d 项\n", choice)    // 3.执行对应函数    switch choice {    case 1:      showAllStudents()    case 2:      addStudent()    case 3:      delStudent()    case 4:      os.Exit(1)    default:      fmt.Println("净整些没用的")    }  }}

再来看看结构体版的解法:

package main/*  结构体版学生管理系统*/// 学生结构体type student struct {  id   int64  name string}// 学生管理者结构体type studentManager struct {  allStudents map[string]student}// 查看学生方法func (s studentManager) showStudents() {}// 增加学生方法func (s studentManager) addStudent() {}// 修改学生方法func (s studentManager) editStudent() {}// 删除学生方法func (s studentManager) delStudent() {}func main() {}

我们的解题思路是大概是这样,这个架子先搭起来。

但是我发现把所有的这些方法都写在一个文件里,着实太乱了。不好维护,我们把它们拆成不同的文件实现:

我把对学生操作的方法单独用一个文件来写:

main.go

package mainimport (  "fmt"  "os")/*  结构体版学生管理系统*/var sm studentManager // 声明一个全局的学生管理者对象// 菜单函数func showMenu() {  fmt.Println("******欢迎光临学生管理系统******")  fmt.Println(`      1、查看所有学生      2、添加学生      3、修改学生      4、删除学生      5、退出    `)}func main() {  // 修改全局的那个变量  sm = studentManager{    allStudents: make(map[int64]student, 100),  }  for {    showMenu()    // 等待用户输入    fmt.Print("请输入要执行的操作:")    var choice int    fmt.Scanln(&choice)    fmt.Println("你输入的是:", choice)    switch choice {    case 1:      sm.showStudents()    case 2:      sm.addStudent()    case 3:      sm.editStudent()    case 4:      sm.delStudent()    case 5:      os.Exit(1)    default:      fmt.Println("是不是傻?")    }  }}

student_manager.go

package mainimport "fmt"// 学生结构体type student struct {  id   int64  name string}// 学生管理者结构体type studentManager struct {  allStudents map[int64]student}// 查看学生方法func (s studentManager) showStudents() {  // 从 map 中把所有的学生遍历出来  // stu 是具体每个学生  for _, stu := range s.allStudents {    fmt.Printf("学号:%d,姓名:%s\n", stu.id, stu.name)  }}// 增加学生方法func (s studentManager) addStudent() {  // 根据用户输入的内容创建一个新的学生  var (    stuID   int64    stuName string  )  // 获取用户输入  fmt.Print("请输入学生学号:")  fmt.Scanln(&stuID)  fmt.Print("请输入学生姓名:")  fmt.Scanln(&stuName)  // 根据用户输入创建结构体对象  newStu := student{    id:   stuID,    name: stuName,  }  // 把新学生结构体放入 map 中  s.allStudents[newStu.id] = newStu  fmt.Println("添加成功")}// 修改学生方法func (s studentManager) editStudent() {  // 获取用户输入的学号  var stuID int64  fmt.Print("请输入学生学号:")  fmt.Scanln(&stuID)  // 展示学号对应的学生信息,如果没有提示查无此人  stuObj, ok := s.allStudents[stuID] // ok = true 表示有这个key  if !ok {    fmt.Println("查无此人")    return  }  fmt.Printf("学生信息:\n学号:%d,姓名:%s\n", stuObj.id, stuObj.name)  // 输入修改的学生名  fmt.Print("请输入学生的新名字:")  var newName string  fmt.Scanln(&newName)  // 更新学生姓名  stuObj.name = newName  s.allStudents[stuID] = stuObj // 更新 map 中的学生信息  fmt.Println("修改成功")}// 删除学生方法func (s studentManager) delStudent() {  // 输入学生的学号  fmt.Print("请输入要删除的学生学号:")  var stuID int64  fmt.Scanln(&stuID)  // 查询学生的信息,不存在则返回查无此人  _, ok := s.allStudents[stuID]  if !ok {    fmt.Println("查无此人")    return  }  // 存在则在 map 中将学生信息删除  delete(s.allStudents, stuID)  fmt.Println("删除成功")}

里面的每一步我都注释了,应该就是这样的解题思路。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值