Golang中defer的坑

目录

#1 - defer nil函数

#2 - 在循环中使用defer

#3 - 延迟调用含有闭包的函数

#4 - 在执行快中使用defer

#5 - 延迟方法的坑

#6 - defer的执行顺序

#7 - 作用域屏蔽了参数

#8 - 参数很快得到了值

#9 - 循环中存址

#10 - 不返回的意义


#1 - defer nil函数

如果一个延迟函数被赋值为nil, 运行时的panic异常会发生在外围函数执行结束后而不是defer的函数被调用的时候。例:

func main() {
  var run func() = nil
  defer run()

  fmt.Println("runs")
}

输出结果

runs

❗️ panic: runtime error: invalid memory address or nil pointer dereference

发生了什么?

名为 func 的函数一直运行至结束,然后defer函数会被执行且会因为值为nil而产生panic异常。然而值得注意的是,run()的声明是没有问题,因为在外围函数运行完成后它才会被调用。

上面只是一个简单的案例,但同样的案例也可能发生在真实世界中,所以如果你遇上的话,可以想想是不是掉进了这个坑里。

#2 - 在循环中使用defer

切忌在循环中使用defer,除非你清楚自己在做什么,因为它们的执行结果常常会出人意料。

虽然,在某些情况下,在循环中使用defer会相当方便,例如将函数中的递归转交给defer,但这显然已经不是本文应该讲解的内容。

func main() {
    for {
        row, err := db.Query("SELECT ...")
        if err != nil {
            ...
        }
        defer row.Close()
    }
}

在上面的例子中,defer row.Close()在循环中的延迟函数会在函数结束过后运行,而不是每次 for 循环结束之后。这些延迟函数会不停地堆积到延迟调用栈中,最终可能会导致一些不可预知的问题。

解决方案1:不使用defer,直接在末尾调用。

func main() {
    for {
        row, err := db.Query("SELECT ...")
        if err != nil {
            ...
        }
        row.Close()
    }
}

解决方案2:将任务转交给另一个函数然后在里面使用defer,在下面这种情况下,延迟函数会在每次匿名函数执行结束后执行。

func main() {
    for {
        func() {
            row, err := db.Query("SELECT ...")
            if err != nil {
                ...
            }
            defer row.Close()
        }()
    }
}

#3 - 延迟调用含有闭包的函数

有时出于某种缘由,你想要让那些闭包延迟执行。例如,连接数据库,然后在查询语句执行过后中断与数据库的连接。例如:

type database struct{}

func (db *database) connect() (disconnect func()) {
    fmt.Println("connect")

    return func() {
        fmt.Println("disconnect")
    }
}

运行一下:

db := &database{}
defer db.connect()

fmt.Println("query db...")

输出结果:

query db...
connect

最终的情况是connect()执行结束后,其执行域得以被保存起来,但内部的闭包不会被执行。

解决方案1:

func() {
  db := &database{}
  close := db.connect()
  defer close()

  fmt.Println("query db...")
}

(糟糕的)解决方案2:

func() {
  db := &database{}
  defer db.connect()()

  ..
}

#4 - 在执行快中使用defer

你可能想要在执行块执行结束后执行在块内延迟调用的函数,但事实并非如此,它们只会在块所属的函数执行结束后才被执行,这种情况适用于所有的代码块除了上文的函数块例如,for,switch 等。

因为:延迟是相对于一个函数而非一个代码块

例如:

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

    fmt.Println("block: ends")
  }

  fmt.Println("main: ends")
}

输出结果:

block: ends
main: ends
block: defer runs

上例的延迟函数只会在函数执行结束后运行,而不是紧接着它所在的块(花括号内包含 defer 调用的区域)后执行,就像代码中的演示的那样,你可以使用花括号创造单独的执行块。

如果你希望在另一个块中使用defer,可以使用匿名函数(正如在第二个坑中我们采用的解决方案):

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

    fmt.Println("func: ends")
  }()

  fmt.Println("main: ends")
}

