[TOC]

0x01 Go语言基础之错误处理

描述: Go语言中目前(1.16 版本中)是没有异常处理机制(Tips :说是在2.x版本中将会加入异常处理机制),但我们可以使用error接口定义以及panic/recover函数来进行异常错误处理。

1.error 接口定义

描述: 在Golang中利用error类型实现了error接口,并且可以通过errors.New或者fmt.Errorf来快速创建错误实例。

主要应用场景: 在 Go 语言中,错误是可以预期的,并且不是非常严重,不会影响程序的运行。对于这类问题可以用返回错误给调用者的方法,让调用者自己决定如何处理,通常采用 error 接口进行实现。

error接口定义:

1
2
3
type error interface {
Error() string
}

Go语言的标准库代码包errors方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 方式1.在errors包中的New方法(Go 1.13 版本)。
package errors
// go提供了errorString结构体,其则实现了error接口
type errorString struct {
text string
}
func (e *errorString) Error() string {
return e.text
}

// 在errors包中,还提供了New函数,来实例化errorString,如下:
func New(text string) error {
return &errorString{text}
}

// 方式2.另一个可以生成error类型值的方法是调用fmt包中的Errorf函数(Go 1.13 版本以后)
package fmt
import "errors"
func Errorf(format string, args ...interface{}) error{
return errors.New(Sprintf(format,args...))
}

采用 errors 包中装饰一个错误;

1
2
3
errors.Unwrap(err error)	//通过 errors.Unwrap 函数得到被嵌套的 error。	
errors.Is(err, target error) //用来判断两个 error 是否是同一个
errors.As(err error, target interface{}) //error 断言


实际示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"errors"
"fmt"
"math"
)

// 错误处理
// 1.Error
func demo1() {
// 1.声明并初始化为error类型
var errNew error = errors.New("# 错误信息来自 errors.New 方法。")
fmt.Println(errNew)

// 2.调用标准库中Errorf方法
errorfFun := fmt.Errorf("- %s", "错误信息来自 fmt.Errorf 方法。")
fmt.Println(errorfFun)

// 3.实际案例
result, err := func(a, b float64) (ret float64, err error) {
err = nil
if b == 0 {
err = errors.New("此处幂指数不能为0值,其结果都为1")
ret = 1
} else {
ret = math.Pow(a, b)
}
return
}(5, 0)

if err != nil {
fmt.Println("# 输出错误信息:", err)
fmt.Printf("5 ^ 0 = %v", result)
} else {
fmt.Printf("5 ^ 2 = %v", result)
}
}

func main() {
demo1()
}

执行结果:

1
2
3
4
# 错误信息来自 errors.New 方法。
- 错误信息来自 fmt.Errorf 方法。
# 输出错误信息: 此处幂指数不能为0值,其结果都为1
5 ^ 0 = 1


实际示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
)

// 定义一个 DivideError 结构 (值得学习)
type DivideError struct {
dividee int
divider int
}
// 实现 `error` 接口 (值得学习)
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}

// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}

func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}

执行结果:

1
2
3
4
5
100/10 =  10
errorMsg is:
Cannot proceed, the divider is zero.
dividee: 100
divider: 0


2.panic 函数

描述: 当遇到某种严重的问题时需要直接退出程序时,应该调用panic函数从而引发的panic异常, 所以panic用于不可恢复的错误类似于Java的Error。

具体流程:是当panic异常发生时,程序会中断运行,并立即执行在该goroutine,随后程序崩溃并输出日志信息。日志信息包括panic、以及value的函数调用的堆栈跟踪信息。

panic 函数语法定义:

1
func panic(v interface{})

Tips : panic函数接受任何值作为参数


示例1.数组越界会自动调用panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestA() {
fmt.Println("func TestA{}")
}

func TestB(x int) {
var a [10]int
a[x] = 111
}

func TestC() {
fmt.Println("func TestC()")
}

func main() {
TestA()
TestB(20) //发生异常,中断程序
TestC()
}

执行结果:
1
2
>>> func TestA{}
panic: runtime error: index out of rang


示例2.调用panic函数引发的panic异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func A() {
fmt.Println("我是A函数 - 正常执行")
}

func B() {
fmt.Println("我是B函数 - 正在执行")
panic("func B():panic")
fmt.Println("我是B函数 - 结束执行")
}

func C() {
fmt.Println("我是c函数 - 正在执行")
}

func demo2() {
A()
B() //发生异常,中断程序
C()
}

执行结果:
1
2
3
4
5
6
7
8
9
10
11
我是A函数 - 正常执行
我是B函数 - 正在执行
发生异常: panic
"func B():panic"
Stack:
2 0x00000000004b69a5 in main.B
at /home/weiyigeek/app/project/go/src/weiyigeek.top/studygo/Day02/05error.go:47
3 0x00000000004b6a8a in main.demo2
at /home/weiyigeek/app/project/go/src/weiyigeek.top/studygo/Day02/05error.go:57
4 0x00000000004b6ac5 in main.main
at /home/weiyigeek/app/project/go/src/weiyigeek.top/studygo/Day02/05error.go:63

WeiyiGeek.panic异常

WeiyiGeek.panic异常

Q: 什么时候使用Error,什么时候使用Panic?

  • 对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界、不可恢复的环境问题、栈溢出、数据库连接后需操作,我们才使用 panic。
  • 对于其他的错误情况,我们应该是期望使用 error 来进行判定。


3.recover 函数

描述: panic异常会导致程序崩溃,而recover函数专门用于“捕获”运行时的panic异常,它可以是当前程序从运行时panic的状态中恢复并重新获得流程控制权。

通常我们会使用 Recover 捕获 Panic 异常,例如Java中利用Catch Throwable来进行捕获异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Java
try {
...
} catch (Throwable t) {
...
}

// C++
try {
...
} catch() {

}

