-
make
的作用是初始化内置的数据结构,也就是我们在前面提到的切片、哈希表和 Channel2; -
new
的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针3;
在代码中往往都会使用如下所示的语句初始化这三类基本类型,这三个语句分别返回了不同类型的数据结构:
slice := make([]int, 0, 100)
hash := make(map[int]bool, 10)
ch := make(chan int, 5)
-
slice
是一个包含data
、cap
和len
的结构体reflect.SliceHeader
; -
hash
是一个指向runtime.hmap
结构体的指针; -
ch
是一个指向runtime.hchan
结构体的指针;
相比与复杂的 make
关键字,new
的功能就简单多了,它只能接收类型作为参数然后返回一个指向该类型的指针:
i := new(int)
var v int
i := &v
分析使用 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
是否经过开放编码的优化;
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
中嵌套多次调用;
main
函数中的 defer
语句并没有执行,执行的只有当前 Goroutine 中的 defer
。defer
关键字对应的 runtime.deferproc
会将延迟调用函数与调用方所在 Goroutine 进行关联。所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数也是非常合理的。
嵌套崩溃
Go 语言中的 panic
是可以多次嵌套调用的
panic
关键字在 Go 语言的源代码是由数据结构 runtime._panic
表示的。每当我们调用 panic
都会创建一个如下所示的数据结构存储相关信息:
-
argp
是指向defer
调用时参数的指针; -
arg
是调用panic
时传入的参数; -
link
指向了更早调用的runtime._panic
结构; -
recovered
表示当前runtime._panic
是否被recover
恢复; -
aborted
表示当前的panic
是否被强行终止;
从数据结构中的 link
字段我们就可以推测出以下的结论:panic
函数可以被连续多次调用,它们之间通过 link
可以组成链表。
panic
函数是终止程序的实现原理
编译器会将关键字 panic
转换成 runtime.gopanic
,该函数的执行过程包含以下几个步骤:
- 创建新的
runtime._panic
并添加到所在 Goroutine 的 _panic
链表的最前面; - 在循环中不断从当前 Goroutine 的
_defer
中链表获取 runtime._defer
并调用 runtime.reflectcall
运行延迟调用函数; - 调用
runtime.fatalpanic
中止整个程序;
关于recover 后续再看
LIST:
- 编译器会负责做转换关键字的工作;
- 将
panic
和recover
分别转换成runtime.gopanic
和runtime.gorecover
; - 将
defer
转换成runtime.deferproc
函数; - 在调用
defer
的函数末尾调用runtime.deferreturn
函数;
- 在运行过程中遇到
runtime.gopanic
方法时,会从 Goroutine 的链表依次取出runtime._defer
结构体并执行; - 如果调用延迟执行函数时遇到了
runtime.gorecover
就会将 _panic.recovered
标记成 true 并返回 panic
的参数;
- 在这次调用结束之后,
runtime.gopanic
会从runtime._de
f
er
结构体中取出程序计数器pc
和栈指针sp
并调用runtime.recovery
函数进行恢复程序; -
runtime.recovery
会根据传入的pc
和sp
跳转回runtime.deferproc
; - 编译器自动生成的代码会发现
runtime.deferproc
的返回值不为 0,这时会跳回runtime.deferreturn
并恢复到正常的执行流程;
- 如果没有遇到
runtime.gorecover
就会依次遍历所有的 runtime._defer
,并在最后调用 runtime.fatalpanic
中止程序、打印 panic
的参数并返回错误码 2;
http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!!
但行好事 莫问前程
--身高体重180的胖子