Go -- 结构体

  • Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

一、类型别名和自定义类型

1、自定义类型

  • 在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型
  • Go语言中可以使用type关键字来定义自定义类型(基于基本数据类型自己创建一个类型)。
  • 自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。
  • 格式:type 自定义类型 基本类型

示例:

//将MyInt定义为int类型
type Myint int
func main() {
	var n Myint
	n = 100
	fmt.Printf("%T\n", n)    // main.Myint , 输出类型为自定义的Myint类型
}

2、类型别名

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

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

  • 格式type 别名 = 基本类型

示例:

type TypeAlias = int

func main() {
	var n TypeAlias
	n = 100
	fmt.Printf("%T\n", n)   // int 类型,类型别名只是设置了别名,并没有产生新的类型
}

二、结构体

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

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

  • 结构体的用途:用来解决代码中无法定义有着多维度的个体的问题(类似python的类)

1、结构体的定义

  • 使用type和struct关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}
# 类型名:标识自定义结构体的名称,在同一个包内不能重复。
# 字段名:表示结构体字段名。结构体中的字段名必须唯一。
# 字段类型:表示结构体字段的具体类型。

示例:定义一个Person结构体

// 定义一个类型为struct的自定义类型Person,在这个结构体内,定义人的各种属性
type Person struct {
	name   string
	age    int
	hobby  []string
	gender string
}

func main() {
	var p1 Person
	p1.name = "小明"
	p1.gender = "男"
	p1.age = 20
	p1.hobby = []string{"羽毛球", "足球"}

	fmt.Println(p1)      // {小明 20 [羽毛球 足球] 男}
	fmt.Println(p1.name) // 小明    , 通过.的方式,调用结构体内的变量
	fmt.Printf("%T\n", p1)  // main.Person ,是一个自定义类型
}

2、结构体实例化

  • 只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
  • 结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
  • 格式 var 结构体实例 结构体类型

2.1、基本实例化

  • 示例1:先用var实例化,后初始化值
// 定义一个类型为struct的自定义类型Person,在这个结构体内,定义人的各种属性
type Person struct {
	name   string
	age    int
	hobby  []string
	gender string
}

func main() {
	var p1 Person
	p1.name = "小明"
	p1.gender = "男"
	p1.age = 20
	p1.hobby = []string{"羽毛球", "足球"}

	fmt.Println(p1)      // {小明 20 [羽毛球 足球] 男}
	fmt.Println(p1.name) // 小明    , 通过.的方式,调用结构体内的变量
	fmt.Printf("%T\n", p1)  // main.Person ,是一个自定义类型
}
  • 示例2:在实例化的同时初始化
type Person struct {
	name   string
	age    int
	gender string
}

func main() {
	// 利用var在实例化同时初始化
	var p1 = Person{
		name:   "小明",
		gender: "男",
		age:    18,
	}
	// 利用:=
	p2 := Person{
		name:   "小网",
		gender: "男",
		age:    18,
	}
	// 初始化时,可以不写key,但是值的顺序一定要和声明时的key一致
	p3 := Person{"小鬼", 20, "女",}
	
	type Person struct {
	name   string
	age    int8
	dreams []string
}

// 定义的同时赋值
var p4 = struct {
	name string
	age  int
}{"abc", 10}
	
	fmt.Println(p1.name)  // 小明
	fmt.Println(p2.name)  // 小网
	fmt.Println(p3.name)  // 小鬼 
	fmt.Printf("%#v\n", p)   // struct { name string; age int }{name:"abc", age:10}
}

2.2、匿名结构体

  • 在定义一些临时数据结构等场景下还可以使用匿名结构体。(用完之后就不用了的场景)

示例:

func main() {
	// 利用var关键字直接定义struct匿名结构体类型
	var p1 struct {
		name   string
		age    int
		gender string
	}
	p1.age = 20
	p1.gender = "男"
	p1.name = "小王"
	fmt.Printf("%T\n", p1)    // struct { name string; age int; gender string } ,输出匿名结构体类型
}

