1. defer的使用

  defer 延迟调用。我们先来看一下,有defer关键字的代码执行顺序:

 func main() {
defer func() {
fmt.Println("1号输出")
}()
defer func() {
fmt.Println("2号输出")
}()
}

  输出结果:

 2号出来
1号出来

  结论:多个defer的执行顺序是倒序执行(同入栈先进后出)。

  由例子可以看出来,defer有延迟生效的作用,先使用defer的语句延迟到最后执行。

1.1 defer与返回值之间的顺序

 func defertest() int

 func main() {
fmt.Println("main:", defertest())
} func defertest() int {
var i int
defer func() {
i++
fmt.Println("defer2的值:", i)
}()
defer func() {
i++
fmt.Println("defer1的值:", i)
}()
return i
}

  输出结果:

 defer1的值:
defer2的值:
main:

  结论:return最先执行->return负责将结果写入返回值中->接着defer开始执行一些收尾工作->最后函数携带当前返回值退出

  return的时候已经先将返回值给定义下来了,就是0,由于i是在函数内部声明所以即使在defer中进行了++操作,也不会影响return的时候做的决定。

 func test() (i int)

 func main() {
fmt.Println("main:", test())
} func test() (i int) {
defer func() {
i++
fmt.Println("defer2的值:", i)
}()
defer func() {
i++
fmt.Println("defer1的值:", i)
}()
return i
}

  详解:由于返回值提前声明了,所以在return的时候决定的返回值还是0,但是后面两个defer执行后进行了两次++,将i的值变为2,待defer执行完后,函数将i值进行了返回。

2. defer定义和执行

 func test(i *int) int {
return *i
} func main(){
var i = // defer定义的时候test(&i)的值就已经定了,是1,后面就不会变了
defer fmt.Println("i1 =" , test(&i))
i++ // defer定义的时候test(&i)的值就已经定了,是2,后面就不会变了
defer fmt.Println("i2 =" , test(&i)) // defer定义的时候,i就已经确定了是一个指针类型,地址上的值变了,这里跟着变
defer func(i *int) {
fmt.Println("i3 =" , *i)
}(&i) // defer定义的时候i的值就已经定了,是2,后面就不会变了
defer func(i int) {
//defer 在定义的时候就定了
fmt.Println("i4 =" , i)
}(i) defer func() {
// 地址,所以后续跟着变
var c = &i
fmt.Println("i5 =" , *c)
}() // 执行了 i=11 后才调用,此时i值已是11
defer func() {
fmt.Println("i6 =" , i)
}() i =
}

  结论:会先将defer后函数的参数部分的值(或者地址)给先下来【你可以理解为()里头的会先确定】,后面函数执行完,才会执行defer后函数的{}中的逻辑。

例题分析

 //例子1
func f() (result int) {
defer func() {
result++
}()
return
}
//例子2
func f() (r int) {
t :=
defer func() {
t = t +
}()
return t
}
//例子3
func f() (r int) {
defer func(r int) {
r = r +
}(r)
return
}

  例1的正确答案不是0,例2的正确答案不是10,例3的正确答案不是6......

  这里先说一下返回值。defer是在return之前执行的。这条规则毋庸置疑,但最重要的一点是要明白,return xxx这一条语句并不是一条原子指令!

  函数返回的过程:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。defer表达式可能会在设置函数返回值之后,且在返回到调用函数之前去修改返回值,使最终的函数返回值与你想象的不一致。

  return xxx 可被改写成:

 返回值 = xxx
调用defer函数
空的return

  所以例子也可以改写成:

 //例1
func f() (result int) {
result = //return语句不是一条原子调用,return xxx其实是赋值+ret指令
func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
result++
}()
return
}
//例2
func f() (r int) {
t :=
r = t //赋值指令
func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t +
}
return //空的return指令
}
例3
func f() (r int) {
r = //给返回值赋值
func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值
r = r +
}(r)
return //空的return
}

  所以例1的结果是1,例2的结果是5,例3的结果是1.

3. defer内部原理

  从例子开始看:

 packmage main

 import()

 func main() {
defer println("这是一个测试")
}

  反编译一下看看:

 ➜  src $ go build -o test test.go
➜ src $ go tool objdump -s "main\.main" test
 TEXT main.main(SB) /Users/tushanshan/go/src/test3.go
