本文最后修改时间:2019-01-14 20:53

Go-learning

3.语言详解--函数

3.1 定义

函数是结构化编程的最小模块单元。他将复杂的算法过程分解为若干个小任务,隐藏相关细节,使得程序结构更加清晰。函数被设计成相对独立,通过接受输入参数完成一段算法指令,输出货存储相关结果。关键字func 用于定义函数,go中的函数不太方便的限制,也借鉴了动态语言的某些优点。

  • 无须前置声明
  • 不支持命名嵌套定义(nested)
  • 不支持同名函数重载(overload)
  • 不支持默认参数
  • 支持不定长便参
  • 支持多返回值
  • 支持命名返回值
  • 支持匿名函数和闭包

不支持左花括号另起一行

  • 函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。 code:
func hello() {
   println("hello,sir!")
}

func exec(f func()){
   f()
}

func main(){
   f := hello
   exec(f)   
}
  • 第一类对象(first-class object)只可在运行期创建,可用做函数参数或者返回值,可存入变量的实体。最常见的用法就是匿名函数。

  • 从代码维护的角度来说,使用命名类型更加方便

code:

// 定义函数类型
type formatFunc(string, ...interface{}) (string, error)

//如不使用命名类型,这个参数签名会长的爆炸。
type format(f formatFunc, s string, a...interface{}) (string, error) {
   return f(s, a...)
}
  • 函数只能判断是否为nil 不支持其他比较操作。

code:

func a...
func b...
println(a == nil)
println(a == b)  // 无效操作
  • 从函数返回局部变量指针是安全的,编译器会通过逃逸分析(escape analysis)来决定是否在堆上分配内存。

code:

func test() *int {
  a := 0x100
  return &a
}

func main() {
   var a *int =test()
   println(a, &a)
}

输出:(省略)


$go build -gcflags "-l -m"   //禁用函数内联,输出优化信息
....
....

$go tool objdump -s "main\.main" test    //反汇编确认
......
......
  • 函数内联(inline)对内存分配有一定的影响。如果上列中允许内联,那么就会直接在栈上分配内存。
  • 当前编译器并未实现尾递归优化(tail-call optimization)尽管go执行栈的上限是gb规模,轻易不会出现堆栈溢出(Stack Overflow)醋五,但还是需要注意拷贝栈的赋值成本。

建议命名规则

这个就不用多说了

每个程序猿都有自己的规则。。

3.2 参数

go对参数的处理偏向保守,不支持有默认值的可选参数,不支持实参,调用时,必须按签名顺序传递指定类型和数量的实参,就算以 _ 命名的参数也不能忽略。

  • 在参数列表中,相邻的同类型参数可合并

code:

func test(x, y int, s string, _ bool) *int {
   return nil
}

func main() {
   test(1, 2, "abc")                // error : enough arguments in call to test
}
  • 参数可视作函数局部变量,因此不能在相同层次定义同名变量。

code:

func add(x, y int) int {
  x := 100                       //error : no new variables on the left side of :=
  var y int                       // error : y redeclared in this block
  return x + y
}
  • 形参是指函数定义中的参数,实参则是函数的调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量 变量,表达式 或者函数等。

  • 不管是指针,引用类型,还是其他的类型参数,都是值拷贝传递(pass-by-value)。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,回味形参和返回值分配内存空间,并将实参拷贝到形参内存。

code:

func test(x *int) {
   fmt.printf("pointer: %p, target: %v\n", &x, x)         //输出形参x的地址
}

func main() {
   a := 0x100
   p := &a
   fmt.printf("pointer: %p, target: %v\n", &p, p)          //输出实参p的地址

   test(p)
}

从输出结果可以看出,尽管实参和形参都指向同一目标,但传递指针时依然被复制。

  • 下面是一个指针参数导致实参变量被分配到堆上的简单实例,可对比传值参数的汇编代码,可看出具体的差别。

code:

func test(p *int) {
   go func(){
     println(p)                //延长p的生命周期
   }()
}

func main() {
   x := 100
   p := &x
   test(p)
}

输出:

$ go build -gcflags "-m"           //输出编译器优化策略

moved to heap :x
&x excapes to heap                  //逃逸


$ go tool objdump -s "main\.main" test

