struct 结构体【GO 基础】
〇、前言
虽然 Go 语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念,但是可以通过结构体的内嵌,再配合接口,来实现面向对象,甚至具有更高的扩展性和灵活性。那么本文就将详细看下怎么使用结构体。
一、结构体的定义和实例化
Go 语言中的基础数据类型可以表示一些事物的基本属性,但是当想要表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了。
Go 语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体 struct。通过 struct 可以封装自己所需的各种复杂类型。
1.1 什么叫做自定义类型和类型别名?
在详解结构体之前,需要先了解两个概念,到底什么叫做自定义类型和类型别名?
【自定义类型】
在 Go 语言中有一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型,Go 语言中可以使用 type 关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过 struct 定义。
type MyInt int // 新定义一个类型 MyInt,以 int 类型为参照
通过 type 关键字的定义,MyInt 就是一种新的类型,它具有 int 的特性。
【类型别名】
类型别名是 Go1.9 版本添加的新功能。
类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像一个小孩子有自己的小名和户口本的大名,都是指的同一个人。
type TypeAlias = Type
例如长江的 rune 和 byte 就是类型别名,他们的定义如下:
type byte = uint8 type rune = int32
【区别】
那么它们之前的区别是啥呢?下面分别定义一个变量看下它们的类型:
package main import "fmt" // 自定义类型 type NewInt int // 类型别名 type MyInt = int func main() { var a NewInt var b MyInt fmt.Printf("NewInt type of a : %T\n", a) fmt.Printf("MyInt type of b : %T\n", b) }
有输出结果可知,自定义类型的 type 将变成新的名字 NewInt;类型别名 MyInt,则仍属于基本的 int 类型。
1.2 结构体的定义
使用 type 和 struct 关键字来定义结构体。
type 类型名 struct { 字段名 字段类型 字段名 字段类型 … } // 类型名:标识自定义结构体的名称,在同一个包内不能重复 // 字段名:表示结构体字段名,结构体中的字段名必须唯一 // 字段类型:表示结构体字段的具体类型
举个例子,我们定义一个 Person(人)结构体:
type Person struct { name string city string age int8 } // 类型相同的字段可以放到一起,如下: type Person struct { name, city string age int8 }
这样我们就拥有了一个 Person 的自定义类型,它有 name、city、age 三个字段,分别表示姓名、城市和年龄。这样我们使用这个 person 结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型。
1.3 结构体的实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
// 结构体实例化格式: var 结构体实例名 结构体类型
下面是一个 Person 的示例:
package main import "fmt" type Person struct { name string city string age int8 } func main() { var person Person person.name = "中国" person.city = "北京" person.age = 18 fmt.Printf("person.name = %v\n", person.name) fmt.Printf("person %%v = %v\n", person) fmt.Printf("person %%#v = %#v\n", person) }
二、结构体的使用
2.1 匿名结构体
结构体还可以直接用于临时的或仅使用一次的场景中,不用声明结构体的名称,例如
package main import ( "fmt" ) func main() { var user struct { Name string Age int } user.Name = "张三" user.Age = 18 fmt.Printf("%#v\n", user) }
通过匿名结构体声明得来的实例 user。
2.2 指针类型的结构体
当通过使用 new 关键字对结构体进行实例化后,得到的是结构体的地址。
另外,Go 语言中支持对结构体指针直接使用‘.’来访问结构体的成员。如下示例:
package main import ( "fmt" ) type Person struct { name string city string age int8 } func main() { var p = new(Person) // 声明结构体指针 // p3 := &Person{} // 还可以通过 & 对结构体进行取地址操作,相当于对该结构体类型进行了一次 new 实例化操作 p.name = "测试" p.age = 18 p.city = "北京" fmt.Printf("%T\n", p) // 打印出类型 fmt.Printf("p = %#v\n", p) }
2.3 结构体的初始化
【声明即初始化】
此时结构体的值均为默认零值:
package main import ( "fmt" ) type Person struct { name string city string age int8 } func main() { // 声明即初始化 var p = new(Person) fmt.Printf("p = %#v\n", p) }
【使用键值对初始化】
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
// 使用键值对初始化 p1 := Person{ name: "张三", city: "北京", age: 18, } fmt.Printf("p1 = %#v\n", p1) // 对结构体指针进行键值对初始化 p2 := &Person{ name: "张三", city: "北京", age: 18, } fmt.Printf("p2 = %#v\n", p2)
【使用值的列表初始化】
// 使用值的列表初始化 p3 := &Person{ "张三", "北京", 18, } fmt.Printf("p3 = %#v\n", p3)
注意,值列表需要满足以下要求:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
2.4 结构体的内存布局
package main import ( "fmt" ) type test struct { a int8 b int8 c int8 d int8 } func main() { n := test{ 1, 2, 3, 4, } fmt.Printf("n.a %p\n", &n.a) fmt.Printf("n.b %p\n", &n.b) fmt.Printf("n.c %p\n", &n.c) fmt.Printf("n.d %p\n", &n.d) }
2.5 结构体的构造函数
Go 语言的结构体没有构造函数,但可以自己实现。例如,下方的代码就实现了一个 Person 的构造函数。因为 struct 是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
package main import ( "fmt" ) type Person struct { name string city string age int8 } func newPerson(name, city string, age int8) *Person { return &Person{ name: name, city: city, age: age, } } func main() { p9 := newPerson("张三", "测试", 90) fmt.Printf("%#v\n", p9) }
2.6 方法和接收者
Go 语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的 this 或者 self。
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 } // 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是 self、this 之类的命名 // 例如,Person 类型的接收者变量应该命名为 p,Connector 类型的接收者变量应该命名为 c 等 // 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型 // 方法名、参数列表、返回参数:具体格式与函数定义相同
下面是一个简单示例:
package main import ( "fmt" ) // 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。 例如我们为 Person 添加一个 SetAge 方法,来修改实例变量的年龄。
当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
package main import ( "fmt" ) // Person 结构体 type Person struct { name string age int8 } // NewPerson 构造函数 func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, } } // SetAge 设置p的年龄 // 使用指针接收者 func (p *Person) SetAge(newAge int8) { p.age = newAge } // SetAge2 设置p的年龄 // 使用值接收者 func (p Person) SetAge2(newAge int8) { p.age = newAge } func main() { p1 := NewPerson("张三同学", 25) fmt.Println("修改前的 age :", p1.age) p1.SetAge(18) fmt.Println("修改后的 age :", p1.age) p1.SetAge2(20) fmt.Println("修改后的 age :", p1.age) }
使用指针类型的情况:
- 需要修改接收者中的值。
- 接收者是拷贝代价比较大的大对象。
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
【任意类型都可以添加方法】
比如下面示例代码,给自定义类型添加方法:
package main import ( "fmt" ) // 自定义一个类型 MyInt,参考 int 类型 type MyInt int // 为 MyInt 添加一个 SayHello 的方法 func (m MyInt) SayHello() { fmt.Println("Hello, 我是一个 int。") } func main() { var m1 MyInt m1.SayHello() m1 = 100 fmt.Printf("%#v %T\n", m1, m1) }
2.7 结构体允许包含匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
package main import ( "fmt" ) // Person 结构体Person类型 type Person struct { string int } func main() { p1 := Person{ "张三", 18, } fmt.Printf("%#v\n", p1) fmt.Println(p1.string, p1.int) }
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
2.8 嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
package main import ( "fmt" ) // Address 地址结构体 type Address struct { Province string City string } // User 用户结构体 type User struct { Name string Gender string Address Address // 此处可以省略第二个 Address 以匿名类型方式 } func main() { user1 := User{ Name: "张三", Gender: "女", Address: Address{ Province: "北京", City: "北京", }, } fmt.Printf("user1 = %#v\n", user1) }
当嵌套结构体内部存在相同的字段名时,为了避免歧义需要指定具体的内嵌结构体的字段,否则会提示异常。
2.9 结构体实现“继承”
package main import ( "fmt" ) // 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() }
注意:结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
2.10 结构体与 JSON 序列化
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。
JSON 键值对是用来保存 JS 对象的一种方式,键/值对组合中的键名写在前面并用双引号 "" 包裹,使用冒号 : 分隔,然后紧接着值;多个键值之间使用英文 , 分隔。
package main import ( "encoding/json" "fmt" ) // 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 < 2; i++ { // 创建 2 个学生对象 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\n", data) // JSON 反序列化:JSON 格式的字符串-->结构体 str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"}]}` c1 := &Class{} err = json.Unmarshal([]byte(str), c1) if err != nil { fmt.Println("json unmarshal failed!") return } fmt.Printf("%#v\n", c1) }
2.11 结构体标签 Tag
Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag 在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。
注意事项:为结构体编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如,不要在 key 和 value 之间添加空格。
例如我们为 Student 结构体的每个字段定义 json 序列化时使用的 Tag:
package main import ( "encoding/json" "fmt" ) // 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) }
2.12 删除 map 类型的结构体子项
package main import "fmt" type student struct { id int name string age int } func main() { ce := make(map[int]student) ce[1] = student{1, "张三", 22} ce[2] = student{2, "李四", 23} fmt.Println(ce) delete(ce, 2) fmt.Println(ce) }
参考:http://www.topgoer.com/go%E5%9F%BA%E7%A1%80/%E7%BB%93%E6%9E%84%E4%BD%93.html

本文来自博客园,作者:橙子家,欢迎微信扫码关注博主【橙子家czzj】,有任何疑问欢迎沟通,共同成长!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步