12 结构体
文章目录
12 结构体
- Go 中无 ·类· 的概念,不支持类的继承等面向对象的概念
- 通过 结构体的内嵌再配合接口 比面向对象具有更高的扩展性和灵活性
类型别名和自定义类型
自定义类型
- Go 中的基本数据类型:
string
,int
… ,浮点数
等 - Go 可以使用
type
关键字定义自定义类型 - 自定义类型:定义了一个全新的类型
- 可以基于内置基本数据类型
- 可以通过struct定义
type name T
- name 就是一种新类型,具有 T类型的特性
类型别名
- 类型别名规定:
type TypeAlias = Type
TypeAlias
只是Type
的别名,- 本质上是同一个类型
rune
和byte
就是类型别名:type rune = int32
type byte = uint8
类型定义 和 类型别名的区别
type NewInt int
:%T
:main.NewInt
type MyInt = int
:%T
:int
结果显示a的类型是main.NewInt
,表示main包下定义的NewInt
类型。
b的类型是int
。MyInt
类型只会在代码中存在,编译完成时并不会有MyInt
类型。
结构体
- 基本数据类型可以表示一些事物的部分属性
- 结构体可以封装多个基本数据类型,表达一个事物的全部属性
- 本质上是一种聚合类型
- Go 通过struct实现面向对象
结构体的定义
-
使用
type
和struct
-
type 类型名 struct{ 字段名 字段类型 字段名 字段类型 }
-
类型名:同一个包内不能重复
-
字段名:字段名必须唯一,同样字段类型的字段名可写在同一行
-
字段类型:结构体字段的具体类型
-
结构体实例化
- 只有当结构体实例化时,才会真正的分配内存,才能使用结构体的字段
- 使用关键字
var
声明:var 结构体实例 结构体类型
基本实例化
-
声明后,使用操作符
.
访问结构体字段(成员变量)进行初始化 -
type person struct { name string city string age int8 } func main() { var p1 person p1.name = "沙河娜扎" p1.city = "北京" p1.age = 18 fmt.Printf("p1=%v\n", p1) //p1={沙河娜扎 北京 18} fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18} }
匿名结构体
-
定义一些临时数据结构,使用匿名结构体
var user struct{Name string; Age int}
创建指针类型结构体
-
可以使用
new
对结构体进行实例化,得到结构体的地址var p2 = new(person) fmt.Printf("%T",p2) //*main.person fmt.Printf("%#v",p2) //&main.person(..)
-
p2
是一个结构体指针 -
注意:Go 支持对结构体指针直接使用
.
访问结构体成员
取结构体的地址实例化
-
使用
&
对结构体进行取地址操作,相当于对该结构体类型 进行了一次new
实例化操作 -
p3 := &person() p3.name = "qimi"
-
p3.name = "qimi"
在底层 是(*p3).name = "qimi"
- 是 Go 帮我们实现的语法糖
结构体初始化
-
未初始化的结构体,其成员变量都是对应其类型的零值
-
type person struct { name string city string age int8 } func main() { var p4 person fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0} }
使用键值对初始化
-
键对应结构体的字段
-
值对应该字段的初始值
-
p5 := person{ name : "小王子", city : "beijing", age : 18, }
-
-
对结构体指针进行键值对初始化:
-
p6 := &person{ name: "小王子", city: "北京", age: 18, }
-
-
未指定初始值的字段的值为该字段类型的零值
-
p7 := &person{ city: "北京", }
-
使用值得列表初始化
-
初始化结构体时,直接写值:
-
p8 := &person{ "沙河娜扎", "北京", 28, }
-
必须要初始化结构体的所有字段
-
初始值的填充顺序必须与字段,在结构体中的声明顺序一致
-
不能和键值对初始化混用
-
结构体内存布局
- 结构体占用一块 连续的内存
空结构体
- 空结构体不占用空间
fmt.Printf(unsafe.Sizeof(v))
//0
面试题
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 //m : "小王子":&student{name: "小王子", age: 18}
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
-
执行结果:
小王子 => 小王子 娜扎 => 娜扎 大王八 => 大王八
-
运行结果:
大王八 => 大王八 小王子 => 大王八 娜扎 => 大王八
构造函数
-
Go 结构体没有构造函数,需要自己实现
-
struct
是值类型,值拷贝性能开销比较大,所以构造函数返回的是结构体指针类型-
func newPerson(name, city string, age int8) *person { return &person{ name: name, city: city, age: age, } }
-
-
调用:
-
p9 := newPerson("张三", "沙河", 90)
-
方法和接收者
-
Go 中的方法(Method):一种作用于特定类型变量的函数
- 这种特定类型变量叫做
接收者Receiver
- 接收者的概念类似于
this
,self
- 这种特定类型变量叫做
-
方法的定义格式:
-
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 }
- **接收者变量:**接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
- 方法名、参数列表、返回参数: 与函数定义相同
- **接收者变量:**接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
-
示例:
//Person 结构体 type Person struct { name string age int8 } //NewPerson 构造函数 func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, } } //Dream Person做梦的方法 func (p Person) Dream() { fmt.Printf("%s的梦想是学好Go语言!\n", p.name) } func main() { p1 := NewPerson("小王子", 25) p1.Dream() }
-
-
方法与函数的区别:
- 函数不属于任何类型
- 方法属于特定类型
指针类型的接收者
-
指针类型的接收者 : 结构体的指针
-
调用方法时,修改接收者指针的任意成员变量,都是有效的
- 接近于其他面向对象中的
this
,self
- 接近于其他面向对象中的
-
示例:一个方法:修改实例变量的年龄
-
// SetAge 设置p的年龄 // 使用指针接收者 func (p *Person) SetAge(newAge int8) { p.age = newAge }
-
func main() { p1 := NewPerson("小王子", 25) fmt.Println(p1.age) // 25 p1.SetAge(30) fmt.Println(p1.age) // 30 }
-
值类型的接收者
-
方法作用于 值类型接收者时,Go 会在运行前将接收者的值复制一份
-
在值类型接收者的方法中,可以获取接收者的成员值,但无法修改接收者变量本身
-
// SetAge2 设置p的年龄 // 使用值接收者 func (p Person) SetAge2(newAge int8) { p.age = newAge } func main() { p1 := NewPerson("小王子", 25) p1.Dream() fmt.Println(p1.age) // 25 p1.SetAge2(30) // (*p1).SetAge2(30) fmt.Println(p1.age) // 25 }
-
何时使用 指针类型的接收者
- 需要修改接收者的成员值
- 接收者是 拷贝代价比较大的大对象
- 保证一致性,若有个方法使用了指针接收者,其他也应该同步
任意类型添加方法
-
在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 }
-
-
注意:不能给其他包的 类型定义方法
结构体的匿名字段
-
结构体 允许 其成员字段在声明时,没有字段名,只有类型
-
==注意:==Go 会默认采用类型名作为字段名,结构体要求字段名称必须唯一
- 因此,一个结构体中,同种类型的匿名字段只能有一个
-
//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 }
嵌套结构体
-
结构体中,可以嵌套包含 另一个结构体或结构体指针
-
示例
//Address 地址结构体 type Address struct { Province string City string } //User 用户结构体 type User struct { Name string Gender string Address Address } func main() { user1 := User{ Name: "小王子", Gender: "男", Address: Address{ Province: "山东", City: "威海", }, } fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}} }
-
嵌套匿名字段
-
嵌套的结构体也可采用匿名字段:
-
//Address 地址结构体 type Address struct { Province string City string } //User 用户结构体 type User struct { Name string Gender string Address //匿名字段 } func main() { var user2 User user2.Name = "小王子" user2.Gender = "男" user2.Address.Province = "山东" // 匿名字段默认使用类型名作为字段名 user2.City = "威海" // 匿名字段可以省略 fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}} }
-
-
当访问结构体成员时,现在结构体中查找该字段,再去嵌套的匿名字段找
嵌套结构体的字段名冲突
-
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
//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 中使用结构体,可以实现其他面向对象中的 继承
-
//Animal 动物 type Animal struct { name string } func (a *Animal) move() { fmt.Printf("%s会动!\n", a.name) } //Dog 狗 type Dog struct { Feet int8 *Animal //通过嵌套匿名结构体实现继承 } func (d *Dog) wang() { fmt.Printf("%s会汪汪汪~\n", d.name) } func main() { d1 := &Dog{ Feet: 4, Animal: &Animal{ //注意嵌套的是结构体指针 name: "乐乐", }, } d1.wang() //乐乐会汪汪汪~ d1.move() //乐乐会动! }
-
结构体字段的可见性
- 结构体中:
- 大写开头表示可公开访问
- 小写表示私有(仅在定义当前结构体的包中访问)
结构体 与 JSON 序列化
-
JSON
:JavaScript Object Notation
- 轻量级的数据交换格式
- 易于 阅读和编写
- 易于 机器解析和生成
-
JSON 键值对:用于保存 JS 对象的一种方式
-
键/值对 格式:
"键名" : 值,
-
//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格式的字符串 data, err := json.Marshal(c) if err != nil { fmt.Println("json marshal failed") 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:"balue1" key2:"value2"`
-
-
结构体
tag
有一个或多个键值对组成- 键与值是用冒号分隔,值使用双引号括起来
- 同一个结构体字段可以设置多个键值对,不同键值对使用空格分隔
-
==注意: ==为结构体编写
Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。 -
示例:每个字段定义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":"男"} }
-
结构体和方法补充知识点
-
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情况
-