• 10-Go语言结构体


    结构体

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

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

    结构体定义

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

    type 类型名 struct{
      	字段名 字段类型
      	字段名 字段类型
      	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其中:

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

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

    type Person struct{
      name string
      city string
      age int8
    }
    
    //同样的类型可以写在一行:
    type Person1 struct{
      name,city string
      age 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这样我们就有了一个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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    结构体实例化

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

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

    var 结构体实例 结构体类型
    
    • 1
    基本实例化:
    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}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们通过.来访问结构体的字段(成员变量),例如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}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    创建指针类型结构体

    我们还可以通过使用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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    取结构体地址实例化
    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}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    结构体初始化

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

    func struDemo5() {
    	var p4 Person
    
    	fmt.Printf("p4=%#v\n", p4)
    }
    
    //运行结果:
    p4=main.Person{name:"", city:"", age:0}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    使用键值对初始化

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

    unc struDemo6() {
    	p5 := Person{
    		name: "不小孩",
    		city: "北京",
    		age:  28,
    	}
    
    	fmt.Printf("p5=%#v\n", p5) //p5=main.Person{name:"不小孩", city:"北京", age:28}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    func struDemo7() {
    	p6 := &Person{
    		name: "小王子",
    		city: "北京",
    		age:  18,
    	}
    
    	fmt.Printf("p6=%#v\n", p6) // p6=&main.Person{name:"小王子", city:"北京", age:18}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

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

    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[]}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    值列表初始化

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

    	// 列表初始化
    	// 必须按结构体定义时候的属性顺序依次赋值
    func struDemo9() {
    	var stu6 = Student{
    		"胡子",
    		24,
    		false,
    		map[string]int{"语文": 100},
    	}
    
    	fmt.Printf("%+v\n", stu6)
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    空结构体

    空结构体不占用空间.

    //空结构体
    func struDemo10() {
    	var v struct{}
    	fmt.Println(unsafe.Sizeof(v)) //0
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    结构体大小

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

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

    type Foo struct {
    	a int8 //1byte
    	b int8 //1byte
    	c int8 //1byte
    }
    
    //结构体大小
    func struDemo11() {
    	var f Foo
    	fmt.Println(unsafe.Sizeof(f)) //3byte
    
    }
    //结果
    3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    内存对齐

    内存对齐的原理: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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    有的同学可能会认为结构体变量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()的规则如下:
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    此时结构体 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}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    方法的定义

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

    方法定义的格式如下:

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

    其中:

    • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    接收者

    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
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    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 原来的变量已经被更改了
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    结构体的匿名字段

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

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

    //结构体的匿名字段
    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    嵌套结构体

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

    //嵌套结构体
    
    //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)  //{小王子 男 {北京 北京}}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    嵌套匿名字段

    上面的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)            //{小昂子 女 {山西 大同}}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    嵌套结构体的字段名冲突

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

    //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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    结构体的继承

    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() //强会动
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    结构体字段的可见性

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

    结构体与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)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    结构体标签(Tag)

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

    `key1:"value1" key2:"value2"`
    
    • 1

    结构体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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    补充

    切片的源码

    在这里插入图片描述

    练习

    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
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      //变行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)
      	}
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
    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)
      		}
      	}
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
  • 相关阅读:
    后端——面试题-注册、登录、数据库egg-mysql、svg验证码、服务层Service
    单片机学到什么程度才可以去工作?
    matplotlib中坐标轴控制总结
    代码随想录算法训练营第23期day28|491.递增子序列 46.全排列 47.全排列 II
    Shell编程之第一讲——基础知识认识
    二手安捷伦E9323A射频传感器
    什么是hive的高级分组聚合,它的用法和注意事项以及性能分析
    软件工程-从规划、需求(DFD数据流图),到设计、实现和测试
    如何确定自己是否适合做程序员?
    海光异构智能计算专区上线飞桨AI Studio!
  • 原文地址:https://blog.csdn.net/weixin_38753143/article/details/125544824