test3.go: 0x104ea70 65488b0c2530000000 MOVQ GS:0x30, CX
test3.go: 0x104ea79 483b6110 CMPQ 0x10(CX), SP
test3.go: 0x104ea7d 765f JBE 0x104eade
test3.go: 0x104ea7f 4883ec28 SUBQ $0x28, SP
test3.go: 0x104ea83 48896c2420 MOVQ BP, 0x20(SP)
test3.go: 0x104ea88 488d6c2420 LEAQ 0x20(SP), BP
test3.go: 0x104ea8d c7042410000000 MOVL $0x10, (SP)
test3.go: 0x104ea94 488d05e5290200 LEAQ go.func.*+(SB), AX
test3.go: 0x104ea9b MOVQ AX, 0x8(SP)
test3.go: 0x104eaa0 488d05e6e50100 LEAQ go.string.*+(SB), AX
test3.go: 0x104eaa7 MOVQ AX, 0x10(SP)
test3.go: 0x104eaac 48c744241804000000 MOVQ $0x4, 0x18(SP)
test3.go: 0x104eab5 e8b631fdff CALL runtime.deferproc(SB)
test3.go: 0x104eaba 85c0 TESTL AX, AX
test3.go: 0x104eabc JNE 0x104eace
test3.go: 0x104eabe NOPL
test3.go: 0x104eabf e83c3afdff CALL runtime.deferreturn(SB)
test3.go: 0x104eac4 488b6c2420 MOVQ 0x20(SP), BP
test3.go: 0x104eac9 4883c428 ADDQ $0x28, SP
test3.go: 0x104eacd c3 RET
test3.go: 0x104eace NOPL
test3.go: 0x104eacf e82c3afdff CALL runtime.deferreturn(SB)
test3.go: 0x104ead4 488b6c2420 MOVQ 0x20(SP), BP
test3.go: 0x104ead9 4883c428 ADDQ $0x28, SP
test3.go: 0x104eadd c3 RET
test3.go: 0x104eade e8cd84ffff CALL runtime.morestack_noctxt(SB)
test3.go: 0x104eae3 eb8b JMP main.main(SB)
:- 0x104eae5 cc INT $0x3
:- 0x104eae6 cc INT $0x3
:- 0x104eae7 cc INT $0x3

  编译器将defer处理成两个函数调用 deferproc 定义一个延迟调用对象,然后在函数结束前通过 deferreturn 完成最终调用。在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。

内部结构

 //defer
type _defer struct {
siz int32 // 参数的大小
started bool // 是否执行过了
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // defer中的panic
link *_defer // defer链表,函数执行流程中的defer,会通过 link这个 属性进行串联
}
//panic
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
recovered bool // whether this panic is over
aborted bool // the panic was aborted
}
//g
type g struct {
_panic *_panic // panic组成的链表
_defer *_defer // defer组成的先进后出的链表,同栈
}

  因为 defer panic 都是绑定在运行的g上的,这里也说一下g中与 defer panic相关的属性

  再把defer, panic, recover放一起看一下:

 func main() {
defer func() {
recover()
}()
panic("error")
}

  反编译结果:

 go build -gcflags=all="-N -l" main.go
go tool objdump -s "main.main" main
 go tool objdump -s "main\.main" main | grep CALL
main.go: 0x4548d0 e81b00fdff CALL runtime.deferproc(SB)
main.go: 0x4548f2 e8b90cfdff CALL runtime.gopanic(SB)
main.go: 0x4548fa e88108fdff CALL runtime.deferreturn(SB)
main.go: 0x454909 e85282ffff CALL runtime.morestack_noctxt(SB)
main.go: 0x4549a6 e8d511fdff CALL runtime.gorecover(SB)
main.go: 0x4549b5 e8a681ffff CALL runtime.morestack_noctxt(SB)

  defer 关键字首先会调用 runtime.deferproc 定义一个延迟调用对象,然后再函数结束前,调用 runtime.deferreturn 来完成 defer 定义的函数的调用

  panic 函数就会调用 runtime.gopanic 来实现相关的逻辑

  recover 则调用 runtime.gorecover 来实现 recover 的功能

deferproc

  根据 defer 关键字后面定义的函数 fn 以及 参数的size,来创建一个延迟执行的 函数,并将这个延迟函数,挂在到当前g的 _defer 的链表上,下面是deferproc的实现:

 func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
