11.Go语言干货-结构体

1. 类型别名与自定义类型

1.1 自定义类型

在Go语言中有一些基本的数据类型,整型string浮点型布尔等数据类型。

在Go语言中可以根据自身的需求,使用关键字type自定义数据类型。

自定义类型是定义了一个全新的类型

  1. 我们可以根据内置的基本类型来定义。
  2. 也可以通过struct来定义。
// 基于基本类型定义自定义类型的格式
type MyInt int

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

1.2 类型别名

类型别名规定:TypeAliasType的别名,本质上二者没有区别。就像读书的时候给班主任起的外号,班主任自己的名字与他的外号都指向班主任本人。

type TypeAlias  = Type

我们之间学习的runebyte就是类型别名

type byte = uint8
type rune = int32

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

用下面这段代码就可以很清晰了解二者之间的区别

func main() {
	// 自定义类型
	type MyInt int
	// 类型别名
	type AliasInt = int

	var a MyInt
	var b AliasInt

	fmt.Printf("%T", a)//main.MyInt

	fmt.Printf("%T", b)//int
}

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

2.结构体

1.Go语言中基本的数据类型只能表示一些事物单一的基本属性,但是我们需要表达一个事物多个或者全部的属性时,单一的基本数据类型就无法满足需求。
2.Go语言提供了自定义数据类型的方法,可以将多个基本数据属性封装在一起,这种数据类型就是结构体。
3.类似python中的类,Go语言中通过struct来实现面向对象

2.1 结构体的定义

使用typestruct关键字来定义结构体。

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

注意:

  1. 类型名:自定义结构体的名称,在同一个包内不能重名
  2. 字段名:结构体内部字段名称,在同一个结构体中也不能重名
  3. 字段类型:结构体字段的具体类型

举个栗子,定义一个人(person) 的结构体

type person struct{
	name string
	age int8
	addr string
}
// 类型相同的字段也可以写在一行

type person2 struct {
	name,addr string
	age int8
}

我们自定义了人(person)的数据类型,这个类型拥有name,age,addr三个字段,分别表示人的姓名,年龄,地址。这样我们就可以通过person结构体很方便的表示人的信息了。
内置的数据类型用来描述一个值,结构体是用来描述一组值

2.2 结构体实例化

只有当结构体实例化的时候,才被正真地分配了内存,换句话说,只有实例化后才能使用结构体的字段

结构体本质上也是一种数据类型,我们可以想声明内置类型一样,使用var关键字声明结构体类型

2.2.1 基本实例化
package main

import "fmt"

type person struct {
	name string
	age  int8
	addr string
}

