今天来学习一个重要的知识点,也是Go语言的核心 —— 结构体
简介
Go语言中没有“类”的概念,也没有面向对象的那种继承的概念。但Go语言引入了一个新的概念叫结构体
,它通过结构体之间内嵌并配合接口的方式来实现比面向对象更灵活、更易扩展的效果。
结构体
Go语言中如果想表示一个事物的基本属性,我们会想到用基本数据类型去表示,但如果想表示它的全部属性的话,单一的基本数据类型就满足不了,此时就引出了今天的主角——结构体,英文名称struct
。
也就是说我们可以通过struct
来定义自己的类型,Go语言中也是通过这种方式实现面向对象的相关操作的。
定义
使用type
和struct
关键字来定义结构体的,语法格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
...
}
说明:
- 类型名 - 指结构体名称,在同一个包内不能重复
- 字段名 - 指结构体的字段名,类似面向对象里的属性名,具有唯一性
- 字段类型 - 指结构体字段的具体类型
来个小示例,比如我们来定义一个人的结构体,如下:
type person struct {
name string
sex string
age int
}
同样字段类型也可以写在一行,比如:
type person struct {
name, sex string
age int
}
这样子一个人的结构体就字义好了,它有name
、sex
、age
三个字段,分别表示人的姓名、性别、年龄,这样子我们就可以使用该结构很方便的存储人的相关信息了。
Go语言的基本数据类型用来描述一个值,而结构体是描述的一组,所以结构体也可以称为聚合型的数据类型。
实例化
结构体只有在实例化时,才会进行内存分配,因此它也只有实例化后才能使用。结构体本身也是一个数据类型,因此我们可以像基本数据类型一样使用var
关键字进行声明,语法如下:
var 结构体实例 结构体类型
注:如果结构体只有进行声明,没有进行赋值的话,其成员变量对应的是它的默认值。
基本实例化
通过先定义,后赋值的方式进行实例化,如下:
type person struct {
name string
sex string
age int
}
func main() {
var p1 person
p1.name = "xiaohe"
p1.sex = "man"
p1.age = 18
fmt.Printf("p1 = %v\n", p1) // p1 = {xiaohe man 18}
}
键值对实例化
使用键值对直接对结构体进行实例化,也是实际开发中常用的方式,如下:
type person struct {
name string
sex string
age int
}
func main() {
p1 := person{
name: "xiaohe",
sex: "man",
age: 18,
}
fmt.Printf("p1 = %v\n", p1) // p1 = {xiaohe man 18}
}
值的列表顺序实例化
即按照结构体字段的顺序进行实例化,如下:
type person struct {
name string
sex string
age int
}
func main() {
p1 := person{
"xiaohe",
"man",
18,
}
fmt.Printf("p1 = %v\n", p1) // p1 = {xiaohe man 18}
}
注:这种方式在实际开发中不建议,有以下几个缺点
- 不够直观,具所有字段都必须初始化
- 顺序必须与结构体里的字段顺序一致
匿名结构体
匿名结构体和匿名函数差不多,即没有结构体名称,如下:
func main() {
var p2 struct {
name string
age int
}
p2.name = "haha"
p2.age = 19
fmt.Printf("p2 = %v\n", p2) // p2 = {haha 19}
}
匿名字段
匿名字段即没有字段名的结构体字段,如下:
type person struct {
string
int
}
func main() {
p3 := person{
"xiaohe",
20,
}
fmt.Printf("p3 = %v\n", p3) // p3 = {xiaohe 20}
}
注:其实它并不是没有字段名,而是使用字段类型隐式的做为字段名了,因此相同数据类型也只能有一个,如果上面示例中person
结构在写一个int
的话是会报错的。
内存布局
结构体在内存中是如何存储的呢?先说结论:一串连续的内存空间,来个小示例验证一下吧:
type test struct {
a, b, c, d int8
}
func main() {
t1 := test{1, 8, 10, 30}
fmt.Printf("t1.a = %p\n", &t1.a)
fmt.Printf("t1.b = %p\n", &t1.b)
fmt.Printf("t1.c = %p\n", &t1.c)
fmt.Printf("t1.d = %p\n", &t1.d)
}
输出:
t1.a = 0x140000160b0
t1.b = 0x140000160b1
t1.c = 0x140000160b2
t1.d = 0x140000160b3
说明:a
、b
、c
、d
为int8
类型,因此占一个字节,所以该结构体的内存布局为0x140000160b0~
0x140000160b3`。
来个面试题感受一下内存布局的神奇之处吧:
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
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
输出:
小王子 => 大王八
娜扎 => 大王八
大王八 => 大王八
你能想明白原因吗?
嵌套结构体
嵌套结构体即一个结构体或结构体指针嵌套在另一个结构体中,示例如下:
type Address struct {
Province string
City string
}
type Person struct {
Name string
Age int
Address Address // 嵌套关系
}
func main() {
p1 := Person{
Name: "xiaohe",
Age: 20,
Address: Address{
Province: "河南",
City: "商丘",
},
}
fmt.Printf("person is %#v\n", p1)
}
输出:
person is main.Person{Name:"xiaohe", Age:20, Address:main.Address{Province:"河南", City:"商丘"}}
继承
Go 语言中的继承是通过嵌套结构体的方式实现的,示例如下:
type Address struct {
Province string
City string
}
func (a *Address) show() {
fmt.Printf("province is %s, city is %s\n", a.Province, a.City)
}
type Person struct {
Name string
Age int
Address // 这种写法称之为嵌套匿名字段
}
func main() {
p1 := Person{
Name: "xiaohe",
Age: 20,
Address: Address{
Province: "河南",
City: "商丘",
},
}
p1.show() // 这种调用和下面效果是一样的
p1.Address.show()
}
输出:
province is 河南, city is 商丘
province is 河南, city is 商丘
字段可见性
Go语言中可见性都是用大小写来控制的,大写即表示可公开访问,小写表示只能在包内访问。
JSON序列化
Go语言中结构体是通过两个方法来实现 JSON 序列化与反序列化操作的,示例如下:
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{
Name: "xiaohe",
Age: 20,
}
// 序列化
bt, _ := json.Marshal(p1)
fmt.Printf("json str is %s\n", string(bt))
// 反序列化
var p2 Person
json.Unmarshal([]byte(`{"Name":"xiaohe","Age":20}`), &p2)
fmt.Printf("p2 is %#v\n", p2)
}
输出:
json str is {"Name":"xiaohe","Age":20}
p2 is main.Person{Name:"xiaohe", Age:20}
结构体标签
结构体标签是为了表示结构体元信息的一种实现方式,当程序运行时可以通过反射机制读取出来,从而实现定制化的数据展示,它的语法格式如下:
`key1:"value1" key2:"value2"`
说明:由一个或多个键值对组成,使用反引号包裹,不同键值之间使用空格分隔。
使用场景,通过指结构体标签来实现 JSON 序列化时指字段名风格,示例如下:
type Person struct {
Name string `json:"user_name"` // 表明序列化时使用的字段名
Age int // 如果不指定则会直接用字段名
}
func main() {
p1 := Person{
Name: "xiaohe",
Age: 20,
}
// 序列化
bt, _ := json.Marshal(p1)
fmt.Printf("json str is %s\n", string(bt))
}
输出:
json str is {"user_name":"xiaohe","Age":20}
构造函数
Go 语言中是没有构造函数的概念的,不过我们通常会自定义一个方法来达到构造函数的效果,如下:
type Person struct {
Name string `json:"user_name"`
Age int
}
// 类似构造函数去实例化对象
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}
func main() {
p1 := NewPerson("xiaohe", 20)
fmt.Printf("p1 is %#v\n", p1)
}
方法
方法通俗的讲可以理解为只能由指定接收者才能调用的一种特殊类型的函数,它相较于函数定义多了一个接口收者的概念,语法如下:
func (接收者 接收者类型) 方法名(参数) (返回参数) {
方法体
}
说明:
- 接口者:通常以接口者类型首字母的小写来命名,如:
Person
类型则命名为p
- 接收者类型:与参数类似,可以是基础数据类型也可以是结构体类型
- 方法名、参数列表、返回参数:具体格式与函数定义相同
接收者
指针类型
在方法声明时,指针类型是比较常用的一种方式,一方面节约内存空间,另一方面指针类型的方法,当方法体内部对接收者数据进行修改时,外部的接收者数据也会被修改,示例如下:
type Person struct {
Name string `json:"user_name"`
Age int
}
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}
// 类型前加 * 号则表示指针类型,也叫引用类型
func (p *Person) setName(name string) {
p.Name = name
}
func main() {
p1 := NewPerson("xiaohe", 20)
fmt.Printf("p1 is %#v\n", p1)
p1.setName("haha") // 修改名字
fmt.Printf("p1 is %#v\n", p1)
}
输出:
p1 is &main.Person{Name:"xiaohe", Age:20}
p1 is &main.Person{Name:"haha", Age:20}
值类型
相较于指针类型来说,区别在于方法体内部对接收者数据的修改,外部的接收者的数据不会被改变,示例如下:
type Person struct {
Name string `json:"user_name"`
Age int
}
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}
// 这种称为值类型,修改只在方法体内部生效
func (p Person) setName(name string) {
p.Name = name
}
func main() {
p1 := NewPerson("xiaohe", 20)
fmt.Printf("p1 is %#v\n", p1)
p1.setName("haha")
fmt.Printf("p1 is %#v\n", p1)
}
输出:
p1 is &main.Person{Name:"xiaohe", Age:20}
p1 is &main.Person{Name:"xiaohe", Age:20}
至此,结构体相关的东西就差不多了,886~