golang进阶之结构体

BigSun丶 / 2024-03-06 / 原文

目录
  • 一、结构体(Go的面向对象)
    • 1. 结构体的含义
    • 2. type 关键字
      • (1)自定义新类型
      • (2)类型的别名
      • (3)自定义类型和类型别名的区别
  • 二、结构体的定义
  • 三、结构体实例化
    • 1. 基本实例化
    • 2. 匿名结构体
    • 2. 指针型结构体
    • 3. 取结构体的地址实例化
    • 4. 结构体指针进阶实例
  • 四、结构体的初始化
    • 1. 未初始化的结构体的值
    • 2. 使用键值对初始化
    • 3. 使用值的列表初始化(简写初始化)
  • 五、结构体的内存布局

一、结构体(Go的面向对象)

1. 结构体的含义

  • Go语言中的基础数据类型都只是表示事物的某一个基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,我们把这种数据类型叫做结构体,英文名称struct
  • Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。但Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性,通过struct来实现面向对象
  • 简单来说结构体就是一种我们自定义的数据类型,它包含了多种基本数据类型,通过使用 type 和 struct关键字来定义我们的结构体

2. type 关键字

  • 在Go语言中有一些基本的数据类型,如string整型浮点型布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型

(1)自定义新类型

  • 自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如

    • //将MyInt定义为int类型
      type new_int int
      
      - 通过type关键字的定义,new_int就是一种新的类型,它具有int的特性
      - 注意:虽然new_int具有int的特性,但按照Go语言的规则:new_int 类型 和 int 类型属于不同类型
      

(2)类型的别名

  • 类型别名是Go1.9版本添加的新功能。
  • 类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人
起别名语法:
type TypeAlias = Type

- 我们之前见过的rune和byte就是类型别名,他们的定义如下
type byte = uint8
type rune = int32

(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
}

二、结构体的定义

  • 使用typestruct关键字来定义结构体
type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

其中:

- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型

示例:
// 我们定义一个 Person 结构体
type person struct {
	name string
	city string
	age  int8
}

// 同函数参数中的简写一样,同样类型的字段也可以写在一行
type person1 struct {
	name, city string
	age        int8
}

三、结构体实例化

  • 只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段

  • 结构体本身也是一种类型,我们可以像声明变量一样使用var关键字声明结构体类型,也可用 := 符合进行声明并初始化

  • 同python的面向对象的方法一样,我们通过.来访问结构体的字段(成员变量),例如下面的p1.namep1.age

1. 声明结构体类型
var 结构体实例 结构体类型

2. 声明并初始化
var 结构体实例 = 结构体类型{}  // {}可以为空,可以有值
结构体实例 := 结构体类型{}

1. 基本实例化

  • 像声明基本数据类型一样声明结构体
1. 对比map的初始化定义为:注意 {} 内部写法
personSalary := make(map[string]int)
personSalary := map[string]int {"steve": 12000,"jamie": 15000}

********************分隔线********************

2. 结构体的实例化 1:(同函数参数的写法一样,同样类型的字段也可以写在一行)
type person1 struct {
	name, city string
	age        int8
}
p5 := person1{name: "sdf", city: "dfdf", age: 1}

3. 结构体的实例化 2:
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}
}

2. 匿名结构体

  • 在定义一些临时数据结构等场景下还可以使用匿名结构体

  • 未用 type 关键字,而使用 struct 关键字直接定义

package main
     
import (
    "fmt"
)
     
func main() {
    var user struct{Name string; Age int}
    user.Name = "炮王"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}

2. 指针型结构体

  • 我们还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址
  • (Go语法糖)需要注意的是在Go语言中支持对结构体指针直接使用 . 来访问结构体的成员,进行修改和赋值操作
type person struct {
	name, city string
	age        int8
}
var p = new(person)
fmt.Printf("%T\n", p)      // *main.person
fmt.Printf("p=%#v\n", p)  // p=&main.person{name:"", city:"", age:0}  ,可以看出,没有初始化的结构体,其成员变量都是对应其类型的零值

**********************************分隔线**********************************

- 从打印的结果中我们可以看出p是一个结构体指针

- 用 . 访问结构体的成员,如下:
var p = new(person)
p.name = "炮王"
p.age = 28
p.city = "上海"
fmt.Printf("p=%#v\n", p)  // p=&main.person{name:"炮王", city:"上海", age:28}

3. 取结构体的地址实例化

  • 使用 & 对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作
type person struct {
	name, city string
	age        int8
}

p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "大鸡"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"大鸡", city:"成都", age:30}

- 注意:p3.name = "大鸡" 其实在底层是(*p3).name = "大鸡",这是Go语言帮我们实现的语法糖

4. 结构体指针进阶实例

package main

import "fmt"

type person struct {
	name string
	city string
	age  int8
}

type student struct {
	name string
	age  int
}