func main() {
	// 实例化person类型,得到p1实例
	var p1 person
	p1.name = "spl"
	p1.age = 18
	p1.addr = "成都"
	
	fmt.Println(p1) // {spl 18 成都}
	fmt.Printf("我叫%s,今年%d岁,来自%s",p1.name,p1.age,p1.addr) 
	//我叫spl,今年18岁,来自成都
2.2.2 匿名结构体

当我们临时需要数据结构的场景下,可以定义匿名的结构体

func main() {
	var p1 struct {
		name string
		age  int8
		addr string
	}
	p1.name = "spl"
	p1.age = 18
	p1.addr = "成都"

	fmt.Println(p1) // {spl 18 成都}
	fmt.Printf("我叫%s,今年%d岁,来自%s",p1.name,p1.age,p1.addr) 
	//我叫spl,今年18岁,来自成都
}

2.2.3 创建指针类型结构体

我们可以通过使用关键字new对结构体实例化,得到的是结构体的内存地址

var p2 = new(person)
fmt.Printf("%v\n",p2)//&main.person{name:"spl", age:0, addr:""}
fmt.Printf("%T\n",p2)//*main.person
  1. p2 是一个结构体指针(指针的本质就是内存地址)
  2. Go语言中支持对结构体指针直接使用.来访问结构体的成员
var p2 = new(person)
	p2.name = "spl"
	p2.age = 18
	p2.addr = "成都"
	fmt.Printf("%#v\n",p2)
	fmt.Printf("%T\n",p2)
	fmt.Printf("我叫%s,今年%d岁,来自%s",p2.name,p2.age,p2.addr)
`
输出结果:
&main.person{name:"spl", age:18, addr:"成都"}
*main.person
我叫spl,今年18岁,来自成都
`
2.2.4 使用& 得到结构体地址的实例化

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

	p2 := &person{} // 等同于var p2 = new(person)
	p2.name = "spl"
	p2.age = 18
	p2.addr = "成都"

	fmt.Printf("我叫%s,今年%d岁,来自%s",p2.name,p2.age,p2.addr)

值得注意的是p2.name = "spl"的底层是 (*p2).name = "spl",这是Go帮助我们实现的语法糖

3.结构体实例化

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

package main

import "fmt"

type person struct {
	name string
	age  int8
	addr string
}

func main() {
	var p1 person
	fmt.Printf("%#v",p1) // main.person{name:"", age:0, addr:""}
}

3.1 使用键值对初始化结构体

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

	p2 := person{
		name: "spl",
		age:  18,
		addr: "成都",
	}
	fmt.Println(p2)

对结构体指针进行初始化

p3 := &person{
	name:"spl",
	age:18,
	addr:"cd",
}

当结构体某一些字段没有进行初始化,可以不写该字段。此时,没有进行初始化的字段默认为该字段的零值

	p3 := person{
		name: "spl",
	}
	fmt.Printf("%#v",p3) // main.person{name:"spl", age:0, addr:""}

3.2 使用值的列表进行初始化

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

	p4 := person{
		"spl",
		18,
		"cd",
	}
	fmt.Printf("%#v", p4)

使用这种格式初始化结构体的时候应该特别注意:

  1. 必须初始化结构体的所有字段
  2. 初始化的写值的顺序必须与结构体中声明的顺序一致
  3. 该方法不能与键值初始化进行混用

4.结构体的内存布局

结构体是占用一块连续的内存

	type test struct{
		a int8
		b int8
		c int8
		d int8
	}

	t1 := test{
		a:1,
		b:2,
		c:3,
		d:4,
	}
	fmt.Printf("%p\n",&t1.a)
	fmt.Printf("%p\n",&t1.b)
	fmt.Printf("%p\n",&t1.c)
	fmt.Printf("%p\n",&t1.d)

`0xc000012098
0xc000012099
0xc00001209a
0xc00001209b`

4.1 空结构体

空结构体不占用内存

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

5.阶段思考

请问下面代码的执行结果是什么?


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

6.构造函数

值得注意的是Go语言中没有构造函数,但是我们可以自己实现构造函数。
构造函数的实现原理就是:

  1. 定义一个函数
  2. 函数的形参是结构体的字段,返回值为结构体类型或者是结构体指针类型。
  3. 思考,什么时候返回结构体类型?什么时候返回结构体指针类型?
    (当结构体比较复杂,返回结构体类型,开销比较大,所以返回结构体指针类型)
type person struct {
	name string
	age  int8
}

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

func main() {
	p1 := newPerson("spl", 18)
	p2 := newPerson("cxm", 18)

	fmt.Println(p1)
	fmt.Println(p2)

7.给结构体定义方法

之前介绍的都是事物的静态字段,比如实例化的人,有名字,有年龄。但是这个实例是静态的。如何将实例动起来,比如说实例化出的人可以跑、可以跳这个应该怎么实现?
Go语言中提供了方法的概念将实例给动起来。Go语言中的方法本质就是一个特殊函数,这个特殊的函数与平时定义的函数有什么区别呢?
Go语言中结构体与他的方法是隔离开的(下面见到代码就能很清楚这一点),因为结构体与方法函数分离,那么怎么判断这个方法是这个结构体的呢?
Go语言应用了接受者这个概念,通俗的讲就是给方法函数打一个标记,这个标记指向对应的结构体

方法函数的定义格式:

func (接受者变量 接受者类型) 方法函数名(参数)(返回值){
	函数体
}
  • 接受者变量:就是自定一个变量,方便在函数体中调用
  • 接受者类型:我们要知道结构体的本质就是自定义的类型,这里的接受者就是结构体名
  • 方法函数名、参数、返回值:与函数定义相同

举个栗子:

package main

import "fmt"

// 结构体
type person struct{
	name string
	age int8
}

// 构造函数
func newPerson(name string,age int8) person{
	return person{
		name: name,
		age: age,
	}
}

// 结构体person的方法
func (p person) Dream(addr string){
	fmt.Printf("%s在%d岁的时候在%s做着靠Go语言发家致富的梦",p.name,p.age,addr)
}

func main(){
	p1 := newPerson("spl",18)
	p1.Dream("成都")	
}

7.1 指针类型的接受者与值类型接受者

指针类型的接受者是由结构体的指针组成,由于指针的特性,调用方法时直接修改结构体实例在内存中的变量值,所以修改完成后依旧有效!
举个栗子:为person添加一个定义年龄的方法函数

1.结构体类型是结构体指针

// 结构体person的设置年龄的方法
func (p *person) SetAge(age int8){
	fmt.Printf("%d岁年龄有误,修改年龄为%d\n",p.age,age)
	p.age = age
}

func main(){
	p2 := newPerson("cxm",10)
	fmt.Printf("我是%s,我今年%d岁\n",p2.name,p2.age)
	p2.SetAge(18)
	fmt.Printf("我是%s,我今年%d岁\n",p2.name,p2.age)

`
我是cxm,我今年10岁
10岁年龄有误,修改年龄为18
我是cxm,我今年18岁
`

2.结构体类型不是结构体指针(为值类型)

// 结构体person的设置年龄的方法
func (p person) SetAge(age int8){
	fmt.Printf("%d岁年龄有误,修改年龄为%d\n",p.age,age)
	p.age = age
}

func main(){
	p2 := newPerson("cxm",10)
	fmt.Printf("我是%s,我今年%d岁\n",p2.name,p2.age)
	p2.SetAge(18)
	fmt.Printf("我是%s,我今年%d岁\n",p2.name,p2.age)
`
我是cxm,我今年10岁
10岁年龄有误,修改年龄为18
我是cxm,我今年10岁
`

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

7.2什么时候应用指针类型接受者?

1.需要修改接受者(结构体实例)中的值
2.接受者是拷贝代价比较大的结构体
3.保持一致性,如果某个方法使用了指针类型接受者,那么其他的方法也应该使用指针类型的接受者

8.任意类型添加方法

在Go语言中接受者的可以是任何类型,不仅仅是结构体(结构体是包含多个类型的特殊类型)任何类型都可以拥有方法。

举个栗子:我们基于内置的int类型,使用type关键字重新定义一个新的类型,然后为这个类型添加一个自定义方法。

import "fmt"

// 根据int自定义一个MyInt类型
type MyInt int

// 为MyInt自定义一个SayHello的方法

func (m MyInt) Sayhello(){
	fmt.Println("大家好啊!我是基于int类型定义的MyInt类型")
}

func main(){
	var m1  MyInt
	m1.Sayhello()

}

我们直接给int类型定义一个Sayhello2,报错,错误代码:cannot define new methods on non-local type int
原因是:我们只能为当前包的类型定义方法,不能给别的包定义方法

func (i int) Sayhello2(){
	fmt.Println("大家好啊!我是int类型")
}
func main(){
	var m1  MyInt
	m1.Sayhello()

	var m2 int
	m2.Sayhello2()

}
`
cannot define new methods on non-local type int
`

9.结构体匿名字段

结构体匿名字段,不是结构体名称匿名,而是结构体包含的字段匿名

package main

type person struct{
	string
	int
}

func main(){
	p1 := person{
		"spl",
		18,
	}
	fmt.Println(p1.string)
	fmt.Println(p1.int)
}

注意:这里的结构体匿名字段并不代表字段没有名字,而是才使用了使用类型作为字段名,结构体要求内部的字段名不能重名,因此一个结构体中相同类型的字段只能有一个

10.结构体嵌套

结构体嵌套就是一个结构体内部包含另外一个结构体

package main

import "fmt"

// 定义同一个地址的结构体
type address struct {
	province string
	city     string
}

// 定义一个人的结构体
type user struct {
	name string
	age int8
	address address
}


func main(){
	u1 := user{
		name: "spl",
		age: 18,
		address: address{
			province: "四川",
			city: "成都",
		},
	}
	fmt.Println(u1.address.city)
	fmt.Println(u1.address.province)
}

10.1 嵌套匿名字段

上面user结构体中的嵌套address结构体可以采用匿名字段的方法

package main

import "fmt"

// 定义一个地址结构体
type addr struct {
	provice string
	city    string
}

// 定义一个人结构体
type user struct {
	name string
	age  int8
	addr
}

func main() {
	var u1 user
	u1.name = "spl"
	u1.age = 18
	u1.addr.provice = "四川"// 匿名字段默认使用类型名作为字段名
	u1.city = "成都" // 匿名字段也可以省略

	fmt.Println(u1) //{spl 18 {四川 成都}}
}

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

10.2 嵌套结构体的字段名冲突

嵌套结构体内的字段,字段名可能会存在相同的字段名。在这种情况下就避免歧义,使用具体的内嵌结构体字段名。

// 定义address结构体
type address struct {
	provice string
	city    string
}

// 定义mail 结构体
type mail struct {
	provice string
	city    string
}

// 定义user 结构体
type user struct {
	name string
	age  int8
	address
	mail
}

func main(){
	var u1 user
	u1.name = "spl"
	u1.age = 18
	u1.address.provice = "四川" // 指定address中的Provice
	u1.address.city = "成都" // 指定address 中的city

	u1.mail.provice = "四川" // 指定 mail 中的Provice
	u1.mail.city = "彭州" // 指定 mail 中的city
}

11 结构体的继承

通过嵌套结构体匿名字段实现继承

package main

import "fmt"

// 定义一个Animal结构体
type Animal struct{}

// 给animal结构体添加一个move方法
func (a Animal) move() {
	fmt.Println("动起来了!")
}

//定义一个Dog结构体,继承Animal结构体
type Dog struct{
	name string
	Animal
}

func main(){
	d1 := Dog{
		name: "布鲁托",
	}
	d1.move()
}

12 结构体字段的可见性

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

13 结构体与Json序列化

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

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

func main() {
	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格式的字符串-->结构体
	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)
}

14 结构体和方法补充

因为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的情况,在实际编码过程中一定要注意这个问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值