golang的函数与方法

栏目: Go · 发布时间: 6年前

内容简介:在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。函数类型的零值是nil。调用值为nil的函数值会引起panic错误:函数值不仅仅是一串代码,还记录了状态。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。我们看个闭包的例子:在这个例子中,f1函数传入limit参数,返回一个闭包,闭包接受一个参数v,判断v是否大于之前设置进去的limit。

函数

//常规的函数定义
func 方法名(参数列表) 返回值 {
    定义
}

函数的值(闭包)

Go 中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。函数类型的零值是nil。调用值为nil的函数值会引起panic错误:

var f func(int) int
f(3) // 此处f的值为nil, 会引起panic错误

函数值不仅仅是一串代码,还记录了状态。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。我们看个闭包的例子:

func f1(limit int) (func(v int) bool) {
    //编译器发现limit逃逸了,自动在堆上分配
    return func (limit int) bool { return v>limit} 
}

func main() {
    closure := f1(5)
    fmt.Printf("%v\n", closure(1)) //false
    fmt.Printf("%v\n", closure(5)) //false
    fmt.Printf("%v\n", closure(10)) //true
}

在这个例子中,f1函数传入limit参数,返回一个闭包,闭包接受一个参数v,判断v是否大于之前设置进去的limit。

可变参数列表

可变参数,即参数不是固定的,例如fmt.Printf函数那样,注意只有 最后一个参数才可以是声明为可变参数 ,声明:

func 函数名(变量名...类型) 返回值

我们看个例子:

package main

import (
    "fmt"
)

func f1(name string, vals... int) (sum int) {
    for _, v := range vals {
        sum += v
    }
    sum += len(name)
    return
}

func main() {
    fmt.Printf("%d\n", f1("abc", 1,2,3,4 )) //13
}

函数的延迟执行 defer

包含defer语句的 函数执行完毕后(例如return、panic),释放堆栈前会调用被声明defer的语句 ,常用于释放资源、记录函数执行耗时等,有一下几个特点:

  1. 当defer被声明时,其参数就会被实时解析
  2. 执行顺序和声明顺序相反
  3. defer可以读取有名返回值

看个例子:

package main

import (
    "fmt"
)

//演示defer的函数可以访问返回值
func f2() (v int) {
    defer func (){ v++}()
    return 1 //执行这个时,把v置为1
}

//演示defer声明即解释
func f3(i int) (v int) {
    defer func(j int) { v += j} (i) //此时函数i已被解析为10,后面修改i的值无影响
    v = i
    i = i*2
    return
}

//演示defer的执行顺序,与声明顺序相反
func f4() {
    defer func() {fmt.Printf("first\n")} ()
    defer func() {fmt.Printf("second\n")} ()
}

func main() {
    fmt.Printf("%d\n", f2()) // 13
    fmt.Printf("%d\n", f3(10)) // 20
    f4() //second\nfirst\n
}

典型的使用场景,函数执行完毕关闭资源:

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    defer func(f io.Closer) {
        if err := f.Close(); err != nil {
            // log etc
        }
    }(f)

    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }

    defer func(f io.Closer) {
        if err := f.Close(); err != nil {
            // log etc
        }
    }(f)

    return nil
}

在这里例子中可以看到,我们判断了Close()是否成功,因为在一些文件系统中,尤其是NFS,写文件出错往往被延迟到Close的时候才反馈,所以必须检查Close的状态。

异常panic

Go有别于那些将函数运行失败看作是异常的语言。虽然Go有各种异常机制,但这些机制仅仅用于严重的错误,而不是那些在健壮程序中应该被避免的程序错误。runtime在一些情况下会抛出异常,例如除0,我们也能使用panic关键字自己抛出异常

panic(异常的值) //值是啥都行

出现异常之后,默认情况就是程序退出并打印堆栈:

package main


func f6() {
    func () {
        func () int {
            x := 0
            y := 5/x
            return y
        }()
    }()
}

func main() {

    f6()

}

输出

panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.f6.func1.1(...)
    /Users/kitmanzheng/study/go/src/test_func.go:8
main.f6.func1()
    /Users/kitmanzheng/study/go/src/test_func.go:10 +0x11
main.f6()
    /Users/kitmanzheng/study/go/src/test_func.go:11 +0x20
main.main()
    /Users/kitmanzheng/study/go/src/test_func.go:16 +0x20
exit status 2

如果不想程序退出的话,也有办法,就是 使用recover捕捉异常,然后返回error 。在没发生panic的情况下,调用recover会返回nil,发生了panic,那么就是panic的值。看个例子:

package main

import (
    "fmt"
)

type shouldRecover struct{}
type emptyStruct struct{}

func f6() (err error) {
    defer func () {
        switch p := recover(); p {
            case nil: //donoting
        case shouldRecover{}:
            err = fmt.Errorf("occur panic but had recovered")
        default:
            panic(p)
        }
    } ()

    func () {
        func () int {
            panic(shouldRecover{})
            //panic(emptyStruct{})
            x := 0
            y := 5/x
            return y
        }()
    }()

    return
}