// 获取一个_defer对象, 并放入g._defer链表的头部
d := newdefer(siz)
// 设置defer的fn pc sp等,后面调用
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case :
// Do nothing.
case sys.PtrSize:
// _defer 后面的内存 存储 argp的地址信息
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
// 如果不是指针类型的参数,把参数拷贝到 _defer 的后面的内存空间
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}

  通过newproc 获取一个 _defer 的对象,并加入到当前g的 _defer 链表的头部,然后再把参数或参数的指针拷贝到 获取到的 _defer对象的后面的内存空间。

  再看看newdefer 的实现:

 func newdefer(siz int32) *_defer {
var d *_defer
// 根据 size 通过deferclass判断应该分配的 sizeclass,就类似于 内存分配预先确定好几个sizeclass,然后根据size确定sizeclass,找对应的缓存的内存块
sc := deferclass(uintptr(siz))
gp := getg()
// 如果sizeclass在既定的sizeclass范围内,去g绑定的p上找
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == && sched.deferpool[sc] != nil {
// 当前sizeclass的缓存数量==0,且不为nil,从sched上获取一批缓存
systemstack(func() {
lock(&sched.deferlock)
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/ && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
unlock(&sched.deferlock)
})
}
// 如果从sched获取之后,sizeclass对应的缓存不为空,分配
if n := len(pp.deferpool[sc]); n > {
d = pp.deferpool[sc][n-]
pp.deferpool[sc][n-] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-]
}
}
// p和sched都没有找到 或者 没有对应的sizeclass,直接分配
if d == nil {
// Allocate new defer+args.
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
}
d.siz = siz
// 插入到g._defer的链表头
d.link = gp._defer
gp._defer = d
return d
}

  newdefer的作用是获取一个_defer对象, 并推入 g._defer链表的头部。根据size获取sizeclass,对sizeclass进行分类缓存,这是内存分配时的思想,先去p上分配,然后批量从全局 sched上获取到本地缓存,这种二级缓存的思想真的在go源码的各个部分都有。

deferreturn

 func deferreturn(arg0 uintptr) {
gp := getg()
// 获取g defer链表的第一个defer,也是最后一个声明的defer
d := gp._defer
// 没有defer,就不需要干什么事了
if d == nil {
return
}
sp := getcallersp()
// 如果defer的sp与callersp不匹配,说明defer不对应,有可能是调用了其他栈帧的延迟函数
if d.sp != sp {
return
}
// 根据d.siz,把原先存储的参数信息获取并存储到arg0里面
switch d.siz {
case :
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
// defer用过了就释放了,
gp._defer = d.link
freedefer(d)
// 跳转到执行defer
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

freedefer

  释放defer用到的函数,应该跟调度器、内存分配的思想是一样的。

 func freedefer(d *_defer) {
// 判断defer的sizeclass
sc := deferclass(uintptr(d.siz))
// 超出既定的sizeclass范围的话,就是直接分配的内存,那就不管了
if sc >= uintptr(len(p{}.deferpool)) {
return
}
pp := getg().m.p.ptr()
// p本地sizeclass对应的缓冲区满了,批量转移一半到全局sched
if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
// 使用g0来转移
systemstack(func() {
var first, last *_defer
for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/ {
n := len(pp.deferpool[sc])
d := pp.deferpool[sc][n-]
pp.deferpool[sc][n-] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-]
// 先将需要转移的那批defer对象串成一个链表
if first == nil {
first = d
} else {
last.link = d
}
last = d
}
lock(&sched.deferlock)
// 把这个链表放到sched.deferpool对应sizeclass的链表头
last.link = sched.deferpool[sc]
sched.deferpool[sc] = first
unlock(&sched.deferlock)
})
}
// 清空当前要释放的defer的属性
d.siz =
d.started = false
d.sp =
d.pc =
d.link = nil pp.deferpool[sc] = append(pp.deferpool[sc], d)
}