2.3、结构体是值类型的数据

  • 结构体是值类型,在传参到函数内进行修改,不会改变原有的值

示例

// 定义一个Person结构体
type person struct {
	name, gender string
	age          int
}

func f1(x person) {
	x.name = "小宝"
	x.gender = "女"
	fmt.Println("在函数f1内:", x.name, x.gender)
}

func main() {
	var p person
	p.name = "小明"
	p.gender = "男"
	f1(p)
	fmt.Println("在全局:", p.name, p.gender)
}

输出结果

在函数f1内: 小宝 女
在全局: 小明 男

2.4、创建指针类型结构体

  • 有两种方式:使用new 或者 &
  • 我们可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
var p2 = new(person)
fmt.Printf("%T\n", p2)     //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}

示例:

// 定义一个Persion结构体
type person struct {
	name, gender string
	age          int
}

func f(x *person) {
	x.name = "小红"
	x.gender = "女"
	fmt.Println("在函数f1内:", x.name, x.gender)
}

func main() {
	var p2 = new(person)
	fmt.Printf("%T\n", p2)     // *main.person
	fmt.Printf("p2=%#v\n", p2) // p2=&main.person{name:"", gender:"", age:0}   ,是一个指针类型结构体
	f(p2)                      // 在函数f1内: 小红 女
}
  • 对指针类型结构体进行修改,是基于内存地址进行修改,可以改变全局的值(实现在函数内部修改外部变量)
    示例:
// 定义一个Person结构体
type person struct {
	name, gender string
	age          int
}

// 定义函数f,传入的参数是指针类型的结构体
func f(x *person) {
	x.name = "小红"
	x.gender = "女"
	fmt.Println("在函数f1内:", x.name, x.gender)
}

func main() {
	var p person
	p.name = "小明"
	p.gender = "男"
	f(&p)
	fmt.Println("在全局:", p.name, p.gender)
}

输出结果:传入的是内存地址,基于内存地址修改值,原有的值也发生改变

在函数f1内: 小红 女
在全局: 小红 女

2.5、取结构体的地址实例化(这种比较常用)

  • 使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
func main() {
	// &Person 相当于 new(Person)
	p4 := &Person{
		name:   "小网",
		gender: "男",
		age:    18,
	}

	fmt.Printf("p3=%#v\n", p4)   // p3=&main.Person{name:"小网", age:18, gender:"男"}
}

3、结构体内存布局

  • 结构体占用一块连续的内存。(即 结构体内部字段内存地址是连续的)
  • 空结构体是不占用空间的。

示例:

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 0xc000014098
	fmt.Printf("n.b %p\n", &n.b)  // n.b 0xc000014099
	fmt.Printf("n.c %p\n", &n.c)  // n.c 0xc00001409a
	fmt.Printf("n.d %p\n", &n.d)  // n.d 0xc00001409b
}
  • 空结构体是不占用空间的。
var v struct{}
fmt.Println(unsafe.Sizeof(v))  // 0

【进阶知识点】关于Go语言中的内存对齐推荐阅读: 在Go 中恰到好处的内存对齐

4、构造函数

  • Go语言的结构体没有构造函数,我们可以自己实现。 (约定成俗,构造函数的命名以new为开头)
  • 注意点:因为struct是值类型,如果结构体比较复杂的话(比如内部的变量很多),值拷贝性能开销会比较大,所以应该构造函数返回的是结构体指针类型。
    • 当我们的结构体比较简单,可以直接返回结构体(例如person),
    • 当我们的结构体比较复杂时,构造函数应该返回结构体指针(例如 *person),指针永远是16进制的内存地址,占用内存资源较少

示例1:创建一个构造函数newPerson(),返回一个person类型的结构体