.........
CALL runtime.newobject(SB)                //在堆上为x分配内存
  • 要实现传出参数(out),通常建议使用返回值,也可以继续用二级指针。

code:

func test(p **int) {
   x := 100
   *p = &x

}

func main() {
   var p *int
   test(&p)
   println(*p)
}

输出:

100

  • 如果函数参数过多,建议将其重构成一个复合结构类型,也算是变相实现可选参数和命名实参的功能。

code:

type serverOption struct {
   address string
   port    int
   path    string
   timeout time.Duration
   log     *log.Logger
}

func newOption() *serverOption {
   return &serverOption{
     address:"0.0.0.0"
     port:  8080,
     path:  "/var/test",
     timeout: time.Second * 5,
     log:    nil,
   }
}

func server(option *serverOption) {}

func main() {
  opt := newOption()
  opt.port = 8085,                    //命名参数设置

  server(opt)
}
  • 将过多的参数独立成option struct,既便于拓展参数集,也方便通过newOption 函数设置默认配置。这也是代码复用的一种当时,避免处处调用时繁琐的参数配置。

变参

变参本质上就是切片slice。 只能有一个,且必须是最后一个,即列表尾部。

func test(s string, n ...int) string {
    var x int
    for _, i := range n {
    x += i
  }
 return fmt.Sprintf(s, x)
}

func main() {
  println(test("sum: %d", 1, 2, 3))
}

使用切片 slice 对象做变参时,必须展开。

func main() {
  s := []int{1, 2, 3}
  println(test("sum: %d", s...))
}

3.3 返回值

  • 有返回值的函数,必须有明确的return终止语句。
func test(x int) int {
  if x > 0 {
     return 1
    }else if x < 0 {
      return -1
    }
}         // error: missing return at the end of function
  • 除非有painc,或者无break的死循环,则无需return终止语句。
func test(x int) int {
   for {
     break
   }
}             //error : missing return at the end of function
  • 借鉴自动态语言的多分返回值模式,函数得以返回更多状态,尤其是error模式。
import "error"

func div(x, y int) (int, error) {
   if y == 0 {
     return 0, error.New("division by xx")
   }
   return x / y, nil
}
  • 稍有不便的是没有元祖(tuple)类型,也不能用数组,切片接受,但可用_忽略不想要的返回值。多返回值可用作其他函数调用实参,或当做结果直接返回。

  • 多返回值可直接作为其他函数调用实参。

func test() (int, int) {
  return 1, 2
}

func add(x, y int) int {
  return x + y
}

func sum(n ...int) int {
  var x int
  for _, i := range n {
  x += i
}
  return x
}

func main() {

  println(add(test()))
  println(sum(test()))
}
  • 命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
func add(x, y int) (z int) {
  z = x + y
  return
}

func main() {
  println(add(1, 2))
}
  • 命名返回参数可被同名局部变量遮蔽,此时需要显式返回。
func add(x, y int) (z int) {
    {                           // 不能在一个级别,引发 "z redeclared in this block" 错误。
    var z = x + y
    // return                  // Error: z is shadowed during return
    return z                  // 必须显式返回。
    }
}
  • 命名返回参数允许 defer 延迟调用通过闭包读取和修改。
func add(x, y int) (z int) {
    defer func() {
    z += 100
    }()
    z = x + y
    return
}

func main() {
    println(add(1, 2)) // 输出: 103
}
  • 显式 return 返回前,会先修改命名返回参数。
func add(x, y int) (z int) {
    defer func() {
    println(z) // 输出: 203
    }()
    z = x + y
    return z + 200 // 执⾏行顺序: (z = z + 200) -> (call defer) -> (ret)
}