gopanic

 func gopanic(e interface{}) {
gp := getg() var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) atomic.Xadd(&runningPanicDefers, )
// 依次执行 g._defer链表的defer对象
for {
d := gp._defer
if d == nil {
break
} // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
// take defer off list. The earlier panic or Goexit will not continue running.
// 正常情况下,defer执行完成之后都会被移除,既然这个defer没有移除,原因只有两种: 1. 这个defer里面引发了panic 2. 这个defer里面引发了 runtime.Goexit,但是这个defer已经执行过了,需要移除,如果引发这个defer没有被移除是第一个原因,那么这个panic也需要移除,因为这个panic也执行过了,这里给panic增加标志位,以待后续移除
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
d.started = true // Record the panic that is running the defer.
// If there is a new panic during the deferred call, that panic
// will find d in the list and will mark d._panic (this panic) aborted.
// 把当前的panic 绑定到这个defer上面,defer里面有可能panic,这种情况下就会进入到 上面d.started 的逻辑里面,然后把当前的panic终止掉,因为已经执行过了
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
// 执行defer.fn
p.argp = unsafe.Pointer(getargp())
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
p.argp = nil // reflectcall did not panic. Remove d.
if gp._defer != d {
throw("bad defer entry in panic")
}
// 解决defer与panic的绑定关系,因为 defer函数已经执行完了,如果有panic或Goexit就不会执行到这里了
d._panic = nil
d.fn = nil
gp._defer = d.link // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
//GC() pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
freedefer(d)
// panic被recover了,就不需要继续panic了,继续执行剩余的代码
if p.recovered {
atomic.Xadd(&runningPanicDefers, -) gp._panic = p.link
// Aborted panics are marked but remain on the g.panic list.
// Remove them from the list.
// 从panic链表中移除aborted的panic,下面解释
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // must be done with signal
gp.sig =
}
// Pass information about recovering frame to recovery.
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 调用recovery, 恢复当前g的调度执行
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
// 打印panic信息
preprintpanics(gp._panic)
// panic
fatalpanic(gp._panic) // should not return
*(*int)(nil) = // not reached
84 }

  看下里面gp._panic.aborted 的作用:

 func main() {
defer func() { // defer1
recover()
}()
panic1()
} func panic1() {
defer func() { // defer2
panic("error1") // panic2
}()
panic("error") // panic1
}

  执行顺序详解:

  • 当执行到 panic("error") 时

  g._defer链表: g._defer->defer2->defer1

  g._panic链表:g._panic->panic1

  • 当执行到 panic("error1") 时

  g._defer链表: g._defer->defer2->defer1

  g._panic链表:g._panic->panic2->panic1

  • 继续执行到 defer1 函数内部,进行recover()
    此时会去恢复 panic2 引起的 panic, panic2.recovered = true,应该顺着g._panic链表继续处理下一个panic了,但是我们可以发现 panic1 已经执行过了,这也就是下面的代码的逻辑了,去掉已经执行过的panic
 for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}

panic的逻辑:

  程序在遇到panic的时候,就不再继续执行下去了,先把当前panic 挂载到 g._panic 链表上,开始遍历当前g的g._defer链表,然后执行_defer对象定义的函数等,如果 defer函数在调用过程中又发生了 panic,则又执行到了 gopanic函数,最后,循环打印所有panic的信息,并退出当前g。然而,如果调用defer的过程中,遇到了recover,则继续进行调度(mcall(recovery))。

recovery

 func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
// 记录defer返回的sp pc
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr =
gp.sched.ret =
// 重新恢复执行调度
gogo(&gp.sched)
}

gorecover

  gorecovery 仅仅只是设置了 g._panic.recovered 的标志位

 func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
// 需要根据 argp的地址,判断是否在defer函数中被调用
if p != nil && !p.recovered && argp == uintptr(p.argp) {
// 设置标志位,上面gopanic中会对这个标志位做判断
p.recovered = true
return p.arg
}
return nil
}

