Go语言函数深入解析

目录函数定义参数返回值匿名函数函数可变数量参数传参闭包递归延迟调用defer异常处理单元测试压力测试函数定义golang函数特点:无需声明原型。支持不定变参。支持多返回值。支持命名返回参数。支持匿名函数和闭包。函数也是一种类型,一

目录


函数定义

golang函数特点:

  • 无需声明原型。

  • 支持不定 变参。

  • 支持多返回值。

  • 支持命名返回参数。

  • 支持匿名函数和闭包。

  • 函数也是一种类型,一个函数可以赋值给变量。

  • 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。

  • 不支持 重载 (overload)

  • 不支持 默认参数 (default parameter)。

    package main
    import "fmt"
    func test(fn func() int) int {
    return fn()
    }
    // 定义函数类型。
    type FormatFunc func(s string, x, y int) string 
    func format(fn FormatFunc, s string, x, y int) string {
    return fn(s, x, y)
    }
    func main() {
    s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
    
    s2 := format(func(s string, x, y int) string {
        return fmt.Sprintf(s, x, y)
    }, "%d, %d", 10, 20)
    
    println(s1, s2)
    }

参数

函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

  • 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  • 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

    
    package main
    import (
    "fmt"
    )
    /* 定义相互交换值的函数 */
    func swap(x, y *int) {
    var temp int
    
    temp = *x /* 保存 x 的值 */
    *x = *y   /* 将 y 值赋给 x */
    *y = temp /* 将 temp 值赋给 y*/

} func main() { var a, b int = 1, 2 / 调用 swap() 函数 &a 指向 a 指针,a 变量的地址 &b 指向 b 指针,b 变量的地址 / swap(&a, &b) fmt.Println(a, b) }

### 任意类型的不定参数:
```go
package main
import (
    "fmt"
)
func test(s string, n ...int) string {
  var x int
  for _, i := range n {
      x += i
  }
  return fmt.Sprintf(s, x)
}
func main() {
  s := []int{1, 2, 3}
  res := test("sum: %d", s...)    // slice... 展开slice
  println(res)
}

返回值

没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。

package main
import (
  "fmt"
)
func add(a, b int) (c int) {
  c = a + b
  return
}
func calc(a, b int) (sum int, avg int) {
  sum = a + b
  avg = (a + b) / 2
  return
}
func test() (int, int) {
  return 1, 2
}
func sum(n ...int) int {
  var x int
  for _, i := range n {
      x += i
  }
  return x
}
func addp(x, y int) (z int) {
  defer func() {
      println(z) // 输出: 203   defer 延迟调用通过闭包读取和修改
  }()

  z = x + y
  return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}
func main() {
  var a, b int = 1, 2
  c := add(a, b)
  sum, avg := calc(a, b)
  fmt.Println(a, b, c, sum, avg)
  x, _ := test()
  println(x)
  println(add(1, 2))    // 命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
  println(sum(test()))  // 多返回值可直接作为其他函数调用实参。
  println(add(1, 2))   // 显式 return 返回前,会先修改命名返回参数。
}

匿名函数

匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

package main

func main() {
  // 匿名函数申明
  getSqrt := func(a float64) float64 {
      return math.Sqrt(a)
  }
  fmt.Println(getSqrt(4))

  // --- function variable ---
  fn := func() { println("Hello, World!") }
  fn()

  // --- function collection ---
  fns := [](func(x int) int){
      func(x int) int { return x + 1 },
      func(x int) int { return x + 2 },
  }
  println(fns[0](100))

  // --- function as field ---
  d := struct {
      fn func() string
  }{
      fn: func() string { return "Hello, World!" },
  }
  println(d.fn())

  // --- channel of function ---
  fc := make(chan func() string, 2)
  fc <- func() string { return "Hello, World!" }
  println((<-fc)())
}

闭包递归

Go 闭包

package main

import "fmt"

// 外部引用函数参数局部变量
func add(base int) func(int) int {
    return func(i int) int {
        base += i
        return base
    }
}

// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {
    // 定义2个函数,并返回
    // 相加
    add := func(i int) int {
        base += i
        return base
    }
    // 相减
    sub := func(i int) int {
        base -= i
        return base
    }
    // 返回
    return add, sub
}

func main() {
    tmp1 := add(10)
    fmt.Println(tmp1(1), tmp1(2))
    // 此时tmp1和tmp2不是一个实体了
    tmp2 := add(100)
    fmt.Println(tmp2(1), tmp2(2))

    f1, f2 := test01(10)
    // base一直是没有消
    fmt.Println(f1(1), f2(2))
    // 此时base是9
    fmt.Println(f1(3), f2(4))
}

Go 递归

// 数字阶乘
package main
import "fmt"
func factorial(i int) int {
  if i <= 1 {
      return 1
  }
  return i * factorial(i-1)
}
func main() {
    var i int = 7
    fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}

// 斐波那契数列(Fibonacci)
package main
import "fmt"
func fibonaci(i int) int {
  if i == 0 {
      return 0
  }
  if i == 1 {
      return 1
  }
  return fibonaci(i-1) + fibonaci(i-2)
}
func main() {
  var i int
  for i = 0; i < 10; i++ {
      fmt.Printf("%d\n", fibonaci(i))
  }
}

延迟调用defer

go语言 defer

go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。

defer 是先进后出

这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。

defer特性

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

defer用途

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放
    package main
    import "fmt"
    func main() {
    var whatever [5]struct{}
    for i := range whatever {
    defer fmt.Println(i)
    }
    } 

    defer 碰上闭包

    
    package main
    import "fmt"
    func main() {
    var whatever [5]struct{}
    for i := range whatever {
    defer func() { fmt.Println(i) }()
    }
    }

package main import "fmt" type Test struct { name string } func (t *Test) Close() { fmt.Println(t.name, " closed") } func Close(t Test) { t.Close() } func main() { ts := []Test{{"a"}, {"b"}, {"c"}} for _, t := range ts { defer Close(t) } }


defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的this指针如何处理,通过这个例子可以看出go语言并没有把这个明确写出来的this指针当作参数来看待。

多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
```go
package main
func test(x int) {
  defer println("a")
  defer println("b")
  defer func() {
      println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
  }()
  defer println("c")
}

