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
}
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}
我们通过.
来访问结构体的字段(成员变量),例如p1.name
和p1.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}
我们还可以通过使用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)
}
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}
p3.name = "黑煤球"
其实在底层是(*p3).name = "黑煤球"
,这是Go语言帮我们实现的语法糖。
没有初始化的结构体,其成员变量都是对应类型的零值。
func struDemo5() {
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}
}
也可以对结构体指针进行键值对初始化,例如:
func struDemo7() {
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:
//结构体字面量初始化
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[]}
初始化结构体的时候可以简写,也就是初始化不写键,直接写值:
// 列表初始化
// 必须按结构体定义时候的属性顺序依次赋值
func struDemo9() {
var stu6 = Student{
"胡子",
24,
false,
map[string]int{"语文": 100},
}
fmt.Printf("%+v\n", stu6)
}
使用这种格式初始化时,需要注意:
// 结构体字面量初始化
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)
}
空结构体不占用空间.
//空结构体
func struDemo10() {
var v struct{}
fmt.Println(unsafe.Sizeof(v)) //0
}
结构体占用连续的内存空间。
结构体占用的内存大小是由每个属性的大小和内存对齐决定的。
type Foo struct {
a int8 //1byte
b int8 //1byte
c int8 //1byte
}
//结构体大小
func struDemo11() {
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
}
func struDemo12() {
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()
的规则如下:
unsafe.Alignof(x)
至少为 1。unsafe.Alignof(x.f)
,unsafe.Alignof(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
}
此时结构体 Bar2 变量的内存布局示意图如下:
总结一下:在了解了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}
Go语言中的方法method
是一种作用于特定类型变量的函数。这种特定类型变量叫做接受者(receiver)。接收者的概念类似于其他语言中的this或者self。
方法定义的格式如下:
func(接受者变量 接受者类型) 方法名(参数列表)(返回参数){
函数体
}
其中:
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为 p
,Connector
类型的接收者变量应该命名为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语言
值接收者
当方法作用于值类型接受者时,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 原来的变量已经被更改了
什么时候该使用指针类型的接受者
在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
}
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就叫做匿名字段。
**注意:**这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
//结构体的匿名字段
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
}
一个结构体可以嵌套包含一个结构体或结构体指针,就像下面的示例代码那样。
//嵌套结构体
//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) //{小王子 男 {北京 北京}}
}
上面的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) //{小昂子 女 {山西 大同}}
}
当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
//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语言中使用结构体也可以实现其他编程语言中面向对象的继承。
//结构体继承
//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() //强会动
}
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
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)
}
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包访问
}
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":"男"}
}
切片的源码
下面的代码执行结果为?为什么?
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:
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)
}
}
编写学生管理系统
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)
}
}
}