0
点赞
收藏
分享

微信扫一扫

go make 和 new && go的for range && defer && panic

  • ​make​​ 的作用是初始化内置的数据结构,也就是我们在前面提到的切片、哈希表和 Channel​​2​​
  • ​new​​ 的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针​​3​​

在代码中往往都会使用如下所示的语句初始化这三类基本类型,这三个语句分别返回了不同类型的数据结构:

slice := make([]int, 0, 100)
hash := make(map[int]bool, 10)
ch := make(chan int, 5)
  1. ​slice​​​ 是一个包含​​data​​​、​​cap​​​ 和​​len​​​ 的结构体​​​reflect.SliceHeader​​​;
  2. ​hash​​​ 是一个指向​​​runtime.hmap​​​ 结构体的指针;
  3. ​ch​​​ 是一个指向​​​runtime.hchan​​​ 结构体的指针;

相比与复杂的 ​​make​​​ 关键字,​​new​​ 的功能就简单多了,它只能接收类型作为参数然后返回一个指向该类型的指针:

i := new(int)
var v int
i := &v

go make 和 new && go的for range  && defer && panic_链表

 

分析使用 ​​for i, elem := range a {}​​ 遍历数组和切片,关心索引和数据的情况??

其编译器解析后代码如下:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
tmp := ha[hv1]
v1, v2 = hv1, tmp
...
}

 

  对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ​​ha​​,在赋值的过程中就发生了拷贝,

而我们又通过 ​​len​​ 关键字预先获取了切片的长度,所以在循环中追加新的元素也不会改变循环执行的次数;遇到这种同时遍历索引和元素的 range 循环时,Go 语言会额外创建一个新的 ​​v2​​ 变量存储切片中的元素,循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝。

package main
import "fmt"

type student struct {
Name string
Age int
}

func main() {
arr := []int{1, 2, 3}
newArr := []*int{}
for _, v := range arr {
fmt.Println("")
fmt.Printf("origin addr: %p value: %v", &v, v)
newArr = append(newArr, &v)
}
for _, s := range newArr {
fmt.Println("")
fmt.Printf("addr: %p value: %v", s, *s)
}
fmt.Printf("\n\n")
students := pase_student()
for k, v := range students {
fmt.Printf("key=%s,value=%v \n", k, v)
}
}

func pase_student() map[string]*student {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for _, stu := range stus {
m[stu.Name] = &stu
}
return m
}

结果为:

origin addr: 0xc000016060 value: 1
origin addr: 0xc000016060 value: 2
origin addr: 0xc000016060 value: 3
addr: 0xc000016060 value: 3
addr: 0xc000016060 value: 3
addr: 0xc000016060 value: 3

key=zhou,value=&{wang 22}
key=li,value=&{wang 22}
key=wang,value=&{wang 22}

  因为在循环中获取返回变量的地址都完全相同,所以会发生神奇的指针一节中的现象。---->循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖,赋值时也会触发拷贝

因此当我们想要访问数组中元素所在的地址时,不应该直接获取 range 返回的变量地址 ​​&v2​​,而应该使用 ​​&a[index]​​ 这种形式。

defer 

 使用 ​​defer​​ 的最常见场景是在函数调用结束后完成一些收尾工作,例如在 ​​defer​​ 中回滚数据库的事务, close 回收资源

 Go 语言中使用 ​​defer​​ 时会遇到两个常见问题

  • ​defer​​​ 关键字的调用时机以及多次调用​​defer​​ 时执行顺序是如何确定的;
  • ​defer​​ 关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果

作用域

向 ​​defer​​ 关键字传入的函数会在函数返回之前运行

func main() {
{
defer fmt.Println("defer runs")
fmt.Println("block ends")
}

fmt.Println("main ends")
}

$ go run main.go
block ends
main ends
defer runs

  从上述代码的输出我们会发现,​​defer​​ 传入的函数不是在退出代码块的作用域时执行的,它只会在当前函数和方法返回之前被调用。

func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))

time.Sleep(time.Second)
}

$ go run main.go
0s

  调用 ​​defer​​ 关键字会立刻拷贝函数中引用的外部参数,所以 ​​time.Since(startedAt)​​ 的结果不是在 ​​main​​ 函数退出之前计算的,而是在 ​​defer​​ 关键字调用时计算的,最终导致上述代码输出 0s

​defer​​ 关键字在 Go 语言源代码中对应的数据结构:

type _defer struct {
siz int32
started bool
openDefer bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}

 

​runtime._defer​​ 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 ​​link​​ 字段串联成链表。​​runtime._defer​​ 结构体中还包含一些垃圾回收机制使用的字段

  • ​siz​​ 是参数和结果的内存大小;
  • ​sp​​ 和 ​​pc​​ 分别代表栈指针和调用方的程序计数器;
  • ​fn​​ 是 ​​defer​​ 关键字中传入的函数;
  • ​_panic​​ 是触发延迟调用的结构体,可能为空;
  • ​openDefer​​ 表示当前 ​​defer​​ 是否经过开放编码的优化;