// 定义 person 类型结构体
type person struct {
	name string
	age  int
}
// 约定成俗,构造函数的名称为new开头,即newPerson()
func nwePerson(name string, age int) person {
	return person{
		name: name,
		age:  age,
	}
}
func main() {
	p1 := nwePerson("小王", 20)
	p2 := nwePerson("肖工", 56)
	fmt.Println(p1)
	fmt.Println(p2)
}

输出结果

{小王 20}
{肖工 56}

示例2:创建一个构造函数newPerson(),返回一个 *person 指针类型的结构体

type Person struct {
	name   string
	age    int
	gender string
}

type person struct {
	name string
	age  int
}

func nwePerson(name string, age int) *person {
	return &person{
		name: name,
		age:  age,
	}
}
func main() {
	p1 := nwePerson("小王", 20)
	p2 := nwePerson("肖工", 56)
	fmt.Println(p1)
	fmt.Println(p2)
}

5、方法和接收者

5.1、认识方法

  • Go语言中的方法(Method)是一种作用于特定类型变量的函数。(注意:方法只能适用于自定义的类型)
  • 这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
  • 方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

方法的定义格式如下:

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

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

示例:

type person struct {
	name string
	age  int
}
// 构造函数
func nwePerson(name string, age int) *person {
	return &person{
		name: name,
		age:  age,
	}
}
// 定义一个swimming方法; 注意:方法只能适用于自定义的类型(如这里的person添加方法)
func (p person) swimming() {
	fmt.Printf("%s 在游泳", p.name)
}

func main() {
	p1 := nwePerson("小王", 20)  // 构造一个实例p1
	p1.swimming()  // p1调用swimming方法
}

  • 补充知识: 在Go语言中,如果表示符首字母是大写的,就表示对外部包可见(暴露的,公有的),一般定义了公有的标识符,我们都需要对其进行注释
    示例
# Person 这是一个人的结构体 (该结构体在后面包的导入中是可以给外部包使用的,但是如果命名为persion就不能给外部使用了)
type Person struct {
	name string
	age  int
}

5.2、指针类型的接收者

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

例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。


type Person struct {
	name string
	age  int8
}

func nwePerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

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

func main() {
	p1 := nwePerson("小王", 20)
	fmt.Println("修改前年龄:", p1.age)
	p1.SetAge(22)
	fmt.Println("修改后年龄:", p1.age)
}

输出结果

修改前年龄: 20
修改后年龄: 22

5.3、值类型的接收者

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

示例:


type Person struct {
	name string
	age  int8
}

func nwePerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

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

func main() {
	p1 := nwePerson("小王", 20)
	fmt.Println("修改前年龄:", p1.age)
	p1.SetAge(22)
	fmt.Println("修改后年龄:", p1.age)
}

输出结果

修改前年龄: 20
修改后年龄: 20

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

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

6、任意类型添加方法

  • 在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
    • 方法是只能给自定义的特殊类型添加的,基础类型(如int,string)是无法添加方法的,因为可以基于基础类型自定义新类型,然后再添加方法
    • 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

示例:

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

//SayHello 为MyInt添加一个SayHello的方法
func (m MyCustom) SayHello() {
	fmt.Println("Hello, 我是一个自定义类型")
}
func main() {
	m1 := MyCustom(100)             // 对100的int类型进行类型转换 
	m1.SayHello()                   // Hello, 我是一个自定义类型
	fmt.Printf("%#v  %T\n", m1, m1) // 100  main.MyCustom
}

补充: 不能给别的包内的自定义类型添加方法,如果需要给别的包中的自定义类型添加方法,可以在当前包内基于别的包的自定义类型再定义一个类型,然后添加方法

7、结构体的匿名字段(不常用)

  • 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
  • 注意: 这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
    • 适用于字段较少,且类型不重复的场景

示例

//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 
}

8、嵌套结构体

  • 一个结构体中可以嵌套包含另一个结构体或结构体指针(即把另外一个结构体嵌套在一个结构体中)