func main() {
    println(add(1, 2)) // 输出: 203

命名返回值

对返回值命名和简短变量定义一样。

func paging(sql string, index nil) (count int, pages int, err error) {

}

3.4 匿名函数

  • 匿名函数是指没有定义名字符号的函数。除了没有名字外,匿名函数和普通函数完全相同。

  • 最大的区别是我们可在函数内部定义匿名函数,形成类似嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。

  • 匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。

// --- 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)())
  • 除了闭包因素外,匿名函数也是一种常见的重构手段,可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。
  • 相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序先知,必要时刻抽离,便于实现干净清晰的代码层次。
  • 闭包复制的是原对象指针,这就很容易解释延迟引用现象。
  • 闭包(closure)是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。
func test() func() {
    x := 100
    fmt.Printf("x (%p) = %d\n", &x, x)
    return func() {
    fmt.Printf("x (%p) = %d\n", &x, x)
    }
}

func main() {
    f := test()
    f()
}

输出:

x (0x2101ef018) = 100
x (0x2101ef018) = 100
  • 在汇编层面,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指 针。当调用匿名函数时,只需以某个寄存器传递该对象即可。
  • 通过输出指针,我们注意到闭包直接引用了原环境变量。分析汇编代码,返回值不仅是匿名函数还包括了引用的环境变量指针。
  • 汇编代码和我们主题无关,这里就不贴上去了。。。
FuncVal { func_address, closure_var_pointer ... }
  • 闭包通过指针引用缓解经变量,会导致生命周期延长,甚至会被分配到堆内存,所谓“延迟求值”的特征。
func test() []func() {
  var s []func()

  for i :=;i < 2; i++ {
    s = append(s, func() {
      println(&i, i)
    )}
  }
   return s
}

func main() {
  for _, f :=range test() {
    f()
  }
}
  • 解决办法就是每次用不同的环境变量或传参复制,让各自闭包环境各不相同。

  • 多个匿名函数引用统一环境变量,也会让事情变得更加复杂。任何的修改行为都会影响其他函数取值,在并发模式下可能需要做同步处理。

3.5 延迟调用

  • 关键字 defer 用于注册延迟调用。这些调用直到 return 前才被执行,通常用于释放资源或错误处理。
func test() error {
  f, err := os.Create("test.txt")
  if err != nil { return err }
  defer f.Close()                          // 注册调用,而不是注册函数。必须提供参数,哪怕为空。
  f.WriteString("Hello, World!")
  return nil
}
  • 多个 defer 注册,按 FILO 次序执⾏行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
func test(x int) {
  defer println("a")
  defer println("b")
  defer func() {
  println(100 / x)                 // div0 异常未被捕获,逐步往外传递,最终终止进程。
  }()
  defer println("c")
}

func main() {
  test(0)
}

输出:

c
b
a
panic: runtime error: integer divide by zero
  • 延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。
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)
}

输出:

x = 20 y = 120
defer: 10 120
  • 滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。
var lock sync.Mutex
  func test() {
  lock.Lock()
  lock.Unlock()
}

func testdefer() {
  lock.Lock()
  defer lock.Unlock()
}

func BenchmarkTest(b *testing.B) {
    for i := 0; i < b.N; i++ {
    test()
  }
}

func BenchmarkTestDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
    testdefer()
  }
}

输出:

BenchmarkTest? 50000000 43 ns/op
BenchmarkTestDefer 20000000 128 ns/op

3.6 错误处理

  • 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
func test() {
    defer func() {
    if err := recover(); err != nil {
    println(err.(string))                      // 将 interface{} 转型为具体类型。
  }
}()
  panic("panic error!")
}
  • 由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。
func panic(v interface{})
func recover() interface{}
  • 延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
func test() {
    defer func() {
    fmt.Println(recover())
}()
defer func() {
    panic("defer panic")
}()
    panic("test panic")
}

func main() {
    test()
}

输出:

defer panic
  • 捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
func test() {
    defer recover()                            // 无效!
    defer fmt.Println(recover())               // 无效!
    defer func() {
      func() {
      println("defer inner")
      recover()                                // 无效!
  }()
}()
  panic("test panic")
}

func main() {
  test()
}

输出:

defer inner
<nil>
panic: test panic
  • 使用延迟匿名函数或下面这样都是有效的。
func except() {
  recover()
}

func test() {
  defer except()
  panic("test panic")
}
  • 如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。
func test(x, y int) {
    var z int
    func() {
      defer func() {
        if recover() != nil { z = 0 }
        }()
  z = x / y
  return
}()
  println("x / y =", z)
}
  • 除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。
type error interface {
Error() string
}
  • 标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。
var ErrDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
  if y == 0 { return 0, ErrDivByZero }
  return x / y, nil
}

func main() {
  switch z, err := div(10, 0); err {
    case nil:
  println(z)
    case ErrDivByZero:
  panic(err)
  }
}
  • 如何区别使用 panic 和 error 两种方式?惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。