func main() {
	m := make(map[string]*student)  // 定义一个结构体map,可以理解为python中的字典套字典
	stus := []student{
		{name: "炮王", age: 18},
		{name: "阿香", age: 23},
		{name: "大王八", age: 9000},
	}

	for v, stu := range stus {
		fmt.Println(v, stu)
		m[stu.name] = &stu  
	}
	fmt.Println(m)  // map[大王八:0xc0000080a8 炮王:0xc000008048 阿香:0xc000008078]
	for k, v := range m {
		fmt.Println(k, "=>", v.name)  // v为一个个结构体指针,Go语法糖中,结构体指针可以直接使用 . 符号来取值
	}
}

/*
0 {炮王 18}
1 {阿香 23}                                                 
2 {大王八 9000}                                             
map[大王八:0xc0000080a8 炮王:0xc000008048 阿香:0xc000008078]
炮王 => 炮王                                                
阿香 => 阿香                                                
大王八 => 大王八 
*/

四、结构体的初始化

1. 未初始化的结构体的值

  • 没有初始化的结构体,其成员变量都是对应其类型的零值
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}
}

2. 使用键值对初始化

  • 使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值
  • 当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值
  • 下面是 普通结构体的初始化 和 结构体指针的初始化
1. 普通结构体的初始化
p5 := person{
	name: "炮王",
	city: "东京",
	age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"炮王", city:"东京", age:18}

2. 结构体指针的初始化
p6 := &person{
	name: "炮王",
	city: "东京",
	age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"炮王", city:"东京", age:18}

3. 使用值的列表初始化(简写初始化)

  • 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值
p8 := &person{
	"虹桥阿香",
	"东京",
	28,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"虹桥阿香", city:"东京", age:28}

********************************* 使用这种格式初始化时,需要注意:*********************************

- 必须初始化结构体的所有字段
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致
- 该方式不能和键值初始化方式混用

五、结构体的内存布局

  • 结构体占用的是一块连续的内存

  • Go 在编译的时候会按照一定的规则自动进行内存对齐

    • 内存对齐简而言之:内存对齐即为连续内存中,从开始位置起,内存会和左右相邻的内存大小进行对比,和相对且小满足本内存的对齐系数的邻近内存进行进行填充
    • 之所以这么设计是为了减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。如果不进行内存对齐的话,很可能就会增加 CPU 访问内存的次数。这就涉及到 CPU 和内存交互的范畴了,而这里我们只需要了解这样做的原因就可以了
  • 内存对齐详情,对于我们优化程序的内存占用非常有用,建议熟悉掌握

  • 定义结构体时的总结

    • 空结构体是不占用内存的

    • 大多数情况下,应当将小的字段定义在前面,而后再定义大的字段

    • 当空结构体类型作为结构体的最后一个字段时,为了避免内存泄露会额外进行一次内存对齐,所以空结构体类型,要定义在结构体的第一个位置最好

type test struct {
	a int8
	b int8
	c int8
	d int8
}
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)

/*
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063

可以看出,内存地址为连续的
*/


- 内置unsafe包的Sizeof函数来获取一个变量的大小,unsafe包的Alignof函数来获取一个变量的对齐系数

1. 空结构体
var v struct{}
fmt.Println(unsafe.Sizeof(v))  // 0  

2. 结构体的定义(错误示范)
type Bar struct {
	x int32 // 4
	y *Foo  // 8
	z bool  // 1
}

var b1 Bar
fmt.Println(unsafe.Sizeof(b1)) // 24  占用了24的大小

// 结构体变量b1的对齐系数
fmt.Println(unsafe.Alignof(b1))   // 8
// b1每一个字段的对齐系数
fmt.Println(unsafe.Alignof(b1.x)) // 4:表示此字段须按4的倍数对齐
fmt.Println(unsafe.Alignof(b1.y)) // 8:表示此字段须按8的倍数对齐
fmt.Println(unsafe.Alignof(b1.z)) // 1:表示此字段须按1的倍数对齐

3. 结构体的定义(正确示范)
type Bar2 struct {
	x int32 // 4
	z bool  // 1
	y *Foo  // 8
}

var b2 Bar2
fmt.Println(unsafe.Sizeof(b2)) // 16  占用了16的大小

3.1 结构体的定义(正确示范)
type Bar3 struct {
	z bool  // 1
	x int32 // 4
	y *Foo  // 8
}

var b3 Bar3
fmt.Println(unsafe.Sizeof(b3)) // 16  占用了16的大小

4.包含空结构体类型的结构体的定义(错误示范)
type Demo2 struct {
	n int8     // 1
	m struct{} // 0
}

var d2 Demo2
fmt.Println(unsafe.Sizeof(d2))  // 2

4.1 包含空结构体类型的结构体的定义(正确示范)
type Demo1 struct {
	m struct{} // 0
	n int8     // 1
}

var d1 Demo1
fmt.Println(unsafe.Sizeof(d1))  // 1