美文网首页
第四章 函数

第四章 函数

作者: 牧码人爱跑马 | 来源:发表于2018-07-14 18:52 被阅读0次

一、函数

1.1 什么是函数

函数是执行特定任务的代码块。
关键字func用于定义函数。go中函数特点:

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

1.2 参数传递

go语言函数的参数也是存在值传递引用传递

函数运用场景

值传递

package main

import (
   "fmt"
   "math"
)

func main(){
   /* 声明函数变量 */
   getSquareRoot := func(x float64) float64 {
      return math.Sqrt(x)
   }

   /* 使用函数 */
   fmt.Println(getSquareRoot(9))

}

形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数等。


引用传递
引用传递其实是一个伪命题。因为严格来说go中并没有引用传递,这么说只是为了便于理解。
因为不管是指针、引用类型,还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。
请看下面的例子

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)
}

输出:

pointer: 0xc042004028, target: 0xc042008098
pointer: 0xc042004038, target: 0xc042008098

从结果可以看出,虽然形参和实参都指向同一目标(值相同,指向同一内存地址),但指针本身的地址&p不同,说明传递指针时依然是被复制,即所谓的没有引用传递。

//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { 
*a = *a+1 // 修改了a的值
return *a // 返回新值
} 
func main() {
x := 3
fmt.Println("x = ", x)   // 输出 "x = 3"
x1 := add1(&x) // 调用 add1(&x) 传x的地址
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
fmt.Println("x = ", x) // 应该输出 "x = 4"
}
  • 传指针使得多个函数能操作同一个对象。
  • 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
  • Go语言中string, slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)
    上句源于《Go Web编程》一书,但严格来说string其实在底层实现上是引用类型,但是因为string不允许修改,只能生成新的对象,在逻辑上和值类型无差别。

从表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,还可能会导致它被分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。

其实在栈上复制小对象只须很少的指令即可完成,远比运行时进行堆内存分配要快得多。另外,并发编程也是提倡尽可能使用不可变对象(只读或复制),这可消除数据同步等麻烦。当然,如果复制成本很高,或需要修改原对象状态,自然使用指针更好。
下面是一个指针参数导致实参变量被分配到堆上的简单示例。可对比传值参数的汇编代码,从中可看出具体的差别。

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

func main() {
    x:=100
    p:=&x
    test(p)
}
go tool objdump -s "main\.main" 堆分配.go  //输出编译器优化策略
.\堆分配.go:11:5: &x escapes to heap  //逃逸
.\堆分配.go:10:5: moved to heap: x
go tool objdump -s "main\.main" 4.函数.exe
...
 ... CALL runtime.newobject(SB)  //在堆上为x分配内存
... CALL main.test(SB)

要实现传出参数(out),通常建议使用返回值。当然,也可以继续用二级指针。

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

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

输出:100
注意这个例子的巧妙之处,test2实际上是把&p=&x了,这样没有返回值也实现了返回的效果。

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

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=9000           //命名参数设置
    server(opt)
}

1.3 可变参

Go对参数的处理偏向保守,不支持默认值的可选参数,不支持命名实参。
但支持变参,变参本质上就是一个切片,只能接受一到多个同类型参数,且必须放在列表末尾。

func testargs( s string, a ...int){
    fmt.Printf("%T,%v",a,a)
}
func main() {
    testargs("abc",1,2,3,4)
}

输出:

[]int,[1 2 3 4]

将切片作为变参时,必须对其进行展开操作。如果是数组,先将其转换为切片。

func testargs( s string, a ...int){
    fmt.Printf("%T,%v",a,a)
}

func main() {
    a:=[3]int{10,20,30}
    testargs("abc",a[:]...)  //转换为slice后展开
}

既然变参时切片,那么参数赋值的仅是切片自身,并不包括底层数组,也因此可修改原数据。如果需要,可用内置函数copy复制底层数据。

func testargs( s string, a ...int){
    for i:=range a{
        a[i]+=100
    }
}

func main() {
    a:=[3]int{10,20,30}
    testargs("abc",a[:]...)  //转换为slice后展开
    fmt.Println(a)
}

输出:[110 120 130]

1.4 返回值

一个函数可以没有返回值,也可以有一个返回值,也可以有返回多个值。

func swap(x, y string) (string, string) {
   return y, x
}

func main() {
   a, b := swap("Mahesh", "Kumar")
   fmt.Println(a, b)
}

稍有不便的是没有元组(tuple)类型,也不能用数组、切片接收返回值,但可用_忽略掉不想要的返回值。多返回值可用作其他函数调用实参,或当作结果直接返回。

func div(x,y int)(int,error){
    if y==0{
        return 0,errors.New("division by zero")
    }
    return x /y, nil
}

func log(x int, err error){
    fmt.Println(x,err)
}

func testdiv()(int,error){
    return div(5,0)
}

func main(){
    log(testdiv())
}
func eval(a,b int,op string) (int,error){
    switch op {
    case "+":
        return a+b,nil
    case "-":
        return a-b,nil
    case "*":
        return a*b,nil
    case "/":
        return a/b,nil
    default:
        return 0,fmt.Errorf("unsupported op:%s",op)

    }
}

