本文是对匿名函数、高阶函数、闭包、同步、延时及其他 Goalng 函数类型或特性的概览。

这篇文章是针对 Go 语言中不同的函数类型或特性的摘要总结。
更为深入的探讨我会在近期的文章中进行,因为那需要更多的篇幅。这只是一个开端。

命名函数

一个命名函数拥有一个函数名,并且要声明在包级作用域中——其他函数的外部

我已经在另一篇文章中对它们进行了完整的介绍


这是一个命名函数:Len 函数接受一个 string 类型的参数并返回一个 int 类型的值


可变参数函数

变参函数可接受任意数量的参数

我已经在另一篇文章中对它们进行了完整的介绍



方法

当你将一个函数附加到某个类型时,这个函数就成为了该类型上的一个方法。因此,它可以通过这个类型来调用。在通过类型来调用其上的某个方法时,Go 语言会将该类型(接收者)传递给方法。

示例

新建一个计数器类型并为其定义一个方法:

func (c Count) Incr() int {
func Incr(c Count) int

如上的方法与以下写法有同样的效果(但并不等价):

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
Incr() intIncr() int
func onApiHit(c Counter) {
@@ -138,13 +122,11 @@ onApiHit(&dummyCounter)
// dummyCounter = 1
Incr() intonApiHit()Incr() intonApiHit()

接口方法与普通方法的区别在于接口方法更具伸缩性、可扩展性,并且它是松耦合的。你可以利用接口方法在不同的包之间进行各自所需的实现,而不用修改 onApiHit 或是是其他方法的代码

函数是一等公民

一等公民意味着 Go 语言中函数也是一种值类型,可以像其他类型的值一样被存储或是传递。

函数可以作为一种值类型和其他的类型配合使用,反之亦然

示例

Crunchers”crunch“Crunchers”crunch“
intint
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
}
CrunchCruncher
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

示例

crunchermaincruncher
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
         })
}
crunchCruncher
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 类型的函数。

crunch(nums, mul, add, sub)
type tokenizer func() (token string, ok bool)

下面的 split 函数是一个高阶函数,它根据指定的分割符来分割一个字符串,然后返回一个可以遍历这个被分割的字符串中所有单词的闭包这个闭包可以使用”token“和”last“两个在其捕获的环境下定义的变量。 下面的 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 变量
  • 在这里,我们使用了 split 函数将一句话分割成了若干个单词,然后得到了一个迭代器函数,并将它赋值给 iter 变量
  • 然后,我开始了一个当 iter 函数返回 false 的时候才停止的无限循环
  • 每次调用 iter 都能返回下一个单词

结果:

The
quick
brown
fox
jumps
over
the
lazy
dog

再次提示,这里面有更详细的描述哦~


延时函数

延时函数 (defer funcs)

延时函数只在其父函数返回时被调用。多个延时函数会以栈的形式一个接一个被调用。

我在另一篇文章中对延时函数有详细介绍



并发函数

go func()go func()

goroutine 是一种轻量级的线程机制,它能使你方便快捷的安排并发体系。其中,main 函数在 main-goroutine 中执行。

示例

这里,“start”匿名函数通过“go”关键字进行调用,不会阻塞父函数的执行: 这里,“start” 匿名函数通过 “go” 关键字进行调用,不会阻塞父函数的执行:

start := func() {
@@ -408,18 +380,16 @@ concurrent func: ends
main: ends


如果并发函数没有调起睡眠状态,那么main 函数不会它们的执行结束。

如果 main 函数中没有睡眠等阻塞调用,那么,main 函数会终止,而不会等待并发函数执行完。

main: continues...
main: ends

你能在任意一门语言中使用递归函数,Go 语言中的递归函数实现与它们也没有本质上的区别。然而,你可别忘了每一次的函数调用通常都会创建一个调用栈。但在 Go 中,栈是动态的,它们能根据相应函数的需要进行增减。如果你可以不使用递归解决手上的问题,那最好。

黑洞函数

黑洞函数能被多次定义,并且不能用通常的方式进行调用。它们在测试解析器的时候有时会非常有用:看这里

func _() {}
func _() {}

内联函数

行内函数

Go 语言的连接器会在运行时将函数置于可执行环境以方便后面的调用。相比于直接执行代码,有时候调用函数所需开销会小一些。因此,编译器将函数体直接注入到调用器中。 Go 语言的链接器会将函数放置到可执行环境中,以便稍后在运行时调用它。与直接执行代码相比,有时调用函数是一项昂贵的操作。所以,编译器将函数的主体注入调用者函数中。

更多的相关资料请参阅:这里、这里、这里和这里。

外部函数

如果你省略掉函数体,仅仅进行函数声明,连接器会尝试在任何可能的地方找到这个外部函数。例如:Atan Func在这里只进行了声明,而后在这里进行了实现。 @@ -460,6 +424,6 @@ via: https://blog.learngoprogramming.com/go-functions-overview-anonymous-closure

校对:rxcai、polaris1119

本文由 GCTT 原创编译,Go 中文网 荣誉推出

想看更多golang特讯请关注微信公众号: