『GCTT 出品』Go 语言函数众生相
本文是对匿名函数、高阶函数、闭包、同步、延时(defer)及其他 Go 函数类型或特性的概览。
这篇文章是针对 Go 语言中不同的函数类型或特性的摘要总结。
更为深入的探讨我会在近期的文章中进行,因为那需要更多的篇幅。这只是一个开端。
命名函数
一个命名函数拥有一个函数名,并且要声明在包级作用域中——其他函数的外部
👉
我已经在另一篇文章中对它们进行了完整的介绍
这是一个命名函数:Len 函数接受一个 string 类型的参数并返回一个 int 类型的值
可变参数函数
变参函数可接受任意数量的参数
👉
我已经在另一篇文章中对它们进行了完整的介绍
方法
当你将一个函数附加到某个类型时,这个函数就成为了该类型上的一个方法。因此,它可以通过这个类型来调用。在通过类型来调用其上的某个方法时,Go 语言会将该类型(接收者)传递给方法。
示例
新建一个计数器类型并为其定义一个方法:
type Count intfunc (c Count) Incr() int {
c = c + 1
return int(c)
}
如上的方法与以下写法有同样的效果(但并不等价):
func Incr(c Count) int
原理并不完全如上所示,但你可以像这样来理解
值传递
当 Incr 被调用时,Count 实例的值会被复制一份并传递给 Incr。
var c Count; c.Incr(); c.Incr()// output: 1 1
c 的值并不会增加,因为 c 是通过值传递的方式传递给方法
指针传递(引用传递)
想要改变计数器 c 的值,你需要给 Incr 方法传入 Count 类型指针——*Count
。
func (c *Count) Incr() int {
*c = *c + 1
return int(*c)
}var c Count
c.Incr(); c.Incur()// output: 1 2
在我之前的一些文章中有更多的示例:看这里!看这里!
接口方法
我们用接口方法的方式来重建上面的程序。先创建一个叫做 Counter 的新接口:
type Counter interface {
Incr() int}
下面的 onApiHit 函数能使用任何拥有 Incr() int
方法的类型:
func onApiHit(c Counter) {
c.Incr()
}
我们即刻使用一下这个改造版的计数器——现在你可以使用一个名副其实的计数器接口了:
dummyCounter := Count(0)
onApiHit(&dummyCounter)// dummyCounter = 1
我们在 Count 类型上定义了一个 Incr() int
方法,因此 onApiHit()
方法可以通过它来增长 counter —— 我将 dummyCounter 的指针传入了 onApiHit,否则这个计数器不会因而增长。
接口方法与普通方法的区别在于接口方法更具伸缩性、可扩展性,并且它是松耦合的。你可以利用接口方法在不同的包之间进行各自所需的实现,而不用修改 onApiHit 或是是其他方法的代码
函数是一等公民
一等公民意味着 Go 语言中函数也是一种值类型,可以像其他类型的值一样被存储或是传递。
函数可以作为一种值类型和其他的类型配合使用,反之亦然
示例
以下程序通过 Crunchers 切片将一个数值序列作为参数传递到一个叫 ”crunch“ 的函数中去。
声明一个”用户自定义函数类型“,它需要接收一个 int 类型的值来返回一个 int 类型的值。
这意味着任何使用这种类型的代码都可以接受一个以如下形式签名的函数:
type Cruncher func(int) int
声明一些 cruncher 类型的函数:
func mul(n int) int { return n * 2}func add(n int) int { return n + 100}func sub(n int) int { return n - 1}
Crunch 是一个可变参数函数,通过 Cruncher 类型的可变参数处理一系列的整型数:
func crunch(nums []int, a ...Cruncher) (rnums []int) { // 创建一个等价的切片
rnums = append(rnums, nums...) for _, f := range a { for i, n := range rnums {
rnums[i] = f(n)
}
} return}
声明一个具有一些初始值的整型切片,之后对它们进行处理:
nums := []int{1, 2, 3, 4, 5}
crunch(nums, mul, add, sub)
输出:
[101 103 105 107 109]
匿名函数
匿名函数即没有名字的函数,它以函数字面量的方式在行内进行声明。它在实现闭包、高阶函数、延时函数等特殊函数时有极大作用。
函数签名
命名函数:
func Bang(energy int) time.Duration
匿名函数:
func(energy int) time.Duration
它们有相同的函数签名形式,所以它们可以互换着使用:
func(int) time.Duration
示例
我们用匿名函数的方式重构一下上面的”函数是第一公民“单元中的 cruncher 程序。在 main 函数中声明几个匿名 cruncher 函数。
func main() {
crunch(nums, func(n int) int { return n * 2
}, func(n int) int { return n + 100
}, func(n int) int { return n - 1
})
}
crunch 函数只期望接收到 Cruncher 类型的函数,并不关心它(它们)是命名函数还是匿名函数,因此以上代码可以正常工作。
为了提高可读性,在传入 crunch 之前你可以先将这些匿名函数赋值给变量。
mul := func(n int) int { return n * 2}
add := func(n int) int { return n + 100}
sub := func(n int) int { return n - 1}
crunch(nums, mul, add, sub)
高阶函数
高阶函数可以接收或返回一个甚至多个函数。本质上来来讲,它用其他函数来完成工作。
下面闭包单元中的 split 函数就是一个高阶函数。它的返回结果是一个 tokenizer 类型的函数。
闭包
闭包可以记住其上下文环境中所有定义过的变量。闭包的一个好处就是随时可以在其捕获的环境下操作其中的变量——小心内存泄漏!
示例
声明一个新的函数类型,它返回一个已分割的字符串的下一个单词:
type tokenizer func() (token string, ok bool)
下面的 split 函数是一个高阶函数,它根据指定的分割符来分割一个字符串,然后返回一个可以遍历这个被分割的字符串中所有单词的闭包。这个闭包可以使用 ”token“ 和 ”last“ 两个在其捕获的环境下定义的变量。
小试牛刀:
const sentence = "The quick brown fox jumps over the lazy dog"iter := split(sentence, " ")for {
token, ok := iter() if !ok { break }
fmt.Println(token)
}
- 在这里,我们使用了 split 函数将一句话分割成了若干个单词,然后得到了一个迭代器函数,并将它赋值给 iter 变量
- 然后,我开始了一个当 iter 函数返回 false 的时候才停止的无限循环
- 每次调用 iter 都能返回下一个单词
结果:
The
quick
brown
fox
jumps
over
the
lazy
dog
再次提示,这里面有更详细的描述哦~
延时函数 (defer funcs)
延时函数只在其父函数返回时被调用。多个延时函数会以栈的形式一个接一个被调用。
👉
我在另一篇文章中对延时函数有详细介绍
并发函数
go func()
会与其他 goroutines 并发执行。
goroutine 是一种轻量级的线程机制,它能使你方便快捷的安排并发体系。其中,main 函数在 main-goroutine 中执行。
示例
这里,“start” 匿名函数通过 “go” 关键字进行调用,不会阻塞父函数的执行:
start := func() {
time.Sleep(2 * time.Second)
fmt.Println("concurrent func: ends")
}go start()
fmt.Println("main: continues...")
time.Sleep(5 * time.Second)
fmt.Println("main: ends")
输出
main: continues...
concurrent func: ends
main: ends
如果 main 函数中没有睡眠等阻塞调用,那么,main 函数会终止,而不会等待并发函数执行完。
main: continues...
main: ends
其他类型
递归函数
你能在任意一门语言中使用递归函数,Go 语言中的递归函数实现与它们也没有本质上的区别。然而,你可别忘了每一次的函数调用通常都会创建一个调用栈。但在 Go 中,栈是动态的,它们能根据相应函数的需要进行增减。如果你可以不使用递归解决手上的问题,那最好。
黑洞函数
黑洞函数能被多次定义,并且不能用通常的方式进行调用。它们在测试解析器的时候有时会非常有用:看这里
func _() {}func _() {}
内联函数
Go 语言的链接器会将函数放置到可执行环境中,以便稍后在运行时调用它。与直接执行代码相比,有时调用函数是一项昂贵的操作。所以,编译器将函数的主体注入调用者函数中。
更多的相关资料请参阅:这里、这里、这里和这里。
外部函数
如果你省略掉函数体,仅仅进行函数声明,连接器会尝试在任何可能的地方找到这个外部函数。例如:Atan Func在这里只进行了声明,而后在这里进行了实现。
作者:Inanc Gumus
译者:shockw4ver
校对:rxcai、polaris1119
本文由 GCTT 原创编译,Go 中文网 荣誉推出
文章链接点击不了?点击 阅读原文 到 Go 中文网查看。
本文由 GCTT 原创翻译,Go语言中文网首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