结构体
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或者部分属性时,这时候在用单一的基本数据类型就无法满足要求了,G哦语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫做结构体,英文
struct
。
Go语言中通过
struct
来实现面向对象。
结构体定义
使用
type
和
struct
关键字来定义结构体,具体代码格式如下:
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}funcstruDemo1(){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.name
和
p1.age
等。
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
//匿名结构体funcstruDemo2(){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
关键字对结构体进行实例化,得到的是结构体的地址。格式如下:
//创建指针类型结构体funcstruDemo3(){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语言中支持对结构体指针直接使用.来访问funcstruDemo3(){var p2 =new(Person)
p2.name ="小孩子"//等价于(*p2).name
p2.age =28
p2.city ="上海"
fmt.Printf("%T\n", p2)
fmt.Printf("p2=%#v\n", p2)}
取结构体地址实例化
funcstruDemo4(){
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语言帮我们实现的语法糖。
结构体初始化
没有初始化的结构体,其成员变量都是对应类型的零值。
funcstruDemo5(){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}}
也可以对结构体指针进行键值对初始化,例如:
funcstruDemo7(){
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:
//结构体字面量初始化funcstruDemo8(){
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[]}
值列表初始化
初始化结构体的时候可以简写,也就是初始化不写键,直接写值:
// 列表初始化// 必须按结构体定义时候的属性顺序依次赋值funcstruDemo9(){var stu6 = Student{"胡子",24,false,map[string]int{"语文":100},}
fmt.Printf("%+v\n", stu6)}
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
结构体指针初始化
// 结构体字面量初始化funcdemo5(){
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 // nilvar x =new(int)*x =100// (*nil) = 100
fmt.Println(x)}
空结构体
空结构体不占用空间.
//空结构体funcstruDemo10(){var v struct{}
fmt.Println(unsafe.Sizeof(v))//0}
结构体的内存布局(进阶)
结构体大小
结构体占用连续的内存空间。
结构体占用的内存大小是由每个属性的大小和内存对齐决定的。
type Foo struct{
a int8//1byte
b int8//1byte
c int8//1byte}//结构体大小funcstruDemo11(){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}funcstruDemo12(){var b1 Bar
fmt.Println(unsafe.Sizeof(b1))//24}
有的同学可能会认为结构体变量
b1
的内存布局如下图所示,那么问题来了,结构体变量
b1
的大小怎么会是24呢?
很显然结构体变量
b1
的内存布局和上图中的并不一致,实际上的布局应该如下图所示,灰色虚线的部分就是内存对齐时的填充(padding)部分。
因为 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
函数获取一个变量的大小,此外我们可以通过内置的
unsafe
的
Alignof
函数获取一个变量的对齐系数,例如:
//结构体变量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}funcstruDemo13(){var b2 Bar2
fmt.Println(unsafe.Sizeof(b2))//16}
此时结构体 Bar2 变量的内存布局示意图如下:
总结一下:在了解了Go的内存对齐规则之后,我们在日常的编码过程中,完全可以通过合理地调整结构体的字段顺序,从而优化结构体的大小。
方法
构造函数
Go语言中的结构体没有构造函数,我们可以自己实现。例如,下方的代码就实现了一个person的构造函数。因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销比较大,所以该构造函数返回的是结构体指针类型。
type Person struct{
name string
city string
age int8}//构造函数funcstruDemo14(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(接受者变量 接受者类型) 方法名(参数列表)(返回参数){
函数体
}
其中:
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
举个例子:
//方法//People结构体type People struct{
name string
age int8}//构造函数funcnewPeople(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语言
接收者
- 值接收者当方法作用于值类型接受者时,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
- 指针接收者指针类型接受者由一个结构体的指针组成。由于指针的特性,调用方法时修改接受者指针的任意成员变量,在方法结束后,修改都是有效的。
//指针类型的接受者//定义方法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. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的
int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
//MyInt 将int定义为自定义MyInt类型type MyInt int//SayHello 为MyInt添加一个SayHello的方法func(m MyInt)SayHello(){
fmt.Println("Hello, 我是一个int。")}funcmain(){var m1 MyInt
m1.SayHello()//Hello, 我是一个int。
m1 =100
fmt.Printf("%#v %T\n", m1, m1)//100 main.MyInt}
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就叫做匿名字段。
注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
//结构体的匿名字段type Teacher struct{stringint}functeaDemo(){
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 //嵌套结构体}funcstrudemo15(){
user := User{
Name:"小王子",
Gender:"男",
Address: Address{
Province:"北京",
City:"北京",},}
fmt.Println(user)//{小王子 男 {北京 北京}}}
嵌套匿名字段
上面的user结构体嵌套的address结构体,也可以采取匿名字段的方式,例如:
//嵌套匿名字段type User struct{
Name string
Gender string
Address //匿名字段}funcstudemo16(){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
}funcmain(){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)}//Dogtype Dog struct{
Feet int8*Animal //继承Anmial结构体}func(d *Dog)wang(){
fmt.Printf("%s会汪汪\n", d.name)}//调用funcAnmil(){
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
}funcJson(){
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包访问}funcmain(){
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":"男"}}
补充
切片的源码
练习
- 下面的代码执行结果为?为什么?
type student struct{//定义一个名为student的结构体 name string age int}funcmain(){ 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都指向了同一块内存地址&stu2、那么就知道为啥输出这个样子的,因为stu的指针指向内存地址,每次循环的时候,值都是会变的。循环到最后一项,内部value为最后一项的元素``````//变行1:type student struct{ name string age int}funcex(){ 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. 使用“面向对象”的思维方式编写一个学生信息管理系统。 1. 学生有id、姓名、年龄、分数等信息2. 程序提供展示学生列表、添加学生、编辑学生信息、删除学生等功能
package mainimport("fmt""os")/*使用“面向对象”的思维方式编写一个学生信息管理系统。1、学生有id、姓名、年龄、分数等信息2、程序提供展示学生列表、添加学生、编辑学生信息、删除学生等功能*///1、Student结构体type Student struct{ ID int Name string Age int Score int}//考虑有添加和编辑,考虑maptype Class struct{ Stulist map[int]*Student}//2、提供展示学生列表、添加学生、编辑学生信息、删除学生等方法//查看学生列表方法func(c *Class)showList(){iflen(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、写主执行函数funcmain(){ c :=&Class{ Stulist:make(map[int]*Student),}for{var input int fmt.Print(` 欢迎访问学生管理系统! 1、查看所有学生列表 2、添加学生 3、编辑学生信息 4、删除学生 5、退出 宝子们,请选择你要操作编号:`) fmt.Scan(&input)switch input {case1: c.showList()case2: c.addStudent()case3: c.editStudent()case4: c.deleteStudent()case5: os.Exit(0)}}}
版权归原作者 爱写代码的小男孩 所有, 如有侵权,请联系我们删除。