Golang 之 函数

xys1228 · · 1182 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

函数声明

语法:

func name(parameter-list) (result-list) {
    body
}

如果函数没有返回值, 或者仅仅有一个匿名的返回值的话, 那么 result-list 的圆括号可以省略, 例如:

func sayHello() () {
    fmt.Println("Hello, world!")
}

可以写为:

func sayHello() {
    fmt.Println("Hello, world!")
}

除了匿名的返回值外(即仅仅给出返回值的类型, 而没有名字), Go 还支持命名的返回值, 例如:

func max(x, y int) (z int) {
    if x > y {
       z = x
    } else {
       z = y
    }
    return
}

上面的函数中, 返回值是命名的, 它的名字是 "z", 并且这个返回值在函数内可见. 因此我们可以在函数中对这个返回值变量进程操作, 最后直接使用一个 return 语句返回即可, 此时函数返回的就是变量 z 的值了.

函数变量

在 Go 中, 函数是一等公民, 因此像其他类型一样, 函数也有类型, 并且它们可以赋值给一个变量, 将函数变量作为一个参数传递给另一个函数甚至函数可以作为返回值返回.
例如:

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

func doSomething(x, y int, f func(int, int) int) {
    fmt.Println(f(x, y))
}

func main() {
    f := add

    fmt.Println(f(1, 2))
    doSomething(10, 20, f)
}

上面的例子中, 我们首先将函数 add 赋值给变量 f, 因此可以直接以 f 作为函数名调用.
接着我们将函数变量 f 作为参数传递给 doSomething 函数, 并且在 doSomething 中调用了它.
其中 doSomething 参数 "f func(int, int) int" 含义是接受一个函数变量, 其类型是 func(int, int) int, 即接受一个带有两个 int 参数, 并且返回值是 int 类型的函数.

匿名函数

对于命名函数来说, 它们必须要在包范围上定义, 但是我们可以利用函数字面量在任意的表达式中定义一个函数.
一个函数字面量的语法和函数声明类似, 但是它没有函数名. 并且一个函数字面量是一个表达式, 这个表达式的值被称为 匿名函数.
通过匿名函数, 我们就可以实现闭包:

func increment(x int) func() int {
    return func () int {
        x++
        return x
    }
}

func main() {
    f := increment(10)

    fmt.Println(f())
    fmt.Println(f())
    fmt.Println(f())
    fmt.Println(f())
}

上面的 increment 函数接收一个 int 变量, 然后返回一个 func() int 类型的函数. 注意到, 在返回的匿名函数中, 使用到了 increment 的局部变量, 每次调用返回的这个函数, 那么 x 的值就自增一.
由上面的例子, 我们看到, 一个函数不仅仅是由代码构成的, 它还有内部状态(在这里, 函数 f 的状态就是 x 的值, 它捕获了外部变量 x, 每次调用 f 时, x 的值都改变, 因此 f 的状态也就改变了). 正因为函数内部维护有隐藏的状态, 因此我们才规定一个函数类型是一个引用类型, 并且函数类型是不可比较的.
如上面 increment 函数返回的匿名函数类似, 如果一个匿名函数捕获了外部的一个局部变量, 那么我们就称这个匿名函数为闭包(closure)

变参函数

一个函数如果可以接收的函数个数不定, 则称为变参函数.
一个变参函数最后一个参数的参数名和类型需要添加 ..., 表示此参数是变参, 例如:

func oper(name string, vals ...int) {
    total := 0
    if (name == "add") {
        for _, val := range vals {
            total += val
        }
    } else {
        for _, val := range vals {
            total -= val
        }
    }

    fmt.Printf("Operation %s, result: %d\n", name, total)
    fmt.Printf("vals type: %T\n", vals)
}
func main() {
    oper("add", 1, 2, 3, 4, 5)
}

oper 函数的第一个参数是一个字符串, 剩下的参数是一个或多个 int 变量.
Go 实现变参函数的原理是: 首先分配一个数组, 然后将参数拷贝到数组中, 接着将整个数组的切片作为参数传递给函数.
因此上面 "oper("add", 1, 2, 3, 4, 4)" 调用可以等效为:

arr := []int{1, 2, 3, 4, 5}
oper("add", arr[:]...)
// 也可以直接传递数组
oper("add", arr...)

不过上面的例子有一个需要特别注意的地方, 如果参数已经是 slice 的了, 那么调用变参函数时, 参数后需要添加**...**
虽然在函数内部, 变参是一个 slice, 但是变参函数和接收 slice 的函数是不同类型的.
例如:

func f(...int) {}
func g([]int) {}

func main() {
    fmt.Printf("%T\n", f) // "func(...int)"
    fmt.Printf("%T\n", g) // "func([]int)"
}

关于 panic 和 recover

panic 类似于 Java 中的异常, 当程序运行时出现了致命错误时, 会产生一个 panic.
当然我们也可以手动触发一个 panic, 例如:

func test() {
    panic("some error")
}

我们直接调用内建的 panic 函数就可以收到发出一个 panic 了, 类似于 Java 中的 new Exception() 一样.
如果产生了一个 panic 的话, 通常来说程序会立即终止. 但是有时候我们并不希望发生 panic 时造成整个程序的崩溃, 此时可以使用 recover 函数来捕获这个 panic (相当于 Java 的 try ... catch). 例如:

func test() {
    defer func() {
        if p := recover(); p != nil {
            fmt.Printf("We got error: %v\n", p)
        }
    }()

    panic("some error")
}

func main() {
    test()
    fmt.Println("OK")
}

使用 recover 有两点需要注意的是:

  • recover 调用必须在 defer 函数中.
  • 包裹 recover 的 defer 函数需求在潜在的 panic 代码前调用.

例如:

func test() {
    panic("some error")
    
    defer func() {
        if p := recover(); p != nil {
            fmt.Printf("We got error: %v\n", p)
        }
    }() 
}

上面的代码直接就导致了程序的崩溃, 而不会触发 recover.


有疑问加站长微信联系(非本文作者)

本文来自:博客园

感谢作者:xys1228

查看原文:Golang 之 函数

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1182 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传