8.1、常规嵌套结构体

示例如下:person 和 company 这两个结构体都包含 city城市、province省 这两个字段,那么我们就可以再建一个address结构体(包含city 、province 这两个字段),然后在person 和 company 这两个结构体引入address这个结构体即可

/* 原结构体样式
type person struct {
	name string
	city string
	province string
	age int
}

type company struct{
	name string
	city string
	province string
}
*/

// ----------------------引入嵌套后 ------------------
/*新建一个结构体*/
type address struct {
	city string
	province string
}
/*引入新建的结构体*/
type person struct {
	name string
	age int
	addr address   // 嵌套引入,命名字段为addr,类型为address
}

type company struct{
	name string
	addr address
}

func main() {
	p1 := person{
		name: "小王",
		age:  18,
		addr: address{
			province: "江苏",
			city:     "南京",
		},
	}
	fmt.Printf("人员:%#v\n", p1) // 人员:main.person{name:"小王", age:18, addr:main.address{city:"南京", province:"江苏"}}
	fmt.Println(p1.name, p1.addr.province, p1.addr.city) // 小王 江苏 南京
}

8.2、嵌套结构体匿名字段

  • 上面person结构体中嵌套的address结构体也可以采用匿名字段的方式 (即不给引入的结构体字段命名)
  • 采用匿名的好处是,调用嵌套结构体内的字段时,可以直接用当前结构体.嵌套结构体内部字段的方式调用
    • 当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。
  • 注意:匿名并不是没有名称,而是以结构体类型为名称
    • 因此以下面示例为例,初始化时是address: address {...}

示例:嵌套的结构体address引入为person的字段时不进行命名,直接写结构体类型

type address struct {
	city     string
	province string
}
type person struct {
	name string
	age  int
	address     // 嵌套的结构体不进行命名,直接写结构体类型
}

func main() {
	p1 := person{
		name: "小王",
		age:  18,
		address: address{   // 注意:这里的结构体初始化方式需要修改成这样
			province: "江苏",
			city:     "南京",
		},
	}
	fmt.Printf("人员:%#v\n", p1)  // 人员:main.person{name:"小王", age:18, addr:main.address{city:"南京", province:"江苏"}}
	fmt.Println(p1.name, p1.address.province, p1.address.city) // 小王 江苏 南京
	fmt.Println(p1.name, p1.province, p1.city)    // 小王 江苏 南京 , 这时引入address中的procince可以直接用p1.province来调用
}

8.3、嵌套结构体的字段名冲突

  • 嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
  • 举例说明:当我们结构体(如下的person)中引入两个以上结构体,并且嵌套的结构体中字段存在重复,这种情况下调用嵌套结构体内字段时,就不能采用 当前结构体.嵌套结构体内部字段 这种简写方式了,应该写完整 当前结构体.嵌套结构体.嵌套结构体内部字段

示例:

type address struct {
	city     string
	province string
}

type workPlace struct {
	city     string
	province string
}
type person struct {
	name string
	age  int
	workPlace
	address
}

func main() {
	p1 := person{
		name: "小王",
		age:  18,
		address: address{
			province: "江苏",
			city:     "南京",
		},
	}
	fmt.Printf("人员:%#v\n", p1)                                 // 人员:main.person{name:"小王", age:18, addr:main.address{city:"南京", province:"江苏"}}
	fmt.Println(p1.name, p1.address.province, p1.address.city) // 小王 江苏 南京
	/* fmt.Println(p1.name, p1.province, p1.city)       // 这种情况就不可以采用简写的方式了,需要写全 */
}

9、结构体的“继承”

  • Go语言中是没有继承这个概念的,但是可以利用结构体来模拟实现继承

示例:定义一个animal动物类,再定义一个dog类,然后dog类去继承animal类

// 定义一个animal动物结构体(也可以看成动物类)
type animal struct {
	name string
}