func main() {
  test(0)
} 

* 延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。

package main

func test() {
  x, y := 10, 20

  defer func(i int) {
      println("defer:", i, y) // y 闭包引用
  }(x) // x 被复制

  x += 10
  y += 100
  println("x =", x, "y =", y)
}

func main() {
  test()
}  

defer陷阱

defer 与 closure

import (
  "errors"
  "fmt"
)

func foo(a, b int) (i int, err error) {
  defer fmt.Printf("first defer err %v\n", err)
  defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
  defer func() { fmt.Printf("third defer err %v\n", err) }()
  if b == 0 {
    err = errors.New("divided by zero!")
    return
  }

  i = a / b
  return
}

func main() {
  foo(2, 0)
}  

defer 与 return

package main
import "fmt"
func foo() (i int) {
  i = 0
  defer func() {
      fmt.Println(i)
  }()
  return 2
}

func main() {
  foo()
}

defer nil 函数

package main
import (
  "fmt"
)

func test() {
  var run func() = nil
  defer run()
  fmt.Println("runs")
}

func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println(err)
    }
  }()
  test()

http.Get 成功执行时才使用 defer

package main
import "net/http"
func do() error {
  res, err := http.Get("http://xxxxxxxxxx")
  if res != nil {
      defer res.Body.Close()
  }

  if err != nil {
      return err
  }
  // ..code...
  return nil
}

func main() {
  do()
} 

异常处理

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。

异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

panic:

  1. 内置函数
  2. 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
  3. 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
  4. 直到goroutine整个退出,并报告错误

recover:

  1. 内置函数
  2. 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
  3. 一般的调用建议
    • 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
    • 可以获取通过panic传递的error

注意:

  1. 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
  2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  3. 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package main
func main() {
  test()
}
// panic
func test() {
  defer func() {
    if err := recover(); err != nil {
      println(err.(string)) // 将 interface{} 转型为具体类型。
    }
  }()
  panic("panic error!")
} 

package main
import "fmt"
func test() {
  defer func() {
      fmt.Println(recover())
  }()
  defer func() {
      panic("defer panic")
  }()
  panic("test panic")
}
func main() {
    test()
} 

// recover
package main

import (
    "fmt"
)

func except() {
    fmt.Println(recover())
}

func test() {
    defer except()
    panic("test panic")
}

func main() {
    test()
}

单元测试

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

// split/split.go
package split
import "strings"
// split package with a single split function.

// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
    i := strings.Index(s, sep)

    for i > -1 {
        result = append(result, s[:i])
        s = s[i+1:]
        i = strings.Index(s, sep)
    }
    result = append(result, s)
    return
}

// split/split_test.go
package split
import (
    "reflect"
    "testing"
)

func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
    got := Split("a:b:c", ":")         // 程序输出的结果
    want := []string{"a", "b", "c"}    // 期望的结果
    if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
        t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示
    }
}

压力测试

Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,testing框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。

另外建议安装gotests插件自动生成测试代码:

go get -u -v github.com/cweill/gotests/...

测试用例

package gotest
import (
    "testing"
)

func Test_Division_1(t *testing.T) {
  if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
    t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错
  } else {
    t.Log("第一个测试通过了") //记录一些你期望记录的信息
  }
}

func Test_Division_2(t *testing.T) {
  t.Error("就是不通过")
}  

编写压力测试

import (
    "testing"
)
func Benchmark_Division(b *testing.B) {
  for i := 0; i < b.N; i++ { //use b.N for looping 
    Division(4, 5)
  }
}
func Benchmark_TimeConsumingFunction(b *testing.B) {
  b.StopTimer() //调用该函数停止压力测试的时间计数

  //做一些初始化的工作,例如读取文件数据,数据库连接之类的,
  //这样这些时间不影响我们测试函数本身的性能

  b.StartTimer() //重新开始时间
  for i := 0; i < b.N; i++ {
      Division(4, 5)
  }
}  
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!