命名返回值

对返回值命名和简短变量定义一样,优缺点共存。

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

从上面这个简单示例可以看出,命名返回值让函数声明更加清晰,同时也会改善帮助文档和代码编辑器提示。

命名返回值和参数一样,可当做函数局部变量使用,最后由return隐式返回值。

func div1(x, y int)(z int, err error){
    if y==0{
        err = errors.New("div by zero")
        return    //隐式返回z=0, err="div by zero"
    }
    z = x/y
    return  //隐式返回z , nil
}

这些特殊的“局部变量”会被不同层级的同名变量遮蔽。好在编译器能检查到此类状况,只要改为显式return即可。
如果返回值类型能明确表明其含义,就尽量不要对其命名。

func NewUser()(*User,error)

1.5 空白标识符

_是Go中的空白标识符。它可以代替任何类型的任何值。让我们看看这个空白标识符的用法。

比如rectProps函数返回的结果是面积和周长,如果我们只要面积,不要周长,就可以使用空白标识符。

package main

import (  
    "fmt"
)

func rectProps(length, width float64) (float64, float64) {  
    var area = length * width
    var perimeter = (length + width) * 2
    return area, perimeter
}
func main() {  
    area, _ := rectProps(10.8, 5.6) // perimeter is discarded
    fmt.Printf("Area %f ", area)
}

1.6 闭包

再提到闭包前,必须要先介绍下匿名函数
匿名函数是指没有定义名字符号的函数,是一个"内联"语句或表达式。
除了没有名字外,匿名函数和普通函数最大区别是,我们可在函数内部定义匿名函数,形成类似嵌套结果。匿名函数可直接调用,保存到变量,作为参数或返回值。

匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

匿名函数

赋值给变量

func main(){
    add:=func(x,y int) int{
        return x+y
    }
    println(add(1,2))
}

作为参数:

func ttest( f func()){
    f()
}
func main(){
    ttest(func() {
        println("hello world")
    })
}

作为返回值

func ttest()func(m,n int)int{
    return func(x,y int)int{
        return x+y
    }
}

func main(){
    add:=ttest()
    println(add(1,2))
}

将匿名函数赋值给变量,与为普通函数提供名字标记符有着根本的区别。当然,编译器会为匿名函数生成一个“随机”符号名。

普通函数和匿名函数都可作为结构体字段,或经通道传递。

func testStruct(){
    type calc struct {
        mul func(x,y int) int
    }
    x:=calc{
        mul:func(x, y int) int {
            return x * y
        },
    }
    println(x.mul(2,3))
}

func testChannel(){
    c:=make(chan func(int,int)int ,2)  // 2是双向通道?
    c<-func(x,y int)int{
        return x+y
    }
    println((<-c)(7,2))   //注意下这个,形式很特别
}

func main(){
    testStruct()
    testChannel()
}

除闭包因素外,匿名函数也是一种常见重构手段。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,以实现框架和细节分离。

相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净、清晰的代码层次。

闭包

闭包是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用的环境的组合体。

func testClosure(x int)func(){
    return func() {
        println(x)
    }
}

func main() {
    f:=testClosure(111)
    f()
}

就这段代码而言,test返回的匿名函数会引用上下文环境变量x。 当该函数在main中执行时,它依然可正确读取x的值,这种现象称作闭包。
闭包是如何实现的?匿名函数被返回后,为何还能读取环境变量的值?修改一下代码再看。

func testClosure(x int)func(){
    println(&x)
    return func() {
        println(&x,x)
    }
}

func main() {
    f:=testClosure(0x100)
    f()
}

输出:

0xc04203e000
0xc04203e000 256

通过输出指针,我们注意到闭包直接饮用额原环境变量。分析汇编代码,你会看到返回的不仅仅是匿名函数,还包括所引用的环境变量指针。所以说,闭包是函数和引用环境的组合体更加确切。

本质上返回的是一个funcval结构,可在runtime/runtime2.go中找到相关定义。
go build -gcflags "-N -L"

正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓“延迟求值”的特性。

func testCC()[]func(){
    var s []func()
    for i:=0;i<3;i++{
        s=append(s, func() {
            fmt.Println(&i,i)
        })
    }

    return s
}

func main(){
    for _,f:=range(testCC()){
        f()
    }
}

输出:

0xc042008098 3
0xc042008098 3
0xc042008098 3

对这个结果不必惊讶。很简单,for循环复用局部变量i,那么每次添加的匿名函数引用的自然是同一变量。添加操作仅仅是将匿名函数放入列表,并未执行。因此,当main执行这些函数时,他们读取的是环境变量i最后一次循环时的值。

解决方法就是每次用不同的环境变量或传参赋值,让各自闭包环境各不相同。

func testCC()[]func(){
    var s []func()
    for i:=0;i<3;i++{
        x:=i               //每次循环都重新定义
        s=append(s, func() {
            fmt.Println(&x,x)
        })
    }

    return s
}

func main(){
    for _,f:=range(testCC()){
        f()
    }
}

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