// 给animal动物实现一个"移动"的方法
func (a animal) move() {
	fmt.Printf("%s 是动物,会动\n", a.name)
}

// 定义一个"狗"这个类
type dog struct {
	feet uint8
	animal
}

// 给狗实现一个"汪汪叫"的方法
func (d dog) wang() {
	fmt.Printf("%s 会汪汪叫\n", d.name)
}

func main() {
	// 实例化一个狗的实例
	d1 := dog{
		animal: animal{name: "白狗"},
		feet:   4,
	}
	d1.wang()
	d1.move() // 因为dog类将animal类的所有方法都继承过来了,所以animal能用的方法dog也能用(此时 d1.name = animal.name)
}

10、结构体与JSON序列化

  • JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。
  • JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
  • 序列化与反序列化:
    • 序列化:Go语言中的结构体变量------> json格式的字符串, 提供给其他语言识别
    • 反序列化:json 格式字符串 ----> Go语言能识别的结构体变量

10.1、序列化

10.1.1、 错误示例
如下示例,得到的结果是空的

  • 原因是Go的结构体字段的可见性问题,结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
  • 我们是要将当前包内结构体属性传入到 encoding/json 这个包内调用Marshal方法进行格式化,因此需要写成大写
  • 利用json.Marshal() 方法进行序列化,()内传入实例化的结构体,返回byte类型的值 和 error错误信息。
import (
	"encoding/json"
	"fmt"
)

type person struct {
	name string
	age  uint8
}

func main() {
	// 实例化一个person对象
	p1 := person{
		name: "小王",
		age:  18,
	}
	b, err := json.Marshal(p1) // json.Marshal()会对传入的结构体实例进行格式化,返回的是byte数据和错误信息
	if err != nil {            // 添加判断,如果接收的错误信息不为空,则打印出信息并退出执行
		fmt.Printf("marshal err: %v\n", err)
		return
	}
	fmt.Println(string(b)) // 将byte类型数据转换成string类型并打印(由于byte本身就是string类型,因此可以强转成string)
}

输出结果

{}

10.1.2、 对错误示例进行修改

  • 将内部属性名称小写的改成大写开头即可

import (
	"encoding/json"
	"fmt"
)

type person struct {
	Name string
	Age  uint8
}

func main() {
	// 实例化一个person对象
	p1 := person{
		Name: "小王",
		Age:  18,
	}
	b, err := json.Marshal(p1) // json.Marshal()会对传入的结构体实例进行格式化,返回的是byte数据和错误信息
	if err != nil {            // 添加判断,如果接收的错误信息不为空,则打印出信息并退出执行
		fmt.Printf("marshal err: %v\n", err)
		return
	}
	fmt.Println(string(b)) // 将byte类型数据转换成string类型并打印(由于byte本身就是string类型,因此可以强转成string)
}

输出结果:

{"Name":"小王","Age":18}

10.2、结构体标签(Tag)

  • Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,
  • 具体的格式如下:
# 用反撇号来定义tag,可以定义多个tag
`key1:"value1" key2:"value2"` 
  • 结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
  • 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

示例:在上面示例中,返回的信息是{"Name":"小王","Age":18},name和age是大写字母开头的,用tag后,就可以转换成小写了

package main

import (
	"encoding/json"
	"fmt"
)

type person struct {
    // 注意,在``内,冒号:后面是没有空格的,多个tag之间用空格隔开
	Name string `json:"name" dat:"name" ini:"name"` // 当使用json时Name字段为name,同理,当使用数据库时字段为name(以此类推,可以设置多个tag)
	Age  uint8  `json:"age"`
}

func main() {
	// 实例化一个person对象
	p1 := person{
		Name: "小王",
		Age:  18,
	}
	b, err := json.Marshal(p1)
	if err != nil {
		fmt.Printf("marshal err: %v\n", err)
		return
	}
	fmt.Println(string(b))
}

输出结果

{"name":"小王","age":18}

