Go进阶之习惯函数一等公民

23 阅读8分钟

在Go语言中.函数是唯一一种基于特定输入 实现特定任务并可反馈任务执行结果的代

码块.本质上可以说Go程序就是一组函数的集合.

1.函数特点:

1).以func关键字开头.

2).支持多返回值.

3).支持具名返回值.

4).支持递归调用.

5).支持同类型的可变参数.

6).支持defer.实现函数优雅返回.

2.一等公民概念(wiki发明人 c2站点作者的诠释):

如果一门编程语言对某种语言元素的创建和使用没有限制.我们可以像对待值(value)

一样对待这种元素语法.那么我们就称这种语法元素是这门编程语言的一等公民.拥有

一等公民待遇的语法元素可以存储在变量中.可以作为参数传递给函数.可以在函数内

部创建并可以作为返回值从函数返回.在动态类型语言中.语言运行还支持对一等公民

类型检查.

3.Go对一等公民的诠释:

3.1正常创建:

我们可以在源码底层正常创建一个函数.如下所示:

源码位置:src/fmt/print.go:newPrinter

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p
}

3.2在函数内创建:

在Go语言中我们可以在函数内定义一个新函数.如下在hexdumpWords函数内部定

义一个匿名函数(被赋值给变量p1).

源码位置:src/runtime/print.go:

```
// hexdumpWords prints a word-oriented hex dump of [p, end).
//
// If mark != nil, it will be called with each printed word's address
// and should return a character mark to appear just before that
// word's value. It can return 0 to indicate no mark.
func hexdumpWords(p, end uintptr, mark func(uintptr) byte) {
    printlock()
    var markbuf [1]byte
    markbuf[0] = ' '
    minhexdigits = int(unsafe.Sizeof(uintptr(0)) * 2)
    for i := uintptr(0); p+i < end; i += goarch.PtrSize {
       if i%16 == 0 {
          if i != 0 {
             println()
          }
          print(hex(p+i), ": ")
       }

       if mark != nil {
          markbuf[0] = mark(p + i)
          if markbuf[0] == 0 {
             markbuf[0] = ' '
          }
       }
       gwrite(markbuf[:])
       val := *(*uintptr)(unsafe.Pointer(p + i))
       print(hex(val))
       print(" ")

       // Can we symbolize val?
       fn := findfunc(val)
       if fn.valid() {
          print("<", funcname(fn), "+", hex(val-fn.entry()), "> ")
       }
    }
    minhexdigits = 0
    println()
    printunlock()
}
```

3.3作为类型:

可以使用函数自定义类型.示例如下:

源码位置:src/net/http/server.go

```
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// [Handler] that calls f.
type HandlerFunc func(ResponseWriter, *Request)
```

3.4存储到变量:

可以将定义好的函数存储到一个变量中.

源码位置:src/runtime/vdso_linux.go

image.png

3.5作为参数传入函数:

可以将函数作为参数传入函数.

源码位置:src/time/sleep.go

```
// AfterFunc waits for the duration to elapse and then calls f
// in its own goroutine. It returns a [Timer] that can
// be used to cancel the call using its Stop method.
// The returned Timer's C field is not used and will be nil.
func AfterFunc(d Duration, f func()) *Timer {
    return (*Timer)(newTimer(when(d), 0, goFunc, f, nil))
}
```

3.6作为返回值从函数返回:

函数可以作为返回值从函数返回.

总结:

Go中的函数可以像普通整型那样被创建和使用.

除了上面的情况.函数还可以被放入数组 切片或map等结构.可以像其他类型变量一

样赋值个interface{}.示例如下:

```
package main

import (
    "fmt"
    "gomodule/data"
    _ "gomodule/pubsub"
    "time"
)

type binaryFunc func(int, int) int

func main() {
    var i interface{} = binaryFunc(func(a, b int) int { return a + b })

    c := make(chan func(int2, int3 int) int, 10)

    fns := []binaryFunc{
       func(a, b int) int { return a + b },
       func(a, b int) int { return a - b },
       func(a, b int) int { return a * b },
    }

    c <- func(int2, int3 int) int {
       return int2 + int3
    }

    fmt.Println(fns[0](5, 6))

    f := <-c
    fmt.Println(f(5, 6))

    v, ok := i.(binaryFunc)
    if !ok {
       fmt.Println("type assertion error")
       return
    }

    fmt.Println(v(5, 6))
}
```

4.特殊作用:

4.1像对整型变量那样对函数进行显示类型转换.

Go是类型安全的语言.不允许隐式类型转换.

```
var a int = 5
var b int32 = 6
//违法操作.无法编译.
fmt.Println(a + b)
//可以编译
fmt.Println(a + int(b))
```

对整型变量进行的操作也可以用在函数上.函数也可以被显示类型转换.这样的类型转

换在特定的领域有奇妙的作用.示例如下:

```
package main

import (
    "fmt"
    "gomodule/data"
    _ "gomodule/pubsub"
    "net/http"
    "time"
)


func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}

func greeting(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
}
```

这是一个常见的webserver例子.这里用到了一等公民函数的特性.

ListenAndServe源码如下:

```
// ListenAndServe listens on the TCP network address addr and then calls
// [Serve] with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case [DefaultServeMux] is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}
```

ListenAndServe会将来自客户端的http请求交给handler处理.这里的handler参

数的类型http.handler接口如下:

```
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
```