go make 和 new && go的for range  && defer && panic_链表_02

 list

  • 后调用的 ​​defer​​ 函数会先执行:
  • 后调用的 ​​defer​​ 函数会被追加到 Goroutine ​​_defer​​ 链表的最前面;
  • 运行 ​​runtime._defer​​ 时是从前到后依次执行;
  • 函数的参数会被预先计算;
  • 调用 ​​runtime.deferproc​​ 函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算;

panic 和 recover

  • ​panic​​ 能够改变程序的控制流,调用 ​​panic​​ 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 ​​defer​​;
  • ​recover​​ 可以中止 ​​panic​​ 造成的程序崩溃。它是一个只能在 ​​defer​​ 中发挥作用的函数,在其他作用域中调用不会发挥作用;
  • ​panic​​ 只会触发当前 Goroutine 的 ​​defer​​;
  • ​recover​​ 只有在 ​​defer​​ 中调用才会生效;
  • ​panic​​ 允许在 ​​defer​​ 中嵌套多次调用;

go make 和 new && go的for range  && defer && panic_数据结构_03

   ​​main​​ 函数中的 ​​defer​​ 语句并没有执行,执行的只有当前 Goroutine 中的 ​​defer​​。​​defer​​ 关键字对应的 ​​runtime.deferproc​​ 会将延迟调用函数与调用方所在 Goroutine 进行关联。所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数也是非常合理的。

go make 和 new && go的for range  && defer && panic_数据结构_04

 嵌套崩溃

Go 语言中的 ​​panic​​ 是可以多次嵌套调用的

go make 和 new && go的for range  && defer && panic_链表_05

 ​​panic​​​ 关键字在 Go 语言的源代码是由数据结构 ​​runtime._panic​​​ 表示的。每当我们调用 ​​panic​​ 都会创建一个如下所示的数据结构存储相关信息:

  1. ​argp​​​ 是指向​​defer​​ 调用时参数的指针;
  2. ​arg​​​ 是调用​​panic​​ 时传入的参数;
  3. ​link​​​ 指向了更早调用的​​​runtime._panic​​​ 结构;
  4. ​recovered​​​ 表示当前​​​runtime._panic​​​​ 是否被​​recover​​ 恢复;
  5. ​aborted​​​ 表示当前的​​panic​​ 是否被强行终止;

从数据结构中的 ​​link​​ 字段我们就可以推测出以下的结论:​​panic​​ 函数可以被连续多次调用,它们之间通过 ​​link​​ 可以组成链表。

​panic​​ 函数是终止程序的实现原理

编译器会将关键字 ​​panic​​ 转换成 ​​runtime.gopanic​​,该函数的执行过程包含以下几个步骤:

 

  1. 创建新的 ​​runtime._panic​​ 并添加到所在 Goroutine 的 ​​_panic​​ 链表的最前面;
  2. 在循环中不断从当前 Goroutine 的 ​​_defer​​ 中链表获取 ​​runtime._defer​​ 并调用 ​​runtime.reflectcall​​ 运行延迟调用函数;
  3. 调用 ​​runtime.fatalpanic​​ 中止整个程序;

关于recover  后续再看

LIST:

  1. 编译器会负责做转换关键字的工作;
  1. 将​​panic​​​ 和​​recover​​​ 分别转换成​​runtime.gopanic​​​ 和​​runtime.gorecover​​;
  2. 将​​defer​​​ 转换成​​runtime.deferproc​​ 函数;
  3. 在调用​​defer​​​ 的函数末尾调用​​runtime.deferreturn​​ 函数;
  1. 在运行过程中遇到​runtime.gopanic​​​ 方法时,会从 Goroutine 的链表依次取出​​runtime._defer​​ 结构体并执行;
  2. 如果调用延迟执行函数时遇到了 ​​runtime.gorecover​​ 就会将 ​​_panic.recovered​​ 标记成 true 并返回 ​​panic​​ 的参数;
  1. 在这次调用结束之后,​​runtime.gopanic​​​ 会从​​runtime._de​​​​f​​​​er​​​ 结构体中取出程序计数器​​pc​​​ 和栈指针​​sp​​​ 并调用​​runtime.recovery​​ 函数进行恢复程序;
  2. ​runtime.recovery​​​ 会根据传入的​​pc​​​ 和​​sp​​​ 跳转回​​runtime.deferproc​​;
  3. 编译器自动生成的代码会发现​​runtime.deferproc​​​ 的返回值不为 0,这时会跳回​​runtime.deferreturn​​ 并恢复到正常的执行流程;
  1. 如果没有遇到 ​​runtime.gorecover​​ 就会依次遍历所有的 ​​runtime._defer​​,并在最后调用 ​​runtime.fatalpanic​​ 中止程序、打印 ​​panic​​ 的参数并返回错误码 2;

 


 

http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!! 但行好事 莫问前程 --身高体重180的胖子

举报

相关推荐

0 条评论