10.3、反序列化

  • 利用json.Unmarshal()方法进行反序列化
    • json.Unmarshal()的括号()内传入两个参数,参数1是将字符串变量进行byte类型切片转换后的值,参数2是结构体实例的指针地址
import (
	"encoding/json"
	"fmt"
)

// 声明一个结构体
type person struct {
	Name string
	Age  int8
}

func main() {
	// 定义个符合json格式的字符串用作模拟
	str := `{"name":"小红","age":28}`
	// 1、首先需要定义一个person结构体类型的数据,用来接收反序列化的数据
	var p2 person
	// 2、利用json.Unmarshal()方法进行反序列化,传入byte类型的切片以及实例化的结构体
	json.Unmarshal([]byte(str), &p2) // 注意:这里的参数2传入的是指针地址,原因是我们传入函数的参数都是副本形式,直接传入p2是不会改变p2的值。因此需要传入指针,来修改p2的真实值
	fmt.Printf("%#v\n", p2)          // main.person{Name:"小红", Age:28} ,打印出p2的全部格式
}

11、结构体和方法补充知识点

  • 因为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{"吃饭", "睡觉", "打豆豆"}
	// 调用方法,将data赋值给p1
	p1.SetDreams(data)

	// 因为切片类型数据指向了底层数组,因此我们修改了data切片的数据后,同样也修改了p1.dream的值。这个就不符合我们的要求了
	data[1] = "不睡觉"
	fmt.Println(p1.dreams) // ?
}

输出结果

[吃饭 不睡觉 打豆豆]

正确的写法如下

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

小练

  • 看代码写结果
type student struct {
	name string
	age  int
}

func main() {
	m := make(map[string]*student)
	stus := []student{
		{name: "小王子", age: 18},
		{name: "娜扎", age: 23},
		{name: "大王八", age: 9000},
	}

	for _, stu := range stus {
		m[stu.name] = &stu
	}
	for k, v := range m {
		fmt.Println(k, "=>", v.name)
	}
}
  • 用函数写一个学员管理系统
package main

import (
	"fmt"
	"os"
)

/*
  学员管理系统
  写一个系统能查看、新增和删除学员
*/
type students struct {
	id   int64
	name string
}

var (
	allStudents = make(map[int64]*students)
)

func usage() {
	fmt.Println("欢迎登入学员管理系统")
	fmt.Println(`
	1、查看学员信息
	2、新增学员信息
	3、删除学员信息
	4、退出系统`)
	fmt.Print("请输入操作指令(1|2|3|4):")
}

func newStudents(id int64, name string) *students {
	return &students{
		id:   id,
		name: name,
	}
}
func addStudent() {
	var (
		id   int64
		name string
	)
	fmt.Println("您选择了添加学员")
	fmt.Print("请输入学员id:")
	fmt.Scanln(&id)
	fmt.Print("请输入学员name:")
	fmt.Scanln(&name)
	newStu := newStudents(id, name)
	allStudents[id] = newStu
}
func delStudent() {
	var (
		id int64
	)
	fmt.Print("请输入需要删除的学员id:")
	fmt.Scanln(&id)
	i, ok := allStudents[id]
	if !ok {
		fmt.Printf("该学号(%v)不存在", id)
		return
	}
	delete(allStudents, id) // delete的用法,参数1为传入数据,参数2位键
}
func (s students) quaryStudent() {

}

func main() {
	var choice int
	for {
		usage()
		fmt.Scanln(&choice)
		switch choice {
		case 1:
			fmt.Println("-------------")
			for _, v := range allStudents {
				fmt.Printf("id:%v 姓名:%v\n", v.id, v.name)
			}
			fmt.Println("-------------")
		case 2:
			addStudent()
		case 3:
			fmt.Println("您选择了删除学员")
			delStudent()
		case 4:
			fmt.Println("退出系统")
			os.Exit(0)   // 退出程序
		default:
			fmt.Println("输入错误,请重新输入")
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值