今天的内容大纲
- 函数 defer和panic
- 结构体 重点
- 面向对象
- 接口
- 错误处理
- io操作
- 反射?
今天的内容重点
- 函数 defer和panic 次重点
- 结构体 重点
- 接口 次重点
- io操作 次次重点
今天的作业
- 考察点:map增量更新+接口+结构体方法
- jobManager 增量更新job
- 要求写接口,有start stop hash三个方法
- 写两个结构体,分别实现上述结构
- 写一个jobmanager管理,要求有增量更新
- 远端sync
- 本地有,远端没有,要删除
- 本地没有,远端有,要新增
- 本地有,远端有,不管
下一节要讲什么内容
- write 和 stdin stderr stdout
- 反射
- 包与工程
- 单元测试和基准测试
函数定义
func function_name( [parameter list] ) [return_types] {
函数体
}
- func:函数由 func 开始声明
- function_name:函数名称,函数名和参数列表一起构成了函数签名。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合。
简单使用
package main
import "fmt"
func max(n1, n2 int) int {
if n1 > n2 {
return n1
}
return n2
}
func main() {
fmt.Println(max(1, 10))
fmt.Println(max(-1, -2))
}
Go语言支持对返回值进行命名
- 命名的返回值变量的默认值为类型的0值
- 相当于var xxx
- map必须使用前make
package main
import "fmt"
func f1() (names []string, m map[string]int, num int) {
m = make(map[string]int)
m["k1"] = 2
return
}
func main() {
a, b, c := f1()
fmt.Println(a, b, c)
}
不定长参数
- 如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
- 函数内部的参数是数组
- 外部传参为数组时 ,可以使用 arr... ```go package main
import "log"
// 变长参数 返回最小值 func min(a ...int) int { if len(a) == 0 { return 0 } min := a[0] for _, v := range a { if v < min { min = v } } return min }
func main() { x1 := min(1, 7, 8, 3, 23, 9) log.Printf("[直接传多个参数]:%d", x1) s1 := []int{2, 3, 54, 10, 32, 5, 7, 34} x2 := min(s1...) log.Printf("[数组传参传]:%d", x2) }
## 按值传递 or 按引用传递
- Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量
- 如果希望值传递类型在函数中也可以修改变量,那么应该传递指针
- 引用类似的数据都是引用传递的,slice map chan interface
> 值类型
```go
package main
import (
"log"
"time"
)
// 值类型 值传递
func add1(num int) {
log.Printf("[值传递][传入的参数值为:%d]", num)
num++
log.Printf("[值传递][add1计算后的值为:%d]", num)
}
// 值类型 引用传递
func add2(num *int) {
log.Printf("[引用传递][传入的参数值为:%d]", *num)
*num++
log.Printf("[引用传递][add2计算后的值为:%d]", *num)
}
func main() {
num := 1
log.Printf("[局部遍历的值:%d]", num)
add1(num)
time.Sleep(1 * time.Second)
log.Printf("[局部遍历的值:%d]", num)
add2(&num)
time.Sleep(1 * time.Second)
log.Printf("[局部遍历的值:%d]", num)
}
引用类型 ```go package main
import ( "log" )
// 引用类型 引用传递 func mod(s1 []int, m1 map[string]string) {
log.Printf("[引用传递][传入的参数为:%v %v]", s1, m1)
s1[0] = 100
m1["a"] = "a2"
log.Printf("[引用传递][函数内部处理完的值为:%v %v]", s1, m1)
} func main() { s1 := []int{1, 2, 3} m1 := map[string]string{"a": "a1", "b": "b1"} mod(s1, m1) log.Printf("[引用传递][函数外部的值为:%v %v]", s1, m1) }
## 匿名函数
- 匿名函数最大的用途是来模拟块级作用域,避免数据污染的。
> 不带参
```go
package main
import "fmt"
func main() {
f := func() {
fmt.Println("abdc")
}
f()
fmt.Printf("%T", f)
}
带参数的
package main
import "fmt"
func main() {
f := func(args string) {
fmt.Println(args)
}
f("abdc")
fmt.Printf("%T", f)
}
带返回值 ```go package main
import "fmt"
func main() {
f := func() string { return "abdc" } a := f() fmt.Printf("%T\n", f) fmt.Println(a)
}
> 返回多个匿名函数
```go
package main
import "fmt"
func FGen(x, y int) (func() int, func(int) int) {
//求和的匿名函数
sum := func() int {
return x + y
}
// (x+y) *z 的匿名函数
avg := func(z int) int {
return (x + y) * z
}
return sum, avg
}
func main() {
f1, f2 := FGen(1, 2)
fmt.Println(f1())
fmt.Println(f2(3))
}
闭包
- 闭包:说白了就是函数的嵌套,内层的函数可以使用外层函数的所有变量,即使外层函数已经执行完毕。
- 不同闭包函数变量内部维护不是同一个变量
- 所有的匿名函数都是闭包 ```go package main
import "fmt"
func Greeting() func(string) string { a := " 你好啊 " return func(s string) string { a += s return a } }
func main() {
g1 := Greeting()
g2 := Greeting()
fmt.Println(g1("小乙"))
fmt.Println(g1("李逵"))
fmt.Println(g1("宋江"))
fmt.Println(g2("宋江"))
fmt.Println(g2("李逵"))
}

- 累加器例子
```go
package main
import (
"log"
)
func add1() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
log.Printf("[普通的累加器]:%d", sum)
}
func add2() func(int) int {
// 自由变量
sum := 0
return func(i int) int {
sum += i
return sum
}
}
func callCF() {
f := add2()
sum := 0
for i := 0; i < 10; i++ {
sum = f(i)
log.Printf("[闭包的累加器]:%d", sum)
}
}
func main() {
add1()
// 闭包的
callCF()
}
- 函数式编程的应用场景
- 对于数据的长流程处理
- 类似流水线,装配模式
- 可以随时增删流程
- 装饰器
递归函数必须使用闭包
递归函数定义
- 递归指函数可以直接或间接的调用自身。
- 递归函数通常有相同的结构:一个跳出条件和一个递归体
- 所谓跳出条件就是根据传入的参数判断是否需要停止递归,而递归体则是函数自身所做的一些处理。
- 斐波拉契数列
package main
import "log"
func fib(i int) int {
if i == 0 || i == 1 {
return i
}
return fib(i-1) + fib(i-2)
}
func main() {
log.Println(fib(7))
for i:=0;i<10;i++{
log.Printf("[%d=%d]",i,fib(i))
}
}
- 1+2+....100
- 从低位加从高位加 ```go package main
import ( "log" )
// 实现1+...+100 // 从低位加到高位 func sum1(num int) int { if num == 100 { return num } return num + sum1(num+1) }
// 从高位加到低位 func sum2(num int) int { if num == 1 { return num } return num + sum2(num-1) }
func main() { log.Printf("从低位加到高位 :%d", sum1(1)) log.Printf("从高位加到低位 :%d", sum2(100)) }
## defer语句
- 类似于栈,类比python中的 上下文管理器 `__exit__`
- Go 语言的 defer 会在当前函数返回前执行传入的函数
- 它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源
> defer 保证文件描述符被close
```go
f,err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
倒序打印num ```go package main
import "fmt"
func main() { for i := 0; i < 5; i++ { defer fmt.Println(i) } }
> defer 预计算参数 和defer func()
- 使用defer打印时间差
```go
package main
import (
"log"
"time"
)
func main() {
start := time.Now()
log.Printf("开始时间为:%v", start)
defer log.Printf("时间差:%v", time.Since(start))
time.Sleep(3 * time.Second)
log.Printf("函数结束")
/*
时间差不是3秒,而是很小
2021/07/18 18:04:10 开始时间为:2021-07-18 18:04:10.6563346 +0800 CST m=+0.004339401
2021/07/18 18:04:13 函数结束
2021/07/18 18:04:13 时间差:43.6851ms
*/
}
- 造成上述问题的原因
- Go 语言中所有的函数调用都是传值的
- 调用 defer 关键字会立刻拷贝函数中引用的外部参数 ,包括startAt 和time.Since中的Now
- defer的函数在压栈的时候也会保存参数的值,并非在执行时取值。
- 怎么解决这个问题:使用defer func() ```go package main
import ( "log" "time" )
func main() { start := time.Now() log.Printf("开始时间为:%v", start) defer func() { log.Printf("开始调用defer") log.Printf("时间差:%v", time.Since(start)) log.Printf("结束调用defer") }() time.Sleep(3 * time.Second)
log.Printf("函数结束")
/*
2021/07/18 18:09:01 开始时间为:2021-07-18 18:09:01.7131174 +0800 CST m=+0.004520101
2021/07/18 18:09:04 函数结束
2021/07/18 18:09:04 开始调用defer
2021/07/18 18:09:04 时间差:3.0482416s
2021/07/18 18:09:04 结束调用defer
*/
}
- 因为拷贝的是函数指针
# defer 和return
- f1返回1
```go
func f1() (res int) {
defer func() {
res++
}()
return 0
}
原理解读
- return xxx这条语句不是一条原子操作
- 先给返回值赋值
- return 1要翻译成 res =1 +return
- 然后调用defer语句
- 最后返回调用函数中 ,空的return
```shell script 返回值 = xxx 调用defer 函数 空的return
> f1可以改造成
```go
func f11() (res int) {
res = 0
func() {
res++
}()
return
}
f2改造成 ```go func f22() (res int) { t := 5 res = t // 真正的res返回值 赋值 func() { t = t + 5 }() return }
> f3改造成
```go
func f33() (res int) {
res = 0 // 赋值指令
func(res int) {
res = res + 5 //值传递,深拷贝, 操作的是副本
}(res)
return // 空的return
}
- f4的结果是什么
- 为什么?
- 改造成解释defer和return的顺序函数 ```go package main
import "fmt"
func f4() (t int) {
defer func() {
t = t * 10
}()
return 1
}
func main() { fmt.Println(f4()) }
> f4 改造成这样
```go
func f44() (t int) {
t = 1 // 先给返回值赋值
func() {
t = t * 10 // 匿名函数,引用闭包的自由变量 t ,会直接更改t的值
}()
return
}
全部的示例 ```go package main
import ( "fmt" "log" )
// 返回1 func f1() (res int) { defer func() { res++ }() return 0 }
func f11() (res int) { res = 0 func() { res++ }() return
}
// 匿名不带参 func f2() (res int) { t := 5 defer func() { t = t + 5 }() return t }
// f2可以改造为 func f22() (res int) { t := 5 res = t // 真正的res返回值 赋值 func() { t = t + 5 }() log.Printf("f22.t=%d",t) return }
// 匿名带参数 func f3() (res int) { defer func(res int) { res = res + 5 }(res) return res } func f33() (res int) { res = 0 // 赋值指令 func(res int) { res = res + 5 //值传递,深拷贝, 操作的是副本 }(res)
return // 空的return
}
func f4() (t int) {
defer func() {
t = t * 10
}()
return 1
}
func f44() (t int) { t = 1 // 先给返回值赋值 func() { t = t * 10 // 匿名函数,引用闭包的自由变量 t ,会直接更改t的值 }()
return
}
func main() { fmt.Println(f1()) fmt.Println(f11()) fmt.Println(f2()) fmt.Println(f22()) fmt.Println(f3()) fmt.Println(f33()) fmt.Println(f4()) fmt.Println(f44()) }
## 没有panic的defer

## defer和return

## defer 和 panic

- panic会触发defer出栈执行
- defer 最大的功能是 panic 后依然有效
- 所以defer可以保证你的一些资源一定会被关闭,从而避免一些异常出现的问题。
- 遇到panic会执行已经压栈的defer ,
> 不用recovery捕获异常
- panic之后的defer 还没来得及压栈
```go
package main
import "fmt"
func main() {
defer_func()
}
func defer_func() {
defer func() {
fmt.Println("1")
}()
defer func() {
fmt.Println("2")
}()
defer func() {
fmt.Println("3")
}()
panic("我是panic")
defer func() {
fmt.Println("4")
}()
/*
3
2
1
panic: 我是panic
*/
}
使用recover捕获异常
- 如果遇到recover,返回recover处继续往下执行,不会抛出panic异常信息
- 注意是recover处向下执行,而不会返回再执行 ```go package main
import "fmt"
func main() { defer_func() }
func defer_func() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("被recover捕获的panic 内容是:%v\n", err)
}
fmt.Println("1")
}()
defer func() {
fmt.Println("2")
}()
defer func() {
fmt.Println("3")
}()
panic("我是panic")
fmt.Println("4")
}
- recover 在几个defer的中间
```go
package main
import "fmt"
func main() {
defer_func()
}
func defer_func() {
defer func() {
fmt.Println("2")
}()
defer func() {
fmt.Println("3")
}()
defer func() {
if err := recover(); err != nil {
fmt.Printf("被recover捕获的panic 内容是:%v :%T\n", err, err)
}
fmt.Println("1")
}()
panic("我是panic")
fmt.Println("4")
/*
被recover捕获的panic 内容是:我是panic :string
1
3
2
*/
}
defer中含有panic
- panic仅有最后一个可以被recover捕获。
- 这个异常将会覆盖掉main中的异常panic("panic"),最后这个异常被第二个执行的defer捕获到。 ```go package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
} else {
fmt.Println("哈哈")
}
}()
defer func() {
panic("defer内部触发的panic")
}()
panic("defer触发的panic")
}
## defer中函数参数包含子函数
- 先压栈1,再压栈2
- 子函数在压栈是就要执行获取返回值作为参数
- 因为压栈时需要连同函数地址、函数形参一同进栈
- 所以调用顺序为 压栈f1,调用f3,压栈f2,调用f4 调用f2,f1
```go
package main
import "fmt"
func function(index int, value int) int {
fmt.Println(index)
return index
}
func main() {
defer function(1, function(3, 0))
defer function(2, function(4, 0))
}
/*
3
4
2
*/
结构体知识点
- 结构体定义
- 结构体声明
- 结构体初始化
- new函数介绍
- 属性访问与修改
- 结构体命名嵌入
- 结构体匿名嵌入
- 结构体指针类型嵌入
- 结构体可见性
- 结构体浅拷贝和深拷贝
结构体的特点
- Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
- 结构体的目的就是把数据聚集在一起,以便能够更加便捷地操作这些数据
结构体和类的概念
- go里面没有类, go 用一种特殊的方式,把结构体本身看作一个类。
- 一个成熟的类,具备成员变量和成员函数,结构体本身就有成员变量,再给他绑定上成员函数,是不是就可以了!
01 结构体定义
定义方法
- 在 Golang 中最常用的方法是使用关键字 type 和 struct 来定义一个结构体,例如 ```go type Person struct { Name string Age int Labels map[string]string }
### 字段说明
- 结构体中的字段可以是任何类型,甚至是结构体本身,也可以是函数或者接口
## 02 结构体声明和初始化
### 方法一:使用var 声明变量并初始化
- 关键字var可以使用结构类型声明变量,并初始化为零值,举例
- 关键字 var 创建了类型为 Person 且名为 p 的变量,p被称作类型 Person 的一个实例(instance)。
```go
package main
import "fmt"
type Person struct {
Name string
Age int
Labels map[string]string
}
func main() {
var p Person
fmt.Println(p)
}
var 不加等号 :初始化零值
- 当声明变量时,这个变量对应的值总是会被初始化。
- 使用var关键字用零值初始化,对数值类型来说,零值是 0;对字符串来说,零值是空字符串;对布尔类型,零值是 false。
- var + NewXXX 初始化全局变量
var 等号 :使用自定义数据初始化
package main
import (
"log"
)
type Person struct {
Name string
Age int
Labels map[string]string
}
func main() {
var p = Person{Name: "xiaoyi"}
log.Printf("%v ", p)
log.Printf(" %+v", p)
}
var特点:可以在函数外部使用,可以声明初始化全局变量
- 举例使用 var=XXXX 初始化全局缓存
方法二:使用短变量声明操作符(:=) 初始化
特点1:使用自定义值初始化
fill结构体字段goland快捷键 alt+enter
- 写法1:明确写出字段的名字以及对应的值,可以不用关心定义时的字段顺序 ```go package main
import ( "log" )
type Person struct { Name string Age int Labels map[string]string }
var p = Person{Name: "xiaoyi"}
func main() { p1 := Person{ Age: 10, Name: "小乙",
Labels: map[string]string{},
}
p2 := Person{"李逵", 20, map[string]string{}}
log.Printf(" %+v", p1)
log.Printf(" %+v", p2)
}
- 写法2: 不写字段名称,只写值,这时字段的顺序必须和定义时一致`xiaoyi:= Person{"xiaoyi",18,"xy@qq.com"}`
```go
p2 := Person{"李逵", 20, map[string]string{}}
特点2:不可以在函数外部使用,不可以声明初始化全局变量
03 new和 make
new 只能把内存初始化为零值并返回其指针
- 举例来对比下 new 和 var返回的类型 ```go package main
import ( "log" )
type Person struct { Name string Age int Labels map[string]string }
func main() {
p1 := new(Person)
p1 = &Person{
Age: 10,
Name: "小乙",
Labels: map[string]string{},
}
log.Printf(" %+v", p1)
}
### new和make对比
- 简单说 new只分配内存,make用于slice,map,和channel的初始化。
- 对比表格
| 函数名 | 适用范围 | 返回值 | 填充值 |
| ---- | ---- | ---- | ---- |
| new | new可以对所有类型进行内存分配 | new 返回指针 | new 填充零值 |
| make | make 只能创建类型(slice map channel) | make 返回引用 | make 填充非零值 |
```go
func make(t Type, size ...IntegerType) Type
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
new在结构体struct上的不同
- new返回的是指针、struct返回的是值
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
Labels map[string]string
}
func main() {
var p1 *Person = new(Person)
p1.Name = "小乙"
p1.Age = 18
var p2 Person = Person{
Name: "李逵",
Age: 20,
}
fmt.Println(p1, p2)
}
属性访问与修改
使用选择器访问属性
- 在 Golang 中,访问结构体成员需要使用点号操作符,点号操作符也被称为选择器(selector),使用时的格式为:
结构体.成员名
- 举例 访问和修改 ```go package main
import "log"
type Person struct { Name string Age int Labels map[string]string }
func main() { p1 := Person{ Name: "abc", Age: 18, Labels: nil, } // 访问属性 log.Printf("[p.name:%v][p.Age:%v]", p1.Name, p1.Age) // 修改属性 p1.Age += 1 log.Printf("[p.name:%v][p.Age:%v]", p1.Name, p1.Age)
}
# 匿名结构体的使用
## 匿名字段
- 顾名思义就是没有字段名的字段
```go
type test struct {
name string
age int
int //匿名字段
}
- 匿名字段和面向对象编程中的继承概念相似,可以被用来模拟类似继承的行为
匿名结构体
- 又称内嵌结构体
- 结构体可以包含一个或多个匿名(或者称为内嵌)字段,即这些字段没有显式的名字
- 举例student 和 person
package main
import (
"fmt"
"log"
)
type Person struct {
Name string
Age int
Labels map[string]string
}
type Student struct {
StudentId int
Person //匿名结构体
}
func main() {
p1 := Person{
Name: "xiaoyi",
Age: 18,
}
s1 := Student{
StudentId: 123,
Person: p1,
}
fmt.Println(s1)
// 访问属性
log.Printf("[p1.name:%v][p1.Age:%v]", p1.Name, p1.Age)
log.Printf("[s1.name:%v][s1.Age:%v]", s1.Name, s1.Age)
}
结构体命名嵌入
- 结构体命名嵌入,嵌入的结构体有字段名
- 使用字段名.属性名访问
结构体匿名嵌入,嵌入的结构体无字段名 : Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员
- 使用.属性直接访问
- 需要注意小写属性导出问题
举例如下 ```go package main
import ( "log" )
type Person struct { Name string Age int Labels map[string]string }
type Student struct { StudentId int Person //匿名结构体 } type Teacher struct { TeacherId int P Person //命名结构体 }
func main() { p1 := Person{ Name: "xiaoyi", Age: 18, } // 结构体匿名嵌入 s1 := Student{ StudentId: 123, Person: p1, } log.Printf("[匿名结构体 可以直接访问继承的属性名][s1.name:%v][s1.Age:%v]", s1.Name, s1.Age) log.Printf("[匿名结构体 可以加嵌入的结构体名称访问继承的属性名][s1.name:%v][s1.Age:%v]", s1.Person.Name, s1.Person.Age) t1 := Teacher{ TeacherId: 456, P: p1, } // 结构体的命名嵌入 log.Printf("[命名嵌入,访问继承的属性必须加上嵌入的字段名][t1.name:%v][t1.Age:%v]", t1.P.Name, t1.P.Age) }
- 匿名嵌入一般用来抽取公共字段,举个例子,公有云资源字段,公共字段放在嵌入的结构体中
```golang
type Common struct {
// common 13个
ID uint64 `json:"id" gorm:"column:id"`
Uid string `json:"uid" gorm:"column:uid"` //uuid
Hash string `json:"hash" gorm:"column:hash"` //所有key v的hash,判读其是否改变
Name string `json:"name" gorm:"column:name"` // 名称
CloudProvider string `json:"cloud_provider" gorm:"column:cloud_provider"` // 公有云厂商
ChargingMode string `json:"charging_mode" gorm:"column:charging_mode"` // 付费模式
Region string `json:"region" gorm:"column:region"` // region
AccountId uint64 `json:"account_id" gorm:"column:account_id"` // 账户
VpcId string `json:"vpc_id" gorm:"column:vpc_id"` // vpc id
SecurityGroups datatypes.JSON `json:"security_groups" gorm:"column:security_groups"` // 绑定的安全组
PrivateIp datatypes.JSON `json:"private_ip" gorm:"column:private_ip"` // 内网ips
Status string `json:"status" gorm:"column:status"` // 状态
Tags datatypes.JSON `json:"tags" gorm:"column:tags"` // 标签json
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // 创建时间
}
type Elb struct {
Common
ElbType string `json:"elb_type" gorm:"column:elb_type"` // 类型
IpAddress string `json:"ip_address" gorm:"column:ip_address"` // 规格
DnsName string `json:"dns_name" gorm:"column:dns_name"` // 域名
Backends datatypes.JSON `json:"backends" gorm:"column:backends"` // 后端地址
Port datatypes.JSON `json:"port" gorm:"column:port"` // 端口
}
type Rds struct {
Common
// 独有
Engine string `json:"engine" gorm:"column:engine"` // mysql or postgresql
EngineVersion string `json:"engine_version" gorm:"column:engine_version"` // 版本号
InstanceType string `json:"instance_type" gorm:"column:instance_type"` //规格
ArchitectureType string `json:"architecture_type" gorm:"column:architecture_type"` // ha or single
ClusterId uint64 `json:"cluster_id" gorm:"column:cluster_id"` // 规格
ClusterName string `json:"cluster_name" gorm:"column:cluster_name"` // 规格
MasterId string `json:"master_id" gorm:"column:master_id"` // 规格
PublicIp datatypes.JSON `json:"public_ip" gorm:"column:public_ip"` // 公网ips
ResourceId string `json:"resource_id" gorm:"column:resource_id"` // resource_id
IsWriter bool `json:"is_writer" gorm:"column:is_writer"` // 是否读写
Port uint64 `json:"port" gorm:"column:port"` // 端口
}
结构体指针类型嵌入
- 指针嵌入有什么不同:零值不同
- 空指针不能直接访问属性或操作 ```go package main
import "fmt"
type Person struct { Name string Age int Labels map[string]string }
type Student struct { StudentId int Person //匿名结构体 } type Teacher struct { TeacherId int P Person //命名结构体 } type Teacher1 struct { TeacherId int *Person //结构体匿名 指针嵌入 }
func main() {
t1 := Teacher1{}
fmt.Println(t1) // {0 <nil>}
fmt.Println(t1.Age)
/*
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x1c798a]
goroutine 1 [running]:
main.main()
*/
}
# 超纲内容
## 01 使用结构体实例
- 使用结构体,举例:配置文件不同段,global 段、mysql段、redis段
- 使用全局的配置变量可以拿到下属的所有段信息
## 02 字段标记信息反射时再讲
- 在定义结构体时还可以为字段指定一个标记信息,这些标记信息通过反射接口可见,并参与结构体的类型标识。
- 常用来做yaml解析、json解析、xorm/gorm连接mysql字段标记,举例如 公有云ecs结构体
# 结构体可见性
## 01 结构体本身的可访问性
- 实验环境 tt包中test1.go
```go
package tt
type Test struct {
X int
}
- main.go ```go package main
import ( "fmt" "lugo03/tt" )
var a = tt.Test{X: 5}
func main() { fmt.Println(a)
}
- 实验结果 X可以被访问到
- 修改 Test结构体 为test 则报错
### 结论 结构体名的大小写影响结构体本身的可访问性,首字母小写则包外不可见
## 02 结构体变量的成员变量不同包访问
- 实验修改上述 Test.X 为 Test.x,则x不能被访问到
### 结论 结构体字段名的大小写影响字段的可访问性,首字母小写则包外不可见
## 03 结构体变量的成员变量同包访问
- 实验修改main 中的 Test.X 为 Test.x,则 x能被访问到
- main同包文件
```go
package main
import (
"fmt"
"lugo03/tt"
)
// 外部包 结构体名和字段全大写
var a = tt.Test{X: 5}
// 同包 结构体名和字段可以小写
var b = Test2{x: 5}
var c = test3{x: 5}
func main() {
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
结论 同包内,结构体变量的成员变量可随时被访问
总结
- 要使某个符号对其他包( package)可见(即可以访问),需要将该符号定义为以大写字母开头
- go只限制包外的可访问性,而不限制同包内不同文件的可访问性
结构体浅拷贝和深拷贝
- 我们在开发中会经常的把一个变量复制给另一个变量,那么这个过程,可能是深浅拷贝
定义和区别
- 浅拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。释放内存地址时,同时释放内存地址。
- 深拷贝的是数据本身,创造一个样的新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。既然内存地址不同,释放内存地址时,可分别释放。
- 是否真正获取(复制)对象实体,而不是引用。
值类型的数据,默认都是深拷贝
- int , float,string,bool,array,struct
引用类型的数据,默认都是浅拷贝
- slice,map,function,chan
结构体深拷贝
- 结构体中都是基础字段,值类型字段 :=赋值就是深拷贝,举例 ```go package main
import "log"
type Person struct { Name string Age int }
func main() {
p1 := Person{
Name: "123",
Age: 123,
}
p2 := p1
p2.Age = 100
p1.Name = "456"
log.Printf("结构体中的字段都是值类型,那么就是深拷贝")
log.Printf("[p1的内存地址:%p ][value:%+v]", &p1, p1)
log.Printf("[p1的内存地址:%p ][value:%+v]", &p2, p2)
}
## 结构体基础字段(值类型)浅拷贝 :使用指针
- 使用指针浅拷贝 ,浅拷贝中,我们可以看到p1和p2的内存地址是相同的,修改其中一个对象的属性时,另一个也会产生变化。
```go
package main
import "log"
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{
Name: "123",
Age: 123,
}
p2 := &p1 // 等同于 var p2 *Person p2 = &p1
log.Printf("结构体中的字段都是值类型,使用&赋值给另外一个,就是浅拷贝")
p1.Age = 19
(*p2).Name = "898"
log.Printf("[p1的内存地址:%p ][value:%+v]", &p1, p1)
log.Printf("[p2的内存地址:%p ][value:%+v]", p2, p2)
/*
2021/07/25 14:13:14 结构体中的字段都是值类型,使用&赋值给另外一个,就是浅拷贝
2021/07/25 14:13:14 [p1的内存地址:0xc000004078 ][value:{Name:898 Age:19}]
2021/07/25 14:13:14 [p2的内存地址:0xc000004078 ][value:&{Name:898 Age:19}]
*/
}
结构体基础字段 浅拷贝:使用new函数
- new操作,p2 := p1,看上去是深拷贝,其实是浅拷贝,p2和p1两个指针共用同一个内存地址。 ```go package main
import "log"
type Person struct { Name string Age int }
func main() {
p1 := new(Person)
p1.Name = "小乙"
p1.Age = 123
p2 := p1
//log.Printf("结构体中的字段都是值类型,使用&赋值给另外一个,就是浅拷贝")
p1.Age = 19
p2.Name = "898"
log.Printf("[p1的内存地址:%p ][value:%+v]", p1, p1)
log.Printf("[p2的内存地址:%p ][value:%+v]", p2, p2)
}
## 结构体中含有引用类型的字段
- 如果使用 p2:=p1 则引用类型字段是浅拷贝,修改其中字段会互相影响
```go
package main
import "log"
type Person struct {
Name string
Age int
Tags map[string]string
HouseId1 [2]int //数组是值类型
HouseId2 []int // 切片是引用类型
}
func main() {
p1 := Person{
Name: "小乙",
Age: 123,
Tags: map[string]string{"k1": "v1", "k2": "v2"},
HouseId1: [2]int{100, 101},
HouseId2: []int{200, 201},
}
p2 := p1
// 修改两个值类型的字段
p1.Age = 19
p2.Name = "898"
// 修改map
p1.Tags["k1"] = "v11"
// 修改array
p2.HouseId1[0] = 300
// 修改切片
p1.HouseId2[1] = 301
log.Printf("[p1的内存地址:%p ][value:%+v]", &p1, p1)
log.Printf("[p2的内存地址:%p ][value:%+v]", &p2, p2)
}
结构体引用类型字段 如何深拷贝呢
- 方法一:挨个字段自行复制 ```go package main
import "log"
type Person struct { Name string Age int Tags map[string]string HouseId1 [2]int //数组是值类型 HouseId2 []int // 切片是引用类型 }
func main() { p1 := Person{ Name: "小乙", Age: 123, Tags: map[string]string{"k1": "v1", "k2": "v2"}, HouseId1: [2]int{100, 101}, HouseId2: []int{200, 201}, }
p2 := p1
// 针对其中引用类型的字段 ,重新赋值
// 对于map
m := make(map[string]string)
for k, v := range p1.Tags {
m[k] = v
}
p2.Tags = m
s1 := make([]int, 0)
for _, i := range p1.HouseId2 {
s1 = append(s1, i)
}
p2.HouseId2 = s1
// 修改两个值类型的字段
p1.Age = 19
p2.Name = "898"
// 修改map
p1.Tags["k1"] = "v11"
// 修改array
p2.HouseId1[0] = 300
// 修改切片
p1.HouseId2[1] = 301
log.Printf("[p1的内存地址:%p ][value:%+v]", &p1, p1)
log.Printf("[p2的内存地址:%p ][value:%+v]", &p2, p2)
}
2. 使用反射或json
```go
package main
import (
"encoding/json"
"log"
)
type Person struct {
Name string
Age int
Tags map[string]string
HouseId1 [2]int //数组是值类型
HouseId2 []int // 切片是引用类型
}
func main() {
p1 := Person{
Name: "小乙",
Age: 123,
Tags: map[string]string{"k1": "v1", "k2": "v2"},
HouseId1: [2]int{100, 101},
HouseId2: []int{200, 201},
}
var p2 Person
data, _ := json.Marshal(p1)
json.Unmarshal(data, &p2)
// 修改两个值类型的字段
p1.Age = 19
p2.Name = "898"
// 修改map
p1.Tags["k1"] = "v11"
// 修改array
p2.HouseId1[0] = 300
// 修改切片
p1.HouseId2[1] = 301
log.Printf("[p1的内存地址:%p ][value:%+v]", &p1, p1)
log.Printf("[p2的内存地址:%p ][value:%+v]", &p2, p2)
}