func testcs(x int)(func(),func()){

    return func() {
        println(x)
        x+=10
    }, func() {
        println(x)
    }
}
func main(){
    a,b:=testcs(100)
    a()
    b()

}

闭包让我们不用传递参数就可读取或修改环境状态,当然也要为此付出额外代价。对于性能要求较高的场合,须慎重使用。

func getSequence() func() int {
   i:=0
   return func() int {
      i+=1
     return i  
   }
}

func main(){
   /* nextNumber 为一个函数,函数 i 为 0 */
   nextNumber := getSequence()  

   /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   fmt.Println(nextNumber())
   
   /* 创建新的函数 nextNumber1,并查看结果 */
   nextNumber1 := getSequence()  
   fmt.Println(nextNumber1())
   fmt.Println(nextNumber1())
}

结果

1
2
3
1
2

函数做为值

在Go中函数也是一种变量,我们可以通过type来定义它

同种方法:参数类型、个数、顺序相同,返回值相同

package main
import "fmt"
type testInt func(int) bool // 声明了一个函数类型

func isOdd(integer int) bool {
  if integer%2 == 0 {
    return false
  } 
  return true
}
func isEven(integer int) bool {
  if integer%2 == 0 {
  return true
  }
  return false
}

func filter(slice []int, f testInt) []int {
  var result []int
  for _, value := range slice {
    if f(value) {
        result = append(result, value)
    }
  }
  return result
} 
func main(){
  slice := []int {1, 2, 3, 4, 5, 7}
  fmt.Println("slice = ", slice)
  odd := filter(slice, isOdd) // 函数当做值来传递了
  fmt.Println("Odd elements of slice are: ", odd)
  even := filter(slice, isEven) // 函数当做值来传递了
  fmt.Println("Even elements of slice are: ", even)
}

type testInt func(int) bool就是将该种函数类型赋值给testInt

一般步骤:

  1. 定义一个函数类型
  2. 实现定义的函数类型
  3. 作为参数调用

有点接口的感觉

函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活

1.7 Panic和Recver

Panic和Recover
Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢?

func panic(v interface{})
func recover() interface{}

Panic
是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F调用panic,函数F的执行被中
断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产
生。也可以由运行时错误产生,例如访问越界的数组。
Recover
是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

下面这个函数演示了如何在过程中使用panic

var user = os.Getenv("USER")
  func init() {
    if user == "" {
        panic("no value for $USER")
    }
}   

下面这个函数检查作为其参数的函数在执行时是否会产生panic:

func throwsPanic(f func()) (b bool) {
  defer func() {
    if x := recover(); x != nil {
       b = true
    }
  }()
    f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
    return
}

1.8 defer

defer向当前函数注册稍后执行的函数调用。这些调用被称为延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作。
注意,延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并缓存起来。如对状态敏感,可改用指针或闭包。下面这个例子,说明用闭包时,变量跟着全局改动而改,但参数不变。

func main() {
    x,y:=1,2
    defer func(a int) {
        fmt.Println("defer x,y",a,y)  //对y进行闭包调用
    }(x)                                 //注册时复制调用参数

    x+=100                               //对x的修改不会影响延迟调用
    y+=100
    fmt.Println(x,y)
}
输出:
101 102
defer x,y 1 102

延迟调用可修改当前函数命名返回值,但其自身返回值被抛弃。
多个延迟注册按FILO次序执行:

func main(){
 defer println("a")
defer println("b")
输出:
b
a

编译器通过插入额外指令来实现延迟调用执行,而return和panic都会终止当前函数流程,引发延迟调用。另外,return语句不是ret汇编指令,它会先更新返回值。

func main() {
    fmt.Println("testdefer: ", testdefer())
}

func testdefer()(z int){
    fmt.Println("prt:", z)
    defer func() {
        fmt.Println("defer:",z)
        z+=100            // 修改命名返回值
    }()
    return 100         //实际执行次序: z= 100, call defer, ret
}
输出:
prt: 0
defer: 100
testdefer:  200

误用
千万记住,延迟调用在在函数结束时才被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。
案例:循环处理多个日志文件,不恰当的defer导致文件关闭时间延长。

func main() {
    for i:=0; i<10;i++{
        path:=fmt.Sprintf("./log/%d.txt",i)
        fmt.Println(path)
        f,err:=os.Open(path)
        if err!=nil{
            log.Println(err)
            continue
        }
        defer f.Close()  // 这个关闭操作在main函数结束时才执行,而不是当前循环中执行  
        
    }

}

应该直接调用,或重构为函数,将循环和处理算法分离。

func main() {
    do := func(n int) {
        path:=fmt.Sprintf("./log/%d.txt",n)
        f,err:=os.Open(path)
        if err!=nil{
            log.Println(err)
            continue
        }
        defer f.Close()  // 这个关闭操作在main函数结束时才执行,而不是当前循环中执行
    }
    for i:=0; i<10;i++{
        do(i)

    }

}

相关文章

网友评论

      本文标题:第四章 函数

      本文链接:https://www.haomeiwen.com/subject/tgekpftx.html