panic 函数语法定义:

1
func recover() interface{}

Tips: 在未发生panic时调用recover会返回nil。


流程说明: 如果调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。


示例1:panic与recover联合使用,此处采用 panic 演示的代码中的B函数进行继续修改
描述: 在Go语言中可以通过defer定义的函数去执行一些错误恢复的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func recoverB() (err error) {
fmt.Println("我是recoverB 函数 - 正在执行")
// 必须是 defer 语句中以及在panic函数前
defer func() {
x := recover()
if x != nil {
err = fmt.Errorf("# 1.进行 recover(恢复) Panic 导致的程序异常,从此之后将会继续执行后续代码:\n%v", x)
}
}() // 此处利用匿名函数
//panic("# 2.recoverB 函数中捕获 Panic")
panic(errors.New("# 2.recoverB 函数中出现 Panic"))
fmt.Println("我是recoverB 函数 - 结束执行") // 无法访问的代码
return
}
func demo3() {
A()
err := recoverB()
if err != nil {
fmt.Println("#recoverB 输出的信息:", err)
}
C()
}

执行结果:
1
2
3
4
5
我是A函数 - 正常执行
我是recoverB 函数 - 正在执行
# recoverB 输出的信息: # 1.进行 recover(恢复) Panic 导致的程序异常,从此之后将会继续执行后续代码:
# 2.recoverB 函数中出现 Panic
我是c函数 - 正在执行


示例 2.recover捕获异常后的异常,不能再次被recover捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func demo4() {
// 采用匿名函数进行立即执行该函数
defer func() { // 声明defer,
fmt.Println("----调用 defer func1 start----")
err := recover() // 此处输出为 nil ,因为panic只能被 recover 捕获一次
fmt.Printf("# 第二次 捕获 : %#v \n", err)
if err != nil {
fmt.Println(err)
}
fmt.Println("----调用 defer func1 end----")
}()

defer func() { // 声明defer,压栈操作后进先出。
fmt.Println("----调用 defer func2 start----")
if err := recover(); err != nil {
fmt.Println("# 第一次 捕获:", err) // 这里的err其实就是panic传入的内容
}
fmt.Println("----调用 defer func2 end----")
}()

panic("panic 异常 抛出 测试!")
}

执行结果:
1
2
3
4
5
6
----调用 defer func2 start----
# 第一次 捕获: panic 异常 抛出 测试!
----调用 defer func2 end----
----调用 defer func1 start----
# 第二次 捕获 : <nil>
----调用 defer func1 end----


Q: panic() 与 recover() 位置区别?
答: panic函数可以在任何地方引发(但panic退出前会执行defer指定的内容),但recover函数只有在defer调用的函数中有效并且一定要位于panic语句之前

TIPS : 非常注意下面这种“错误方式”, 他可能会形成僵尸服务进程,导致Health Check失效。

1
2
3
4
5
defer func() {
if err := recover(); err != nil {
Log.Error("Recovered Panic", err)
}
}()


Q: panic 和 os.Exit 联用时对recover的影响

  • os.Exit 退出时不会调用defer指定的函数.
  • os.Exit 退出时不会输出当前调用栈信息.


4.错误处理最佳实践

  • 1、预定义错误,code里判断
  • 2、及早失败,避免嵌套

0x02 Go语言基础之结构体

描述: Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。但 Go语言中通过结构体的内嵌配合接口比面向对象具有更高的扩展性灵活性

  • Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了(局限性)。

  • Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体(英文名称struct), 我们可以通过struct来定义自己的类型了。

简单得说: 结构体时一种数据类型,一种我们自己可以保持多个维度数据的类型。 所以与其他高级编程语言一样,Go语言也可以采用结构体的特性, 并且Go语言通过struct来实现面向对象


1.类型定义

描述: 在Go语言中有一些基本的数据类型,如string、int{}整型、float{}浮点型、boolean布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型(实际上定义了一个全新的类型)。

Tips : 我们可以基于内置的基本类型定义,也可以通过struct定义。

示例演示:

1
2
//将MyInt定义为int类型
type MyInt int

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。


2.类型别名

描述: 类型别名从字面意义上都很好理解,即类型别名本章上与原类型一样, 就比如像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

示例演示:

1
2
// TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型
type TypeAlias = Type

我们之前见过的runebyte就是类型别名,他们的定义如下:

1
2
type byte = uint8
type rune = int32

Tips: 采用int32别名创建一个变量的几种方式。

1
2
3
4
5
6
7
8
9
10
type MyInt32 = int32
// 方式1
var i MyInt32
i = 1024
// 方式2
var j MyInt32 = 1024
// 方式3
var k = MyInt32(1024)
// 方式4
l := MyInt32(1024) // 此处并非是函数,而是一个强制类型转换而已


Q: 类型定义和类型别名有何区别?

答: 类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

示例演示1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//1.类型定义
type NewInt int

//2.类型别名
type MyInt = int

// 类型定义 与 类型别名 区别演示
func demo1() {
// 类型定义的使用
var i NewInt
i = 1024
fmt.Printf("Type of i: %T, Value:%v \n", i, i)

// 类型别名的使用
var j MyInt
j = 2048
fmt.Printf("Type of j: %T, Value:%v \n", j, j)

// rune 也是类型别名底层还是int32类型
var k rune
k = '中'
fmt.Printf("Type of j: %T, Value:%c \n", k, k)
}

执行结果:
1
2
3
Type of i: main.NewInt, Value:1024 
Type of j: int, Value:2048
Type of j: int32, Value:中

结果显示说明:

  • i 变量的类型是main.NewInt,表示main包下定义的NewInt类型。
  • j 变量的类型是int,因MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。


3.结构体的定义

描述: 语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型


使用typestruct关键字来定义结构体,具体代码格式如下:

1
2
3
4
5
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}

其中:

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

