vlambda博客
学习文章列表

Go 语言设计哲学之十五:函数是一等公民

函数是日常工作中十分常用的,无论是C语言,C++、Java、Python、JavaScript 都是必不可少的。 


Go语言函数的特点如下: 

1 func关键字开头。 

2 支持多返回值。 

3 支持具名返回值。 

4 支持递归调用。 

5 支持同类型的可变参数。 

6 支持defer,让函数优雅的返回。 


一、 GO语言中如何让函数变成 一等公民 的? 


1 正常创建。标准库代码如下 

// $GOROOT/src/fmt/print.go func newPrinter() *pp {  p := ppFree.Get().(*pp)  p.panicking = false  p.erroring = false  p.wrapErrs = false  p.fmt.init(&p.buf)  return p }


2 在函数内创建 


在hexdumpWords函数内定义了一个匿名函数(被赋值给p1) 

// $GOROOT/src/runtime/print.go func hexdumpWords(p, end uintptr, mark func(uintptr) byte) {  p1 := func(x uintptr) {  var buf [2 * sys.PtrSize]byte  for i := len(buf) - 1; i >= 0; i-- {  if x&0xF < 10 {  buf[i] = byte(x&0xF) + '0'  } else {  buf[i] = byte(x&0xF) - 10 + 'a'  }  x >>= 4  }  gwrite(buf[:])  }  ... ... }


3 作为类型 

// $GOROOT/src/net/http/server.go type HandlerFunc func(ResponseWriter, *Request) 


4 作为入参 

$GOROOT/src/time/sleep.go 
func AfterFunc(d Duration, f func()) *Timer { t := &Timer{ r: runtimeTimer{ when: when(d), f: goFunc, arg: f, }, } startTimer(&t.r) return t }


5 作为返回值 

// $GOROOT/src/strings/strings.go func makeCutsetFunc(cutset string) func(rune) bool {  if len(cutset) == 1 && cutset[0] < utf8.RuneSelf {  return func(r rune) bool {  return r == rune(cutset[0])  }  }  if as, isASCII := makeASCIISet(cutset); isASCII {  return func(r rune) bool {  return r < utf8.RuneSelf && as.contains(byte(r))  }  }  return func(r rune) bool { return IndexRune(cutset, r) >= 0 } }


函数除了像上面那些例子,函数还可以被放入数组、切片、map等结构体中,可以像其他类型变量一样被赋值给interface{}、你也可以建立函数类型的channel。 


二、 函数的特殊运用 


1 显示转型

var x int = 5 var y int32 = 6 fmt.Println(x + y)

运行这段代码,会报错,告诉你类型不匹配,你需要进行类型转换,如下 

var x int = 5 var y int32 = 6 fmt.Println(x + int(y))

函数是一等公民,上面是对变量进行的操作,同样也可以运用到函数中,即函数也可以被显示转型。 


package main 
import ( "fmt" "net/http" )
func hi(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, “Hi, Gopher!") }
func main() { http.ListenAndServe(":8080", http.HandlerFunc(hi)) }

上面就是web server的例子。当用户在浏览器中输入端口8080时,会显示 Hi, Gopher!


我们看一下ListenAndServe的源码 

// $GOROOT/src/net/http/server.go func ListenAndServe(addr string, handler Handler) error {  server := &Server{Addr: addr, Handler: handler}  return server.ListenAndServe() }

http请求交给其第二个参数handler处理,而handler参数类型是http.Handler接口 

// $GOROOT/src/net/http/server.go type Handler interface {  ServeHTTP(ResponseWriter, *Request) }


该接口只有一个方法ServeHTTP,原型就是func(http.ResponseWriter, *http.Request),与前面定义的hi原型一致。但无法直接将hi作为参数传入,否则报错(函数hi并未实现接口Handler方法,无法将其赋值给Handler类型参数) 

上面的代码我们是使用 http.HandlerFunc(hi)这样的方式,看看http.HandlerFunc源码: 

// $GOROOT/src/net/http/server.go type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {  f(w, r) }

HandlerFunc其实就是一个基于函数定义的新类型,它的底层类型为func(ResponseWriter, *Request)。该类型有一个方法ServeHTTP,继而实现了Handler接口。 

 http.HandlerFunc(hi)这句语法上没有报错,是因为HandlerFunc的底层类型正是func(ResponseWriter,*Request),与hi的原型是一致的,这和我们对使用过的整型变量的转型是一致的。代码如下:

type MyInt intvar a int = 666b:=MyInt(a)


2 函数式编程

 func multiply(x, y int) int {  return x * y } 

func DoWork(x int) func(int) int { return func(y int) int { return multiply(x, y) } }

func main() { timesTwo := DoWork(6) fmt.Println(timesTwo(8)) }

执行上面的代码输出 48 


将原接受2个参数的函数multiply转换为接受一个参数的DoWork。通过DoWork函数构造multiply。 

这里的函数利用了函数的两点特性: 

1 在函数体内定义函数,通过返回值返回给外部。 

2 闭包 


闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问它的外部函数的作用域。实际上,闭包是将函数内部和函数外部链接起来的桥梁。上面DoWork内部的匿名函数就是一个闭包。 


2 函数式编程 

type Do interface {  Work(func(jobName string,content string)) } 

type Job struct { JobName string Content string }

func (j Job) Work(fn func(jobName string,content string)) { fn(j.JobName, j.Content) }

func WorkDesc(jobName string,content string){ fmt.Println(jobName+""+content) } func main() { job := Job{JobName: "开发工作:",Content: "研发"} job.Work(WorkDesc) }

运行上面的代码,结果: 

开发工作:研发 


3 延迟加载。 

以斐波那契数列为例,它也是一种递归。代码如下: 

func fab(n int) int {  if n == 1 {  return 1  } else {  return n * fab(n-1)  } } func main() {  fmt.Printf("%d\n", fab(10)) }