Go的结构体
结构体
Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct
结构体的定义
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
- 类型名:自定义结构体的名称,在同一个包内不能重复
- 字段名:结构体中的字段名必须惟一
- 字段类型:表示结构体字段的具体类型
例子一
type persion struct {
name string
city string
age int8
}
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段
结构体本身也是一种类型,我们可以像声明内置类型一样使用var
关键字声明结构体类型
type persion struct {
name string
city string
age int8
}
func main() {
var p1 persion
p1.name = "james"
p1.city = "深圳"
p1.age = 25
fmt.Printf("p1=%v
", p1)
fmt.Printf("p1=%#v
", p1)
}
// p1={james 深圳 25}
// p1=main.persion{name:"james", city:"深圳", age:25}
匿名结构体
func main() {
var user struct{Name string; Age int}
user.Name = "james"
user.Age = 25
fmt.Printf("p1=%#v
", user)
}
// user=struct { Name string; Age int }{Name:"james", Age:25}
创建指针类型的结构体
使用new
关键字对结构体进行实例化,得到的是结构体的地址
type persion struct {
name string
city string
age int8
}
func main() {
var p2 = new(persion)
fmt.Printf("%T
", p2)
fmt.Printf("%#v
", p2)
}
// *main.persion
// &main.persion{name:"", city:"", age:0}
p2
是一个结构体指针
结构体指针可以使用.
来访问结构体的成员
type persion struct {
name string
city string
age int8
}
func main() {
var p2 = new(persion)
p2.name = "james"
p2.city = "深圳"
p2.age = 25
fmt.Println(p2.city)
fmt.Printf("%#v
", p2)
}
// 深圳
// &main.persion{name:"james", city:"深圳", age:25}
取结构体的地址实例化
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作
type persion struct {
name string
city string
age int8
}
func main() {
p3 := &persion{} // 等于var p3 = new(persion)
fmt.Printf("%T
", p3)
fmt.Printf("%#v
", p3)
p3.name = "james"
p3.city = "深圳"
p3.age = 25
fmt.Printf("%#v
", p3)
}
// *main.persion
// &main.persion{name:"", city:"", age:0}
// &main.persion{name:"james", city:"深圳", age:25}
p3.name = "james"
其实在底层是(*p3).name = "james"
,这是Go帮我们实现的语法糖
结构体初始化
没有初始化的结构体,其成员变量都是其类型对应的零值
type persion struct {
name string
city string
age int8
}
func main() {
var p4 persion
fmt.Printf("p4=%#v
", p4)
}
// p4=main.persion{name:"", city:"", age:0}
使用键值对初始化
type persion struct {
name string
city string
age int8
}
func main() {
p5 := persion {
name : "james",
city : "深圳",
age : 25,
}
fmt.Printf("p5=%#v
", p5)
}
// p5=main.persion{name:"james", city:"深圳", age:25}
对结构体指针进行初始化
type persion struct {
name string
city string
age int8
}
func main() {
p6 := &persion {
name : "james",
city : "深圳",
age : 25,
}
fmt.Printf("p6=%#v
", p6)
}
// p6=&main.persion{name:"james", city:"深圳", age:25}
也可以只初始化某些字段
type persion struct {
name string
city string
age int8
}
func main() {
p7 := &persion {
name : "james",
}
fmt.Printf("p7=%#v
", p7)
}
// p7=&main.persion{name:"james", city:"", age:0}
结构体内存布局
结构体占用一块连续的内存
type test struct {
a int8
b int8
c int8
d int
}
func main() {
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a: %p
", &n.a)
fmt.Printf("n.b: %p
", &n.b)
fmt.Printf("n.c: %p
", &n.c)
fmt.Printf("n.d: %p
", &n.d)
}
// n.a: 0xc00006a004
// n.b: 0xc00006a005
// n.c: 0xc00006a006
// n.d: 0xc00006a007
猜结果
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "stu1", age: 6},
{name: "stu2", age: 7},
{name: "stu3", age: 6},
}
for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
// stu1 => stu3
// stu2 => stu3
// stu3 => stu3
// 为什么v.name都是stu3,我也搞不懂
go的构造函数
Go语言的结构体没有构造函数,可以自己通过结构体实现
struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型
type persion struct {
name string
city string
age int8
}
func newPersion(name, city string, age int8) *persion {
return &persion{
name : name,
city : city,
age : age,
}
}
func main() {
p9 := newPersion("james", "深圳", 26)
fmt.Printf("%#v
", p9)
}
// &main.persion{name:"james", city:"深圳", age:26}
go的方法和接收者
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母
- 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
- 方法名、参数列表、返回参数:具体格式与函数定义相同
// Persion结构体
type Persion struct {
name string
age int8
}
// NewPersion构造函数
func NewPersion(name string, age int8) *Persion {
return &Persion{
name : name,
age : age,
}
}
// Dream Persion的做梦的方法
func (p Persion) Dream() {
fmt.Printf("%s的梦想是转行厨师
", p.name)
}
func main() {
p1 := NewPersion("james", 26)
p1.Dream()
}
// james的梦想是转行厨师
方法与函数的区别是: 函数不属于任何类型,方法属于特定的类型
指针类型接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this
或者self
。 例如我们为Person
添加一个SetAge
方法,来修改实例变量的年龄
// Persion 结构体
type Persion struct {
name string
age int8
}
// NewPersion 构造函数
func NewPersion(name string, age int8) *Persion {
return &Persion{
name : name,
age : age,
}
}
// SetAge 设置p的年龄
// 使用指针类型接收者
func (p *Persion) SetAge(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPersion("james", 26)
fmt.Println(p1.age) // 26
p1.SetAge(22)
fmt.Println(p1.age) // 22
}
值类型接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身
// Persion 结构体
type Persion struct {
name string
age int8
}
// NewPersion 构造函数
func NewPersion(name string, age int8) *Persion {
return &Persion{
name : name,
age : age,
}
}
// Dream Persion的做梦的方法
func (p Persion) Dream() {
fmt.Printf("%s的梦想是转行厨师
", p.name)
}
func (p Persion) SetAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPersion("james", 27)
p1.Dream()
fmt.Println(p1.age)
p1.SetAge2(30)
fmt.Println(p1.age)
}
// james的梦想是转行厨师
// 27
// 27
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应使用指针接收者
任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法
例子一
type MyInt int
func (m MyInt) SayHello() {
fmt.Println("Hello World I am int")
}
func main() {
var m1 MyInt
m1.SayHello()
m1 = 100
fmt.Printf("%#v %T
", m1, m1)
}
// Hello World I am int
// 100 main.MyInt
注意: 不能给别的包的类型定义方法
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个
// Persion 结构体Persion类型
type Persion struct {
string
int
}
func main() {
p1 := Persion {
"james",
27,
}
fmt.Printf("%#v
", p1)
fmt.Println(p1.string, p1.int)
}
// main.Persion{string:"james", int:27}
// james 27
嵌套结构体
// Address 地址结构体
type Address struct {
Province string
City string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "james",
Gender: "boy",
Address: Address{
Province: "广东",
City: "深圳",
},
}
fmt.Printf("user1=%#v
", user1)
}
// user1=main.User{Name:"james", Gender:"boy", 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 = "james"
user2.Gender = "boy"
user2.Address.Province = "广东" // 通过匿名结构体.字段名访问
user2.City = "深圳" // 直接访问匿名结构体的字段名
fmt.Printf("user2=%#v
", user2)
}
// user2=main.User{Name:"james", Gender:"boy", Address:main.Address{Province:"广东", City:"深圳"}}
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找
注意: 嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段
结构体的"继承"
// Animal 动物
type Animal struct {
name string
}
// Animal都有的move方法
func (a *Animal) move() {
fmt.Printf("%v正在移动!
", a.name)
}
// Dog 动物狗
type Dog struct {
Feet int
*Animal //通过嵌套匿名结构体实现继承
}
// Dog 都有的wang方法
func (d *Dog) wang() {
fmt.Printf("%v会汪汪汪~
", d.name)
}
func main() {
d1 := &Dog{
Feet: 5,
Animal: &Animal{ // 注意嵌套的是结构体指针
name: "大黄",
},
}
d1.wang()
d1.move()
}
// 大黄会汪汪汪~
// 大黄正在移动!
结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
结构体和JSON序列化
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: "102",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
ID: i,
Name: fmt.Sprintf("stu%02d", i),
Gender: "boy",
}
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
", data)
// JSON反序列化: JSON格式字符串==>结构体
str := data
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed")
return
}
fmt.Printf("%#v
", c1)
}
结构体的标签(Tag)
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来
Tag
在结构体字段的后方定义,由一对反引号包裹起来
结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔
重点: 为结构体编写Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格
// 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: "boy",
name: "james",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("%s
", data)
}
// {"id":1,"Gender":"boy"}