func main() {
    err := f6()
    if err != nil {
        fmt.Printf("fail %v\n", err)
    } else {
        fmt.Printf("success\n")
    }
}

输出

fail occur panic but had recovered

在这个例子中,我们手动抛出一个panic,值是shouldRecover,然后外层使用defer + 匿名函数 + recover去捕捉异常,发现panic的值是shouldRecover那么就不退出,而是返回error。

方法

//这种只能给type定义的类型用 
func (type类型参数) 方法名(参数列表) 返回值 {
    定义
}
//eg:
func (t TestType) testFunc() int {
    //...
}

例子中t称为接收器,可以是该类型本身,或该类型的指针,由于是值传递,所以是接收器是该类型时,会复制值,类型比较大时开销大,可以选择使用指针降低开销。而且在使用defer的时候,由于值复制,如果不用指针,变量发生了变化,但是defer运行时还是基于老变量运行的,容易会造成一些坑,除非你明确知道自己要这么做。 建议func (*type)而不是func(type) 。但是如果一个类型低层实际是一个指针,那么不允许在使用该类型的指针作为接收器。

当我们使用指针作为接收器时,记得检查是否是nil。

看下面这个例子:

type myInt struct {
    owner string
    value int
}

func (a myInt) Owner(suffix string) string { //golang不支持默认参数
    return a.owner + suffix
}

func (a *myInt) SetOwner(owner string) {
    if a == nil {
        fmt.Println("set owner to nil point is invalid")
        return
    }
    a.owner = owner
}

func (a myInt) SetOwner2(owner string) { //golang函数参数按值传递,所以这个方法实际只是修改临时变量的owner
    a.owner = owner
}

func SetOwner3(a *myInt, owner string) {
    if a == nil {
        fmt.Println("set owner to nil point is invalid")
        return
    }
    a.owner = owner
}

func main() {
    var k = myInt{"kitman", 3}

    fmt.Print(k.value, " ", k.Owner("aa"), "\n") //输出3 kitmanaa

    k.SetOwner("ak") //相当于SetOwner(&k, "ak")
    fmt.Print(k.value, " ", k.Owner("bb"), "\n") //输出3 akbb

    k.SetOwner2("sss")  //相当于SetOwner(k, "sss")
    fmt.Print(k.value, " ", k.Owner("bb"), "\n") //输出3 akbb

    SetOwner3(&k, "sss")
    fmt.Print(k.value, " ", k.Owner("bb"), "\n") //输出3 sssbb

    var k2 *myInt = nil
    k2.SetOwner("aa")     //输出set owner to nil point is invalid
}

输出

3 kitmanaa

3 akbb

3 akbb

3 sssbb

set owner to nil point is invalid

通过上面的例子,我们可以发现一些知识点:

  1. 使用第二种函数定义的方法,那么就和c++的类差不多。本质上和普通函数一样,就是语法上的差别而已。
  2. 就算给type类型定义方法,函数参数也是按值传递的,所以type参数使用指针才能修改变量。
  3. nil指针也能调用方法,但是如果方法里面没判断指针是否是nil,那么就会core

面向对象继承语义

可以通过使用匿名成员 + 定义方法,实现部分继承的语义:

package main

import (
    "fmt"
)

type Base struct {
    y int
    Y int
}

func (b *Base) FuncByPoint() int {
    if (b == nil) {
        return 0;
    }

    return b.y*b.Y
}

func (b Base) FuncByValue() int {
    return b.y*b.Y
}

type Child struct {
    Base
    x int
    X int
}

func (c *Child) FuncByPoint() int {
    if (c == nil) {
        return 0
    }

    return c.x*c.X
}

func main() {
    var c Child
    c.y = 2
    c.Y = 3
    fmt.Printf("%v\n", c.FuncByPoint())          //0
    fmt.Printf("%v\n", c.Base.FuncByPoint())//6
    fmt.Printf("%v\n", c.FuncByValue()).  //6

    var f1 func() int
    f1 = c.FuncByPoint
    fmt.Printf("%v\n", f1())   //0

    var f2 func(*Child) int
    f2 = (*Child).FuncByPoint
    fmt.Printf("%v\n", f2(&c)) //0
}

这个例子可以看到,Base中定义的方法,被外层的同名方法覆盖,需要显式指明才能调用到Base中的方法。 注意golang中不存在真正的继承,这是嵌入匿名成员,用匿名成员的方法去理解这样的语法 。另外,方法的值也是第一类变量,能赋值给别的变量,比c/c++灵活, golang无论是对象方法,还是类型的方法,都能赋值给别的变量 ,可以参照例子中的写法。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Mastering Regular Expressions, Second Edition

Mastering Regular Expressions, Second Edition

Jeffrey E F Friedl / O'Reilly Media / 2002-07-15 / USD 39.95

Regular expressions are an extremely powerful tool for manipulating text and data. They have spread like wildfire in recent years, now offered as standard features in Perl, Java, VB.NET and C# (and an......一起来看看 《Mastering Regular Expressions, Second Edition》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试