举例说明: 以定义一个Person(人)结构体为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方式(0)
var v struct{}

// 方式(1)
type person struct {
name string
city string
age int8
}

// 方式(2): 同样类型的字段也可以写在一行
type person1 struct {
name, city string
age int8
}

Tips : 上面创建了结构体一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。


4.结构体实例化

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

Tips :结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。例如:var 结构体实例 结构体类型


描述: 结构体初始化是非常必要,因为没有初始化的结构体,其成员变量都是对应其类型的零值。

结构体示例化的三种语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type demo struct {
username string
city string
}

// 1.方式1.利用`.`进行调用指定属性
var m1 demo
demo.username = "WeiyiGeek"

// 2.方式2.使用键值对初始化
m2 := demo {username: "WeiyiGeek",city:"重庆",}
m2 := &demo {username: "WeiyiGeek",city:"重庆",} // ==> new(demo) 此种方式会在结构体指针里面实践。

// 3.方式3.使用值的列表初始化
m3 := demo {
"WeiyiGeek",
"重庆"
}
m3 := &demo {
"WeiyiGeek",
"重庆"
}

Tips : 特别注意在使用值的列表初始化这种格式初始化时, (1)必须初始化结构体的所有字段,(2)初始值的填充顺序必须与字段在结构体中的声明顺序一致,(3) 该方式不能和键值初始化方式混用。


示例演示: 下述演示三种基础方式进行结构体的实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 1.结构体初识还是老示例采用结构体描述人员信息并进行赋值使用
type Person struct {
name string
age uint8
sex bool
hobby []string
}

func demo1() {
// 方式1.声明一个Persin类型的变量x
var x Person
// 通过结构体中的属性进行赋值
x.name = "WeiyiGeek"
x.age = 20
x.sex = true // {Boy,Girl)
x.hobby = []string{"Basketball", "乒乓球", "羽毛球"}
// 输出变量x的类型以及其字段的值
fmt.Printf("Type of x : %T, Value : %v \n", x, x)
x.name = "WeiyiGeeker"
// 我们通过.来访问结构体的字段(成员变量), 例如x.name和x.age等。
fmt.Printf("My Name is %v \n", x.name)

// 方式2.在声明是进行赋值(key:value,或者 value)的值格式
// 使用键值对初始化
var y = Person{
name: "Go",
age: 16,
sex: false,
hobby: []string{"Computer", "ProgramDevelopment"},
}
fmt.Printf("Type of y : %T, Value : %v \n", y, y)
// 非常注意此种方式是按照结构体中属性顺序进行赋值,同样未赋值的为该类型的零值
// 使用值的列表初始化
z := Person{
"WeiyiGeek",
10,
true,
[]string{},
}
fmt.Printf("Type of z : %T, Value : %v \n", z, z)
}

执行结果:
1
2
3
4
Type of x : main.Person, Value : {WeiyiGeek 20 true [Basketball 乒乓球 羽毛球]} 
My Name is WeiyiGeeker
Type of y : main.Person, Value : {Go 16 false [Computer ProgramDevelopment]}
Type of z : main.Person, Value : {WeiyiGeek 10 true []}

Tips : 如果没有给结构体中的属性赋值,则默认采用该类型的零值。


5.结构体内存布局

描述: 结构体占用一块连续的内存,但是需要注意空结构体是不占用空间的。

连续内存空间

示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 示例1.空结构体是不占用空间的
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0


// 示例2.结构体占用一块连续的内存
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p, int8 size: %d\n", &n.a, unsafe.Sizeof(bool(true)))
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


内存对齐分析

[进阶知识点] 关于在 Go 语言中恰到好处的内存对齐
描述: 在讲解前内存对齐前, 我们先丢出两个struct结构体引发思考:

示例1. 注意两个结构体中声明不同元素类型的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Part1 struct {
a bool
b int32
c int8
d int64
e byte
}

type Part2 struct {
e byte
c int8
a bool
b int32
d int64
}

在开始之前,希望你计算一下 Part1 与 Part2 两个结构体分别占用的大小是多少呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func typeSize() {
fmt.Printf("bool size: %d\n", unsafe.Sizeof(bool(true)))
fmt.Printf("int32 size: %d\n", unsafe.Sizeof(int32(0)))
fmt.Printf("int8 size: %d\n", unsafe.Sizeof(int8(0)))
fmt.Printf("int64 size: %d\n", unsafe.Sizeof(int64(0)))
fmt.Printf("byte size: %d\n", unsafe.Sizeof(byte(0)))
fmt.Printf("string size: %d\n", unsafe.Sizeof("WeiyiGeek")) // 注意上面声明的结构体中没有该类型。
}

// 输出结果
bool size: 1
int32 size: 4
int8 size: 1
int64 size: 8
byte size: 1
string size: 16

这么一算 Part1/Part2 结构体的占用内存大小为 1+4+1+8+1 = 15 个字节。相信有的小伙伴是这么算的,看上去也没什么毛病

真实情况是怎么样的呢?我们实际调用看看,如下:

1
2
3
4
5
6
7
func main() {
part1 := Part1{}
fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
fmt.Println()
part2 := Part2{}
fmt.Printf("part2 size: %d, align: %d\n", unsafe.Sizeof(part2), unsafe.Alignof(part2))
}

执行结果:
1
2
3
4
5
part1 size: 32, align: 8
part2 size: 16, align: 8

Tips : `unsafe.Sizeof` 来返回相应类型的空间占用大小
Tips : `unsafe.Alignof` 来返回相应类型的对齐系数

从上述结果中可以看见 part1 占用32个字节而 part2 占用16字节,此时 part1 比我们上面计算结构体占用字节数多了16 Byte, 并且相同的元素类型但顺序不同的 part2 是正确的只占用了 16 Byte, 那为什么会出现这样的情况呢?同时这充分地说明了先前的计算方式是错误的。