goexit

  当手动调用 runtime.Goexit() 退出的时候,defer函数也会执行:

 func Goexit() {
// Run all deferred functions for the current goroutine.
// This code is similar to gopanic, see that implementation
// for detailed comments.
gp := getg()
// 遍历defer链表
for {
d := gp._defer
if d == nil {
break
}
// 如果 defer已经执行过了,与defer绑定的panic 终止掉
if d.started {
if d._panic != nil {
d._panic.aborted = true
d._panic = nil
}
d.fn = nil
// 从defer链表中移除
gp._defer = d.link
// 释放defer
freedefer(d)
continue
}
// 调用defer内部函数
d.started = true
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
if gp._defer != d {
throw("bad defer entry in Goexit")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
// Note: we ignore recovers here because Goexit isn't a panic
}
// 调用goexit0,清除当前g的属性,重新进入调度
goexit1()
}

go中的关键字-defer的更多相关文章

  1. swift学习笔记 - swift中常用关键字

    swift中常用关键字 **用作声明的关键字: ** class.deinit.enum.extension.func.import.init.let.protocol.static.struct.s ...

  2. Java中的关键字 transient

    先解释下Java中的对象序列化 在讨论transient之前,有必要先搞清楚Java中序列化的含义: Java中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息 ...

  3. js中this关键字测试集锦

    参考:阮一峰<javascript的this用法>及<JS中this关键字详解> this是Javascript语言的一个关键字它代表函数运行时,自动生成的一个内部对象,只能在 ...

  4. 【转载】C/C++中extern关键字详解

    1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...

  5. 【转】java中volatile关键字的含义

    java中volatile关键字的含义   在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  6. 深入解析Javascript中this关键字的使用

    深入解析Javascript中面向对象编程中的this关键字 在Javascript中this关键字代表函数运行时,自动生成的一个内部对象,只能在函数内部使用.比如: function TestFun ...

  7. C/C++中extern关键字解析

    1 基本解释:extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 也就是说extern ...

  8. C++中typename关键字的用法

    我在我的 薛途的博客 上发表了新的文章,欢迎各位批评指正. C++中typename关键字的用法

  9. 【有人@我】Android中高亮变色显示文本中的关键字

    应该是好久没有写有关技术类的文章了,前天还有人在群里问我,说群主很长时间没有分享干货了,今天分享一篇Android中TextView在大段的文字内容中如何让关键字高亮变色的文章 ,希望对大家有所帮助, ...

随机推荐

  1. 面试官,不要再问我“Java 垃圾收集器”了

    如果Java虚拟机中标记清除算法.标记整理算法.复制算法.分代算法这些属于GC收集算法中的方法论,那么"GC收集器"则是这些方法论的具体实现. 在面试过程中这个深度的问题涉及的比较 ...

  2. 还在重复写空指针检查代码?考虑使用 Optional 吧!

    一.前言 如果要给 Java 所有异常弄个榜单,我会选择将 NullPointerException 放在榜首.这个异常潜伏在代码中,就像个遥控炸弹,不知道什么时候这个按钮会被突然按下(传入 null ...

  3. .Net Core 3.0 IdentityServer4 快速入门02

    .Net Core 3.0 IdentityServer4 快速入门 —— resource owner password credentials(密码模式) 一.前言 OAuth2.0默认有四种授权 ...

  4. 解决 IDEA 创建 Gradle 项目没有src目录

    第一次写博客,前几天遇到一个问题,就是使用ider创建gradle项目后,src目录没有自动生成出来,今天就给大家分享一下怎么解决. 目录: 1.创建Gradle项目 2.解决没有生成src目录问题 ...

  5. SpringBoot SpringCloud版本对应

  6. 设计模式(七)Builder模式

    Builder模式,从这个名字我们可以看出来,这种设计模式就是用于组装具有复杂结构的实例的. 下面还是以一个实例程序来解释这种设计模式,先看实例程序的类图. 这里为了调试方便,只实现其中一个功能Tex ...

  7. TICK技术栈(三)InfluxDB安装及使用

    1.什么是InfluxDB? InfluxDB是一个用Go语言开发的时序数据库,用于处理高写入和查询负载,专门为带时间戳的数据编写,对DevOps监控,IoT监控和实时分析等应用场景非常有用.通过自定 ...

  8. OPTIONS 请求引发的分析

    阅读提纲: 为什么会出现 OPTIONS 请求? 什么情况下会出现 OPTIONS 请求? OPTIONS 请求会发送什么内容? 跨域前端访问后端时,所有的 Ajax HTTP 请求都会先发送一个 O ...

  9. NOIP模拟 33

    苏轼三连一脸懵逼 然而既惨者就是没素质 T1是正解思路 然而因为直接从暴力修改过来并且忘了把求约数改成求质约数并且由于快速幂打的有缺陷等 没 有 A C ! 如 果 A C rank1就是俺的了! ( ...

  10. Apache配置反向代理、负载均衡和集群(mod_proxy方式)

    Apache配置负载均衡和集群使用mod_jk的方式比较多,但是mod_jk已经停止更新,并且配置相对复杂.Apache2.2以后,提供了一种原生的方式配置负载均衡和集群,比mod_jk简单很多. 1 ...