该接口有一个serveHttp方法.原型为

*func(http.ResponseWrite,http.Request).与我们定义的方法类型一致.但是不

能直接传入.因为greeting没有实现handler接口的方法.所以无法直接传入.

因为main方法中把函数传给了ListenAndServe.看看http.HandlerFunc是什么.

```
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// [Handler] that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
```

handlerFunc是一个基于函数的新类型.然后实现了ServeHttp方法.也就是实现了

Handler接口.所以说就是把传入的函数显示的转换成了HandlerFunc类型.然后后

者实现了Handler接口.这样就满足了要求.

4.2函数式编程:

Go语言演进至今.对多种编程范式或多或少都有支持.

柯里化函数:

是把接收的多个参数函数变换成接收一个单一参数(原函数的第一个函数)的函数.并

返回接受余下的参数和返回结果的新函数技术.

```
package main

import (
    "fmt"
    "gomodule/data"
    _ "gomodule/pubsub"
    "time"
)

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

func partialTimes(x int) func(int) int {
    return func(y int) int {
       return times(x, y)
    }
}

func main() {
    timesTwo := partialTimes(2)
    timesThree := partialTimes(3)
    timesFour := partialTimes(3)

    fmt.Println(timesTwo(5))
    fmt.Println(timesThree(5))
    fmt.Println(timesFour(5))
}
```

这个例子运用函数的两点性质.在函数中定义.通过返回值返回.闭包.

闭包是在函数内部定义的匿名函数.并且允许该匿名函数访问定义它的外部函数的作

用域.本质上.闭包是将函数内部和函数外部连接起来的桥梁.

函子:

函子需要满足两个条件.

函子本身是一个容器类型.以GO语言为例子.这个容器可以是切片 map 甚至

channel.

该容器类型需要实现一个方法.该方法接受一个类型参数.并在容器的每个元素上应用

这个函数.得到一个新函子.原函子容器内部元素不受影响.

```
package main

import (
    "fmt"
    "gomodule/data"
    _ "gomodule/pubsub"
    "time"
)

type IntSliceFunctor interface {
    Fmap(fn func(int) int) IntSliceFunctor
}

type IntSliceFunctorImpl struct {
    ints []int
}

func (isf IntSliceFunctorImpl) Fmap(fn func(int) int) IntSliceFunctor {
    newInts := make([]int, len(isf.ints))
    for i, elt := range isf.ints {
       retInt := fn(elt)
       newInts[i] = retInt
    }
    return IntSliceFunctorImpl{newInts}
}

func NewIntsSliceFunctor(slice []int) IntSliceFunctor {
    return IntSliceFunctorImpl{slice}
}

func main() {

    //原切片.
    intSlice := []int{1, 2, 3, 4}

    functor := NewIntsSliceFunctor(intSlice)

    mapperFunc1 := func(i int) int {
       return i + 10
    }

    fmap := functor.Fmap(mapperFunc1)

    mapperFunc2 := func(i int) int {
       return i + 3
    }

    fmap.Fmap(mapperFunc2)

    fmt.Println(functor)
}
```

1).定义了一个intSliceFunctorImpl类型.用来作为函子的载体.

2).函子要实现的方法命名为Fmap.intSliceFunctorImpl类型实现了该方法.

该方法也是intSliceFunctor唯一的方法.

3).定义创建了intSliceFunctor的函数NewIntSliceFunctor.通过该函数初始化一

个切片.

4).在main方法中定义了两个转换函数.这两个函数应用到上述函子例子.得到新的函

子的内部容器元素值是元容器的元素值经过转换得到的.

5).无论如何应用转换函数.原函子容器的值不受影响.

4.2.3延续传递式:

函数式离不开递归.以求阶乘为例.

```
package main

import (
    "fmt"
    "gomodule/data"
    _ "gomodule/pubsub"
    "time"
)

func factorial(n int) int {
    if n == 1 {
       return 1
    } else {
       return n * factorial(n-1)
    }
}

func main() {
    fmt.Println(factorial(5))
}
```

上述例子未用到一等公民的性质.函数式编程有一种被称为延续式传递的编程风格可

以充分运用函数作为一等公民的性质.

在CPS风格中.函数是不允许有返回值.一个函数A应该将其想返回的值显示传给一个

continuation函数(一般接受一个参数).而这个函数自身是函数A的一个参数.示例如

下.

```
func CpsMax(a, b int) int {
    if a > b {
       return a
    } else {
       return b
    }
}

func main() {
    fmt.Println(CpsMax(5, 6))
}
```

max函数看作上面定义中的A函数在CPS转化之前的状态.根据CPS定义将其转换风

格.

1).去掉max的返回值.并为其添加一个函数类型的参数f(这个函数就是continute函

数):

```
func Max(x, y int, f func(int)) {
}
```

2).将返回结果传递给continuation函数.把return语句替换为对f函数的调用.

```
func CpsMax(x, y int, f func(int)) {
    if x > y {
       f(x)
    } else {
       f(y)
    }
}

func main() {
    CpsMax(5, 6, func(i int) {
       fmt.Println(i)
    })
}
```

总结:

Go函数可以像变量值那样赋值给变量.作为函数传递 作为返回值返回以及在函数内部

创建.

函数可以像变量那样被显示转换.

基于函数特质.了解了Go中几种有用的编程风格.

不要为了符合特定风格滥用函数特质.

星星都失重了.





如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路