在这里要提到 “内存对齐” 这一概念,才能够用正确的姿势去计算,接下来我们详细的讲讲它是什么


Q: What 什么是内存对齐?
答:有的小伙伴可能会认为内存读取,就是一个简单的字节数组摆放(例图1) 表示一个坑一个萝卜的内存读取方式。但实际上 CPU 并不会以一个一个字节去读取和写入内存, 相反 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小, 块大小我们称其为内存访问粒度(例图2):

WeiyiGeek.内存对齐

WeiyiGeek.内存对齐

在样例中,假设访问粒度为 4。 CPU 是以每 4 个字节大小的访问粒度去读取和写入内存的。这才是正确的姿势


Q: Why 为什么要关心对齐?

  • 你正在编写的代码在性能(CPU、Memory)方面有一定的要求
  • 你正在处理向量方面的指令
  • 某些硬件平台(ARM)体系不支持未对齐的内存访问


Q: Why 为什么要做对齐?

  • 平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况
  • 性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作
WeiyiGeek.内存申请

WeiyiGeek.内存申请

在上图中,假设从 Index 1 开始读取,将会出现很崩溃的问题, 因为它的内存访问边界是不对齐的。因此 CPU 会做一些额外的处理工作。如下:

  • 1.CPU 首次读取未对齐地址的第一个内存块,读取 0-3 字节。并移除不需要的字节 0
  • 2.CPU 再次读取未对齐地址的第二个内存块,读取 4-7 字节。并移除不需要的字节 5、6、7 字节
  • 3.合并 1-4 字节的数据
  • 4.合并后放入寄存器

从上述流程可得出,不做 “内存对齐” 是一件有点 “麻烦” 的事。因为它会增加许多耗费时间的动作, 而假设做了内存对齐,从 Index 0 开始读取 4 个字节,只需要读取一次,也不需要额外的运算。这显然高效很多,是标准的空间换时间做法


默认系数
描述: 在不同平台上的编译器都有自己默认的 “对齐系数”,可通过预编译命令 #pragma pack(n) 进行变更,n 就是代指 “对齐系数”。一般来讲,我们常用的平台的系数如下:32 位:4, 64 位:8, 例如, 前面示例中的对齐系数是8验证了我们系统是64位的。

另外要注意不同硬件平台占用的大小和对齐值都可能是不一样的。因此本文的值不是唯一的,调试的时候需按本机的实际情况考虑


不同数据类型的对齐系数

1
2
3
4
5
6
7
8
9
func main() {
fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true)))
fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0)))
fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0)))
fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0)))
fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0)))
fmt.Printf("string align: %d\n", unsafe.Alignof("WeiyiGeek"))
fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{}))
}

执行结果:
1
2
3
4
5
6
7
bool align: 1
byte align: 1
int8 align: 1
int32 align: 4
int64 align: 8
string align: 8
map align: 8

通过观察输出结果,可得知基本都是 2^n,最大也不会超过 8。这是因为我手提(64 位)编译器默认对齐系数是 8,因此最大值不会超过这个数。

Tips: 在上小节中提到了结构体中的成员变量要做字节对齐。那么想当然身为最终结果的结构体,也是需要做字节对齐的


对齐规则

  • 1.结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
  • 2.结构体本身,对齐值必须为编译器默认对齐长度(#pragma pack(n))或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值
  • 3.结合以上两点,可得知若编译器默认对齐长度(#pragma pack(n))超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的


分析流程

Step 1.首先我们先来分析 part1 结构体 到底经历了些什么,影响了 “预期” 结果

成员变量 类型 偏移量 自身占用
a bool 0 1
字节对齐 1 3
b int32 4 4
c int8 8 1
字节对齐 9 7
d int64 16 8
e byte 24 1
字节对齐 25 7
总占用大小 - - 32


成员对齐步骤

  • 第一个成员 a
    • 类型为 bool
    • 大小/对齐值为 1 字节
    • 初始地址,偏移量为 0。占用了第 1 位
  • 第二个成员 b
    • 类型为 int32
    • 大小/对齐值为 4 字节
    • 根据规则 1,其偏移量必须为 4 的整数倍。确定偏移量为 4,因此 2-4 位为 Padding(理解点)。而当前数值从第 5 位开始填充,到第 8 位。如下:axxx|bbbb
  • 第三个成员 c
    • 类型为 int8
    • 大小/对齐值为 1 字节
    • 根据规则1,其偏移量必须为 1 的整数倍。当前偏移量为 8。不需要额外对齐,填充 1 个字节到第 9 位。如下:axxx|bbbb|c…
  • 第四个成员 d
    • 类型为 int64
    • 大小/对齐值为 8 字节
    • 根据规则 1,其偏移量必须为 8 的整数倍。确定偏移量为 16,因此 9-16 位为 Padding。而当前数值从第 17 位开始写入,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd
  • 第五个成员 e
    • 类型为 byte
    • 大小/对齐值为 1 字节
    • 根据规则 1,其偏移量必须为 1 的整数倍。当前偏移量为 24。不需要额外对齐,填充 1 个字节到第 25 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd|e…


整体对齐步骤

  • 在每个成员变量进行对齐后,根据规则 2,整个结构体本身也要进行字节对齐,因为可发现它可能并不是 2^n,不是偶数倍。显然不符合对齐的规则
  • 根据规则 2,可得出对齐值为 8。现在的偏移量为 25,不是 8 的整倍数。因此确定偏移量为 32。对结构体进行对齐


结果说明:

最终 Part1 内存布局 axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx

通过本节的分析,可得知先前的 “推算” 为什么错误?
是因为实际内存管理并非 “一个萝卜一个坑” 的思想。而是一块一块。通过空间换时间(效率)的思想来完成这块读取、写入。另外也需要兼顾不同平台的内存操作情况


Step 2.通过上述我们可知根据成员变量的类型不同,其结构体的内存会产生对齐等动作。而像 part2 结构体一样,按照变量类型对齐值从小到大,进行依次排序进行占用内存空间的结果分析。

通过开头的示例我们可知,只是 “简单” 对成员变量的字段顺序(类型占用字节数从小到大排序)进行改变,就改变了结构体占用大小。

成员变量 类型 偏移量 自身占用
e byte 0 1
c int8 1 1
a bool 2 1
字节对齐 3 1
b int32 4 4
d int64 8 8
总占用大小 - - 16


成员对齐

  • 第一个成员 e
    • 类型为 byte
    • 大小/对齐值为 1 字节
    • 初始地址,偏移量为 0。占用了第 1 位
  • 第二个成员 c
    • 类型为 int8
    • 大小/对齐值为 1 字节
    • 根据规则1,其偏移量必须为 1 的整数倍。当前偏移量为 2。不需要额外对齐
  • 第三个成员 a
    • 类型为 bool
    • 大小/对齐值为 1 字节
    • 根据规则1,其偏移量必须为 1 的整数倍。当前偏移量为 3。不需要额外对齐
  • 第四个成员 b
    • 类型为 int32
    • 大小/对齐值为 4 字节
    • 根据规则1,其偏移量必须为 4 的整数倍。确定偏移量为 4,因此第 3 位为 Padding(理解点)。而当前数值从第 4 位开始填充,到第 8 位。如下:ecax|bbbb
  • 第五个成员 d
    • 类型为 int64
    • 大小/对齐值为 8 字节
    • 根据规则1,其偏移量必须为 8 的整数倍。当前偏移量为 8。不需要额外对齐,从 9-16 位填充 8 个字节。如下:ecax|bbbb|dddd|dddd

整体对齐: 由于符合规则 2,则不需要额外对齐。

结果说明:

Part2 内存布局:ecax|bbbb|dddd|dddd


总结

通过对比 Part1Part2 的内存布局,你会发现两者有很大的不同。如下:

  • Part1:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx
  • Part2:ecax|bbbb|dddd|dddd

仔细一看,Part1 存在许多 Padding。显然它占据了不少空间,那么 Padding 是怎么出现的呢?

通过本文的介绍,可得知是由于不同类型导致需要进行字节对齐,以此保证内存的访问边界

那么也不难理解,为什么调整结构体内成员变量的字段顺序就能达到缩小结构体占用大小的疑问了,是因为巧妙地减少了 Padding 的存在。让它们更 “紧凑” 了。这一点对于加深 Go 的内存布局印象和大对象的优化非常有帮

当然了,没什么特殊问题,你可以不关注这一块。但你要知道这块知识点 😄


6.指针类型结构体

结构体指针实例化

描述: 我们还可以通过使用new关键字(对基础类型进行实例化)对结构体进行实例化,得到的是结构体的地址。

创建一个结构体指针格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方式1.New 实例化
var p2 = new(person)
fmt.Printf("%T\n", p2) // *main.person
fmt.Printf("p2=%#v\n", p2) // p2=&main.person{name:"", city:"", age:0}
// 在Go语言中支持对结构体指针直接使用.来访问结构体的成员。
p2.name = "WeiyiGeek"
p2.age = 22
p2.city = "重庆"
fmt.Printf("p2=%#v\n", p2) //显示出其结构体结构: p2=&main.person{name:"WeiyiGeek", city:"重庆", age:22}

// 方式2.使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "WeiyiGeek"
p3.age = 30
p3.city = "重庆"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"WeiyiGeek", city:"重庆", age:30}

Tips :p3.name = "WeiyiGeek"其实在底层是(*p3).name = "Geeker",这是Go语言帮我们实现的语法糖。


示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
type Person struct {
name string
age uint8
sex bool
hobby []string
}

// 3.结构体指针
func demo3() {
// 方式1.结构体利用new实例化在内存中申请一块空间
var p1 = new(Person)
(*p1).name = "WeiyiGeek" // 取得地址存放的值并将其进行覆盖
p1.age = 20 // Go语言的语法糖自动根据指针找到对应地址的值并将其值覆盖。
fmt.Printf("Type of p1 : %T, Struct 实例化结果: %#v\n", p1, p1)

// 方式2.采用取地址&符号进行实例化结构体(效果与new差不多)
p2 := &Person{}
(*p2).name = "Golang" // 取得地址存放的值并将其进行覆盖
p2.age = 12 // Go语言的语法糖自动根据指针找到对应地址的值并将其值覆盖。
p2.sex = true
fmt.Printf("Type of p2 : %T, Struct 实例化结果: %#v\n", p2, p2)

// 5.使用键值对初始化(也可以对结构体指针进行键值对初始化)
// 当某些字段没有初始值的时候,该字段可以不写。此时没有指定初始值的字段的值就是该字段类型的零值。
p3 := &Person{
name: "北京",
}
fmt.Printf("p3 Value = %#v \n", p3)

// 6.使用值的列表初始化
// 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p4 := &Person{
"WeiyiGeek",
20,
false,
[]string{},
}
fmt.Printf("p4 Value = %#v \n", p4)

// 4.探究Struct结构体开辟的是连续的内存空间(内存对齐效果)
fmt.Printf("*p2 size of = %d, p2 align of = %d \n", unsafe.Sizeof(*p2), unsafe.Alignof(p2))
fmt.Printf("Pointer p2 = %p, \name = %p,p2.name size of = %d \nnage = %p, p2.age size of = %d\nsex = %p, p2.sex size of = %d\nhobby = %p,p2.hobby size of = %d \n", p2, &p2.name, unsafe.Sizeof((*p2).name), &p2.age, unsafe.Sizeof(p2.age), &p2.sex, unsafe.Sizeof(p2.sex), &p2.hobby, unsafe.Sizeof(p2.hobby))
}

执行结果:
1
2
3
4
5
6
7
8
9
10
11
Type of p1 : *main.Person, Struct 实例化结果: &main.Person{name:"WeiyiGeek", age:0x14, sex:false, hobby:[]string(nil)}
Type of p2 : *main.Person, Struct 实例化结果: &main.Person{name:"Golang", age:0xc, sex:true, hobby:[]string(nil)}
p3 Value = &main.Person{name:"北京", age:0x0, sex:false, hobby:[]string(nil)}
p4 Value = &main.Person{name:"WeiyiGeek", age:0x14, sex:false, hobby:[]string{}}
// 结构体占用一块连续的内存地址。
*p2 size of = 48, p2 align of = 8
Pointer p2 = 0xc0001181b0,
name = 0xc0001181b0,p2.name size of = 16
age = 0xc0001181c0, p2.age size of = 1
sex = 0xc0001181c1, p2.sex size of = 1
hobby = 0xc0001181c8,p2.hobby size of = 24

从上述Person 结构体指针 p2 内存对齐结果中可知,元素类型占用的大小 16 + 1 + 1 + 24 = 42 Byte, 但是收到整体对齐的规则约束,该 p2 指针类型的结构体占用的内存空间大小为 48 Byte。


结构体指针函数传递

描述: 我们可以将指针类型的结构体进行地址传递在函数中修改其元素属性内容。

示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func personChange(p Person) {
p.name = "Change" // 拷贝的是 p4 指针类型的结构的副本(值引用)
}

func personPointerChange(p *Person) {
p.name = "PointerChange" // 传递的是 p4 的地址,所以修改的是 p4.name 的属性值
}

func demo4() {
p4 := &Person{
name: "WeiyiGeek",
}
personChange(*p4) // 值传递
fmt.Printf("personChange(*p4) -> name = %v \n", p4.name)

personPointerChange(p4) // 地址传递
fmt.Printf("personPointerChange(*p4) -> name = %v", p4.name)
}

执行结果:

1
2
personChange(*p4) ->	name = WeiyiGeek 
personPointerChange(*p4) -> name = PointerChange

Tips : Go 语言中函数传的参数永远传的是拷贝, 如果要修改原数据必须进行取地址传递并修改。


结构体指针构造函数

描述: Go语言的结构体没有构造函数,但我们可以自己实现一个。

Tips: Go语言构造函数约定俗成用new进行开头,例如 newDog()

例如: 下方的代码就实现了一个person的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// (1) 结构体构造函数
type Person struct {
name, city string
age uint8
}

// 方式1.值传递(拷贝副本) 返回的是结构体
func newPerson(name, city string, age uint8) Person {
return Person{
name: name,
city: city,
age: age,
}
}

// 方式2.地址(指针类型变量)传递返回的是结构体指针
func newPointerPerson(name, city string, age uint8) *Person {
return &Person{
name: name,
city: city,
age: age,
}
}

func demo1() {
// (1) 通过定义的函数直接进行结构体的初始化(值拷贝的方式)
var person = newPerson("WeiyiGeek", "重庆", 20)
fmt.Printf("newPerson Type : %T, Value : %v\n", person, person)
// (2) 通过定义的函数直接传入指针类型的结构体进行初始化(地址拷贝的方式)
var pointerperson = newPointerPerson("Go", "world", 12)
fmt.Printf("newPointerPerson Type : %T, Value : %v\n", pointerperson, pointerperson)
}

执行结果:
1
2
newPerson Type : main.Person, Value : {WeiyiGeek 重庆 20}
newPointerPerson Type : *main.Person, Value : &{Go world 12}

Tips :因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。


7.结构体方法与接收者

描述: Go语言中的方法(Method)是一种作用于特定类型变量的函数, 这种特定类型变量叫做接收者(Receiver), 接收者的概念就类似于其他语言中的 this 或者 self

结构体方法

定义格式:

1
2
3
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}

其中,

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如 Person类型 的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
  • 接收者类型:接收者类型和参数类似,可以是指针类型非指针类型
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。

Tips : 结构体方法名称写法约束规定,如果其标识符首字母是大写的就表示对外部包可见(例如 java 中 public 指定的函数或者是类公共的),如果其标识符首字母是小写的表示对外部不可见(不能直接调用), 当然这是一种开发习惯非强制必须的。


示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//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("WeiyiGeek", 25)
p1.Dream() // WeiyiGeek的梦想是学好Go语言!
}

Tips : 方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。


值类型的接收者

描述: 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。

在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身

例如: 我们为 Person 添加一个SetAge方法,来修改实例变量的年龄, 验证是否可被修改。

1
2
3
4
5
6
7
8
9
10
11
//  使用值接收者:SetAge2 设置p的年龄
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("WeiyiGeek", 25)
p1.Dream()
fmt.Println(p1.age) // 25
p1.SetAge2(30) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 25
}


指针类型的接收者

描述: 指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。此种方式就十分接近于其他语言中面向对象中的this或者self达到的效果。

例如:我们为 Person 添加一个SetAge方法,来修改实例变量的年龄。