#5 - 延迟方法的坑

同样,你也可以使用defer来延迟方法调用,但也可能出一些岔子。例:

type Car struct {
  model string
}

func (c Car) PrintModel() {
  fmt.Println(c.model)
}

func main() {
  c := Car{model: "DeLorean DMC-12"}

  defer c.PrintModel()

  c.model = "Chevrolet Impala"
}

输出结果:

DeLorean DMC-12

如果使用指针对象作为接收者:

func (c *Car) PrintModel() {
  fmt.Println(c.model)
}

则输出结果为:

Chevrolet Impala

#6 - defer的执行顺序

例:

func main() {
  for i := 0; i < 4; i++ {
    defer fmt.Print(i)
  }
}

输出:

3
2
1
0

#7 - 作用域屏蔽了参数

事实上这是一个作用域的坑,但我想要让你知道它与 defer 和已命名的返回值之间的关系。例:

func release(r io.Closer)(err error) {
    defer func() {
        if err := r.Close(); err != nil {
            ...
        }
    }()
    ...
    return
}

接着创建一个 reader 类型的结构体使得调用 Close 的时候返回一个 error:

type reader struct{}

func (r reader) Close() error {
    return errors.New("Close Error")
}

当reader调用Close()的时候总会返回一个 error , release会在defer内部调用。

r := reader{}
err := release(r)
fmt.Print(err)

输出:

nil

延迟函数内的赋值语句在延迟函数的if块中,因此在块中的err变量赋值会创建一个全新的变量,块级变量err的作用域会屏蔽返回变量err,因此,release()还是返回err的原始值。

解决方案:

func release(r io.Closer) (err error) {
    defer func() {
        if err = r.close(); err != nil {
            ...
        }
    }()
    ...
}

#8 - 参数很快得到了值

当一个 defer 函数出现而不是被执行的时候,传递给它的参数的值就会被立刻确定下来,例如:

type message struct {
  content string
}

func (p *message) set(c string) {
  p.content = c
}

func (p *message) print() string {
  return p.content
}

试着运行一下上面的代码

func main() {
  m := &message{content: "Hello"}

  defer fmt.Print(m.print())

  m.set("World")
}

输出为"Hello";

在defer中,fmt.Print被推迟到函数返回后执行,可是m.print()的值在当时就已经被求出,因此,m.print()会返回 "Hello",这个值会保存一直到外围函数返回。

#9 - 循环中存址

当延迟函数执行的时候,会查看当时周围变量中的值 —— 除了被传入参数的值。例如:

for i := 0; i < 3; i++ {
  defer func() {
   fmt.Println(i)
  }()
}

输出:

3
3
3

当代码执行的时候,被延迟的函数会查看当时i的值,这是因为,当defer出现的时候,Go的运行时会保存i的地址,在for循环结束之后i的值变成了3。 因此,当延迟语句运行的时候,所有的延迟栈中的语句都会去查看i地址中的值,也就是3(被当做循环结束后的当时值)。

解决方案1:直接向延迟函数传参

for i := 0; i < 3; i++ {
  defer func(i int) {
   fmt.Println(i)
  }(i)
}

解决方案2:使用一个新的i块级变量来屏蔽循环中的i

for i := 0; i < 3; i++ {
  i := i
  defer func() {
    fmt.Println(i)
  }()
}

解决方案3:如果一次延迟调用一个函数,你可以直接对这个函数使用 defer 

for i := 0; i < 3; i++ {
  defer fmt.Println(i)
}

#10 - 不返回的意义

对于调用者来说,在延迟函数中返回值几乎没有什么影响,可是,你依然可以使用命名返回值来影响返回的结果。

 例如:

func release() error {
  defer func() error {
    return errors.New("error")
  }()

  return nil
}
//返回nil

func release() (err error) {
  defer func() {
    err = errors.New("error")
  }()

  return nil
}
//返回"error"

 

 

 

 

 

 

你可能感兴趣的:(Golang)