结构体
Go语言中没有“类”的概念,也不支持类的“继承”等面向对象的概念,它所做的是通过嵌入字段的方式实现了类型之间的组合。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
1. 自定义类型、类型别名
1.1. 自定义类型
通过Type
关键字定义。
自定义类型是定义一个全新的类型。可以基于基本类型定义,也可以通过struct定义。
// 通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
type MyInt int
1.2. 类型别名
使用 “=” 定义,type TypeAlias = Type
类型别名规定:TypeAlias只是Type的别名,本质上属于同一个类型。
type intAlias = int
// 内置别名类型
type byte = uint8
type rune = int32
type any = interface{}
1.3. 类型定义和类型别名的区别
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
上面代码结果显示 a 的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型;b 的类型是 int。MyInt 类型只会在代码中存在,编译完成时并不会有 MyInt 类型。
2. 结构体定义
- 结构体是一种自定义类型,它是由若干字段组成的。
- 结构体的字段可以是任意类型,甚至可以是结构体本身。
结构体第一个字段的地址和结构体的指针是相同的
。应把频繁执行字段放在第一个,这样能够减少 CPU 指令,提升性能。
通过 struct
关键字定义。
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
类型名:同一个包内不能重复
字段名:同一个结构体内不能重复
字段类型:可以是任意类型,甚至可以是结构体本身
// 定义一个结构体
type Student struct {
Name string
Age int
}
3. 结构体初始化
只有当结构体实例化时,才会分配内存。也就是必须实例化后才能使用结构体的字段。
未赋值字段,初始化为字段类型零值。
var stu Student
fmt.Printf("%#v\n", stu) // main.Student{Name:"", Age:0}
// 通过点符号(.)访问结构体字段
stu.Name = "Tom"
fmt.Println(stu.Name)
使用键值对初始化
stu := Student{Name: "小花", Age: 18}
fmt.Println(stu)
使用值的列表初始化(简写:初始化时不写键,只写值)
需满足一下条件:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
stu := Student{"小明", 16}
fmt.Println(stu)
4. 指针类型结构体
初始化方式与值类型结构体相同,但使用指针类型进行初始化。
使用 new 关键字实例化
// new
var stu1 = new(Student)
fmt.Printf("%T \n", stu1)
使用 & 对结构体进行取址操作,相当于 new
stu := &Student{}
stu.Name = "小明"
(*stu).Age = 6
fmt.Printf("%#v\n", stu)
Go语法糖:会适时地为我们进行自动地转译,在stu之上,之所以我们可以通过stu.Name = "小明"
设置名字,是因为 Go语言把它自动转译为了(*stu).Name = "小明"
。
5. 构造函数
Go语言没有构造函数,我们可以自己实现。
struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
type Student struct {
Name string
Age int
}
func NewStudent(name string, age int) *Student {
return &Student{Name: name, Age: age}
}
func main() {
stu := NewStudent("小花", 18)
fmt.Printf("%#v\n", stu)
}
6. 方法和接收者
Go语言中通过struct来实现面向对象;可以包含方法,方法是结构体的成员函数。
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的 this 或者 self。
方法的接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指针类型。
函数与方法的区别:函数不属于任何类型,而方法属于特定的类型。
6.1. 方法定义
方法定义格式如下:
func (接收者 变量名) 方法名(参数列表)(返回值列表) {
方法体
}
例如:
type Student struct {
Name string
Age int
}
func (stu *Student) SayHello(age int) {
fmt.Printf("Hello, my name is %s, and %d years old.\n", stu.Name, age)
}
6.2. 方法调用
Go语言方法表达式有两种:instance.method(args…), .func(instance, args…)
func main() {
stu := &Student{
Name: "Tom",
Age: 18,
}
stu.SayHello(18)
// 隐式隐式
sSay := stu.SayHello
sSay(20)
// 显式传递
sr := (*Student).SayHello
sr(stu, 22)
}
6.3. 值方法和指针方法
值方法:接收者类型是非指针的自定义数据类型的方法。
指针方法:接收者类型是指针的自定义数据类型的方法。
值方法与指针方法区别:
-
接受者类型:
- 值方法的接收者是该方法所属类型值的一个副本,在方法内对该副本的修改一般不会体现在原值上,除非这个类型本身是某个引用类型(slice、map、chan)。
- 指针方法的接收者是该方法所属类型值的指针的一个副本,在方法内对该副本指向的值进行修改一定会体现在原值上。
-
方法集合:一个自定义数据类型的方法集合中仅包含它的所有值方法;而该类型的指针类型的方法集合囊括了所有值方法和所有指针方法。
package main
import "fmt"
type Student struct {
Name string
Age int
}
// 指针方法
func (stu *Student) SetName(name string) {
stu.Name = name
}
// 值方法
func (stu Student) SetNameCopy(name string) {
stu.Name = name
}
func main() {
stu := &Student{"小红", 20}
stu.SetName("小花")
fmt.Println(stu.Name) // 小花
stu.SetNameCopy("小明")
fmt.Println(stu.Name) // 小花
}
6.4. 指针方法使用场景
- 需要修改接收者中的值;
- 接收者是拷贝代价比较大的对象;
- 保证一致性,如果某个方法使用了指针接收者,那么其它方法也应该使用指针方法。
6.5. 任意类型添加方法
Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m MyInt
m.SayHello() //Hello, 我是一个int。
m = 100
fmt.Printf("%#v %T\n", m, m) //100 main.MyInt
}
7. 结构体成员可见性
Go语言通过首字母的大小写来控制权限。首字母大写包外部可见,首字母小写仅包内部可见。
demo/struct.go
package demo
type Student struct {
Name string // 公共字段
Age int
score int // 包内字段,在包外部不可初始化
}
// 公共方法
func (stu *Student) SetName(name string) {
stu.Name = name
}
// 包内方法
func (stu *Student) setScore(score int) {
stu.score = score
}
main.go
package mian
import (
"demo"
"fmt"
)
func main() {
stu := &demo.Student{Name: "小花", Age: 18}
stu.SetName("小红")
stu.setScore(80) // stu.setScore undefined (type *demo.Student has no field or method setScore)
fmt.Println(stu)
}
8. 结构体匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,那么它就是一个嵌入字段,也可以被称为匿名字段。
匿名字段默认采用类型作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
type BaseType struct {
string
int
}
func main() {
var b = BaseType{}
b.int = 10
fmt.Println(b.int)
}
9. 嵌套结构体
一个结构体可以嵌套包含另一个结构体或结构体指针。
type Address struct {
Province string
City string
}
type User struct {
Name string
Gender string
Address Address
}
func main() {
user := User{
Name: "pprof",
Gender: "女",
Address: Address{
Province: "北京",
City: "北京",
},
}
fmt.Printf("user=%#v\n", use)
}
9.1. 嵌套匿名结构体
嵌套结构体字段也可以使用匿名。推荐使用匿名方式嵌套结构体,如下所示:
type User struct {
Name string
Gender string
Address
}
func main() {
user := User{Name:"pprof", Gender:"女"}
user.Address.Province = "北京"
fmt.Printf("user=%#v\n", use)
}
9.2. 嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名,为了避免歧义需要指定具体的内嵌结构体的字段。
仅匿名内嵌结构体成员可以直接访问。Go直接访问成员匹配规则:
- 结构体成员中含有要访问的成员,则匹配结构体成员。
- 结构体成员中没有要访问的成员,则在
内嵌匿名结构体
中继续查找。若同一层级多个内嵌匿名结构体存在同名成员,则产生歧义,报错。 - 内嵌结构体中未找到要访问的成员,若内嵌匿名结构体内还存在内嵌匿名结构体,则继续向下查找,依此类推。
- 都匹配不到则报错。
package main
type Student struct {
Name string
Age int
CreateTime string
Address
Email
}
type Email struct {
Account string
Phone string
CreateTime string
}
type Address struct {
Name string
Province string
City string
County string
Phone string
CreateTime string
}
func (stu *Student) SetName(name string) {
stu.Name = name
}
func (addr *Address) SetName(name string) {
addr.Name = name
}
func (addr *Address) SetProvince(pro string) {
addr.Province = pro
}
func main() {
var stu = &Student{}
// 1. 嵌套结构体重名字段,需自定具体路径
stu.CreateTime = "2023-12-21"
stu.Address.CreateTime = "2023-12-30"
// 2. 直接访问匿名结构体的字段名(当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。)
stu.Account = "123@mail.com"
// 3. 向下查找字段若同一层级出现重名字段,则产生歧义;需指定具体结构体字段。
stu.Phone = "123123123" //报错:ambiguous selector stu.Phone
stu.Address.Phone = "123123"
// 结构体方法处理方式与字段相同
stu.SetName("老北京")
stu.Address.SetName("小北京")
stu.SetProvince("北京市")
fmt.Printf("%#v", stu)
}
10. 通过嵌套实现“继承”
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
type Dog struct {
Feet int8
*Animal // 通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d := &Dog{
Feet: 4,
Animal: &Animal{ // 注意嵌套的是结构体指针
name: "旺财",
},
}
d.wang() //旺财会汪汪汪~
d.move() //旺财会动!
}
11. 结构体与JSON序列化
package main
import (
"encoding/json"
"fmt"
)
type Class struct {
Title string
Students []*Student
}
type Student struct {
Name string
Age int
Address
}
type Address struct {
Province string
City string
County string
}
func main() {
c := &Class{
Title: "高一(12)班",
Students: make([]*Student, 0, 60),
}
for i := 1; i <= 10; i++ {
stu := &Student{
Name: fmt.Sprintf("Stu_%02d", i),
Address: Address{Province: "北京"},
}
c.Students = append(c.Students, stu)
}
// JSON序列化:结构体 to JSON
data, err := json.Marshal(c)
if err != nil {
fmt.Println("Json marshal failed.")
return
}
fmt.Printf("json:%s\n\n\n", data)
// JSON反序列化:JSON to 结构体
str := `{"Title":"101","Students":[{"Name":"小明","Age":16,"Address":{"Province":"北京"}},{"Name":"小花","Age":15,"Address":{"Province":"上海"}},{"Name":"小红","Age":16,"Address":{"Province":"广东"}}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
12. 结构体标签(Tag)
Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
结构体Tag在字段的后方定义,使用一对反引号(``)包括起来;由一个或多个键值对组成,键与值使用冒号分隔,值用双引号括起来,键值对之间使用一个空格分隔。
具体格式如下:
`key1:"value1" key2:"value2"`
**注意:为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。**例如:不要在key和value之间添加空格。
在结构体字段的定义中,如果字段名与 json 标签不一致,则需要在字段名后面添加一个 json 标签,如:
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Id int `json:"id"` //通过指定tag实现json序列化该字段时的key
Name string `json:"name"`
Age int `json:"-"` //通过指定tag实现json序列化该字段时忽略该字段
Sex string `json:"name,omitempty"` // omitempty表示字段为空值时不序列化
phone string //私有不能被json包访问
}
func main() {
stu := Student{
Id: 1,
Name: "小明",
Age: 18,
phone: "13333333333",
}
str, _ := json.Marshal(stu)
fmt.Printf("json:%s\n", str) //json:{"id":1,"name":"小明"}
str2, _ := json.Marshal(Student{
Id: 2,
Sex: "女",
})
fmt.Printf("json:%s\n", str2) //json:{"id":2,"name":"","sex":"女"}
}
13. 匿名结构体
在定义一些临时数据结构等场景下可以使用匿名结构体。
package main
import (
"fmt"
)
func main() {
var user struct{Name string; Age int}
user.Name = "pprof.cn"
user.Age = 18
fmt.Printf("%#v\n", user)
}