1
2
3
4
5
6
7
8
9
10
11
// 使用指针接收者 : SetAge 设置p的年龄: 传入的 Person 实例化后的变量的地址 p ,并通过p.属性进行更改其内容存储的内容。
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
//调用
func main() {
p1 := NewPerson("WeiyiGeek", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}

Q: 什么时候应该使用指针类型接收者?

  • 一是、需要修改接收者中的值。
  • 二是、接收者是拷贝代价比较大的大对象。
  • 三是、保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

案例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 结构体方法和接收者, 只能被Person结构体实例化的对象进行调用,不能像函数那样直接调用。此处还是采用上面声明的结构体
func (p Person) ChangePersonName(name string) {
p.name = name
fmt.Printf("# 执行 -> ChangePersonName 方法 -> p Ptr : %p ,value : %v\n", &p, p.name)
}
func (p *Person) ChangePointerPersonName(name string, age uint8) {
p.name = name
p.age = age
fmt.Printf("# 执行 -> ChangePointerPersonName 方法 -> p Ptr : %p (关键点),value : %v\n", p, p.name)
}
func demo2() {
// 利用构造函数进行初始化
p1 := newPerson("小黄", "Beijing", 20)
fmt.Printf("p1 Pointer : %p , Struct : %+v \n", &p1, p1)
// 调用 ChangePersonName 方法
p1.ChangePersonName("小黑") // 值类型的接收者(修改的是p1结构体副本的值)
fmt.Printf(" p1 Pointer : %p , Struct : %+v \n", &p1, p1)
// 调用 ChangePointerPersonName 方法
p1.ChangePointerPersonName("小白", 30) //指针类型的接收者 (修改的是p1结构体元素的值)
fmt.Printf(" p1 Pointer : %p , Struct : %+v \n", &p1, p1)
}

执行结果:
1
2
3
4
5
p1 Pointer : 0xc00010c150 , Struct : {name:小黄 city:Beijing age:20} 
# 执行 -> ChangePersonName 方法 -> p Ptr : 0xc00010c1b0 ,value : 小黑
p1 Pointer : 0xc00010c150 , Struct : {name:小黄 city:Beijing age:20}
# 执行 -> ChangePointerPersonName 方法 -> p Ptr : 0xc00010c150 (关键点),value : 小白
p1 Pointer : 0xc00010c150 , Struct : {name:小白 city:Beijing age:30}


任意类型的接收者

描述: 在Go语言中接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 3.任意类型的接收者都可以拥有自己的方法
// MyInt 将int定义为自定义MyInt类型
type MyInt int
// SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello(s string) {
fmt.Printf("Hello, 我是一个int, %s", s)
}
// ChangeM 为MyInt添加一个ChangeM的方法
func (m *MyInt) ChangeM(newm MyInt) {
fmt.Printf("# Start old m : %d -> new m : %d \n", *m, newm)
*m = newm // 关键点修改m其值,此处非拷贝的副本
fmt.Printf("# End old m : %d -> new m : %d \n", *m, newm)
}
func demo3() {
// 声明
var m1 MyInt
// 赋值
m1 = 100
// 方式2
m2 := MyInt(255)
// 调用类型方法
m1.SayHello("Let'Go")
fmt.Printf("SayHello -> Type m1 : %T, value : %+v \n", m1, m1)
// 调用类型方法修改m1其值
m1.ChangeM(1024)
fmt.Printf("ChangeM -> Type m1 : %T, value : %+v \n", m1, m1)
}

执行结果:
1
2
3
4
Hello, 我是一个int, Let'GoSayHello -> Type m1 : main.MyInt, value : 100 
# Start old m : 100 -> new m : 1024
# End old m : 1024 -> new m : 1024
ChangeM -> Type m1 : main.MyInt, value : 1024

Tips : 非常注意,非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。


8.匿名结构体与匿名字段

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

示例演示:

1
2
3
4
5
6
7
8
// 匿名结构体(只能使用一次,所以常常使用与临时场景)
// 2.匿名结构体(只能使用一次,所以常常使用与临时场景)
func demo2() {
var temp struct {title string;address []string}
temp.title = "地址信息"
temp.address = []string{"中国", "重庆", "江北区"}
fmt.Printf("Type of temp : %T\nStruct define: %#v \nValue : %v\n", temp, temp, temp)
}

执行结果:
1
2
3
Type of temp : struct { title string; address []string }
Struct define: struct { title string; address []string }{title:"地址信息", address:[]string{"中国", "重庆", "江北区"}}
Value : {地址信息 [中国 重庆 江北区]}


描述: 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

Tips: 这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

示例演示:

1
2
3
4
5
6
7
8
type Anonymous struct {
string
int
}
func demo4() {
a1 := Anonymous{"WeiyiGeek", 18}
fmt.Printf("Struct: %#v ,字段1: %v , 字段2: %v \n", a1, a1.string, a1.int)
}

执行结果:
1
Struct: main.Anonymous{string:"WeiyiGeek", int:18} ,字段1: WeiyiGeek , 字段2: 18


9.嵌套结构体与匿名字段

描述: 结构体中可以嵌套包含另一个结构体或结构体指针, 并且上面user结构体中嵌套的Address结构体也可以采用匿名字段的方式。

并且为了防止嵌套结构体的相同的字段名冲突,所以在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。

示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//Address 地址结构体
type Address struct {
Province string
City string
}

//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}

//AnonUser 用户结构体
type AnonUser struct {
Name string
Gender string
Address // 采用结构体的匿名字段来嵌套结构体Address
Email // 采用结构体的匿名字段来嵌套结构体Email
}

// 1.嵌套结构体
func demo1() {
// 结构体初始化
user := User{
Name: "WeiyiGeek",
Gender: "男",
Address: Address{
Province: "重庆",
City: "重庆",
},
}
fmt.Printf("Struct : %#v \n", user)
fmt.Printf("Name = %v, Address City = %v \n", user.Name, user.Address.City)
}

// 2.嵌套匿名字段防止字段名称冲突
func demo2() {
var anonuser = AnonUser{
Name: "WeiyiGeek",
Gender: "男",
Address: Address{
"重庆",
"重庆",
},
Email: Email{
"Master@weiyigeek.top",
"2021年8月23日 10:21:36",
},
}
fmt.Printf("Struct : %#v\n", anonuser)
fmt.Printf("Name = %v,Address Province = %v, Email Account = %v \n", anonuser.Name, anonuser.Address.Province, anonuser.Email.Account)
}

执行结果:
1
2
3
4
5
6
7
// 嵌套结构体
Struct : main.User{Name:"WeiyiGeek", Gender:"男", Address:main.Address{Province:"重庆", City:"重庆"}}
Name = WeiyiGeek, Address City = 重庆

//嵌套匿名字段
Struct : main.AnonUser{Name:"WeiyiGeek", Gender:"男", Address:main.Address{Province:"重庆", City:"重庆"}, Email:main.Email{Account:"Master@weiyigeek.top", CreateTime:"2021年8月23日 10:21:36"}}
Name = WeiyiGeek,Address Province = 重庆, Email Account = Master@weiyigeek.top

Tips : 当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。


10.结构体的“继承”

描述: Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import "fmt"

// 父
type Animal struct{ name string }

func (a *Animal) voice(v string) {
fmt.Printf("我是动物,我叫 %v, 我会叫 %s,", a.name, v)
}

// 子
type Dog struct {
eat string
*Animal
}

func (d *Dog) love() {
fmt.Printf("狗狗喜欢吃的食物是 %v.\n", d.eat)
}

type Cat struct {
eat string
*Animal
}

func (c *Cat) love() {
fmt.Printf("猫猫喜欢吃的食物是 %v.\n", c.eat)

}

func main() {
d1 := &Dog{
//注意嵌套的是结构体指针
Animal: &Animal{
name: "小黄",
},
eat: "bone",
}
d1.voice("汪汪.汪汪.")
d1.love()

c1 := &Cat{
//注意嵌套的是结构体指针
Animal: &Animal{
name: "小白",
},
eat: "fish",
}
c1.voice("喵喵.喵喵.")
c1.love()
}

执行结果:
1
2
我是动物,我叫 小黄, 我会叫 汪汪.汪汪.,狗狗喜欢吃的食物是 bone.
我是动物,我叫 小白, 我会叫 喵喵.喵喵.,猫猫喜欢吃的食物是 fish.


11.结构体与“JSON”

描述: JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,其优点是易于人阅读和编写,同时也易于机器解析和生成。

Tips : JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号””包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

在Go中我们可以通过结构体序列号生成json字符串,同时也能通过json字符串反序列化为结构体得实例化对象,在使用json字符串转换时, 我们需要用到"encoding/json"包。

结构体标签(Tag)
描述: Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来,Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:key1:"value1" key2:"value2",可以看到它由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

例如: 我们为Student结构体的每个字段定义json序列化时使用的Tag。

1
2
3
4
5
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}

注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。


示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
"encoding/json"
"fmt"
)

// 结构体转json字符串的三种示例
// 结构体中的字段首字母大小写影响的可见性,表示不能对外使用
type Person1 struct{ name, sex string }

// 结构体对象字段可以对外使用
type Person2 struct{ Name, Sex string }

// 但json字符串中键只要小写时可以采用此种方式
type Person3 struct {
Name string `json:"name"`
Sex string `json:"age"`
}

// # 结构体实例化对象转JSON字符串
func serialize() {
// 示例1.字段首字母大小写影响的可见性
person1 := &Person1{"weiyigeek", "男孩"}
person2 := &Person2{"WeiyiGeek", "男生"}
person3 := &Person3{"WeiyiGeek", "男人"}

//序列化
p1, err := json.Marshal(person1)
p2, err := json.Marshal(person2)
p3, err := json.Marshal(person3)
if err != nil {
fmt.Printf("Marshal Failed :%v", err)
return
}

// 由于返回是一个字节切片,所以需要强转为字符串
fmt.Printf("person1 -> %v\nperson2 -> %v\nperson3 -> %v\n", string(p1), string(p2), string(p3))
}

// # JSON字符串转结构体实例化对象

type Person4 struct {
Name string `json:"name"`
Sex string `json:"sex"`
Addr [3]string `json:"addr"`
}

func unserialize() {
jsonStr := `{"name": "WeiyiGeek","sex": "man","addr": ["中国","重庆","渝北"]}`
p4 := Person4{}

// 在其内部修改p4的值
err := json.Unmarshal([]byte(jsonStr), &p4)
if err != nil {
fmt.Printf("Unmarhal Failed: %v", err)
return
}
fmt.Printf("jsonStr -> Person4 : %#v\nPerson4.name : %v\n", p4, p4.Name)
}

func main() {
serialize()
unserialize()
}

执行结果:
1
2
3
4
5
person1 -> {}
person2 -> {"Name":"WeiyiGeek","Sex":"男生"}
person3 -> {"name":"WeiyiGeek","age":"男人"}
jsonStr -> Person4 : main.Person4{Name:"WeiyiGeek", Sex:"man", Addr:[3]string{"中国", "重庆", "渝北"}}
Person4.name : WeiyiGeek


12.结构体和方法补充知识点

描述: 因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。

示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import "fmt"

type Person struct {
name string
age int8
dreams []string
}

// 不推荐的方式
func (p *Person) SetDreams(dreams []string) {
p.dreams = dreams
}

// 正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。
func (p *Person) NewSetDreams(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}

func main() {
// (1) 不安全的方式
p1 := Person{name: "小王子", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p1.SetDreams(data)
// 你真的想要修改 p1.dreams 吗?
data[1] = "不睡觉" // 会覆盖更改切片中的值从而影响p1中的dreams字段中的值
fmt.Println(p1.dreams) // [吃饭 不睡觉 打豆豆]

// (2) 推荐方式
p2 := Person{name: "WeiyiGeek", age: 18}
data2 := []string{"计算机", "网络", "编程"}
p2.NewSetDreams(data2)
data2[1] = "NewMethod" // 由于NewSetDreams返回中是将拷贝的副本给p2的dreams字段,所以此处更改不会影响其值,
fmt.Println(p2.dreams) // [计算机 网络 编程]
}

执行结果:
1
2
[吃饭 不睡觉 打豆豆]
[计算机 网络 编程]

Tips: 同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。