Go 函数Function · 2023年8月21日 0

Golang分享(三):“一等公民”函数【】

最近和产品对接了一个重构代码的需求,工作量巨大,因此可供阅读源码的时间不多。今天偷个懒分享下自己在学习Golang函数时的一些记录。

0.前言

函数是代码复用的重要手段,可以帮助我们提高开发效率,有效降低代码重合度。真正的程序员从书写函数开始。


1.函数声明

函数的声明格式为:

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

每个函数均包含以下几个部分

  • 关键字func
  • 函数名字name
  • 可选的形参列表parameter-list,参数列表用于定义形参的类型和形参的名称,可以有多个形参,也可以没有
  • 可选的返回值列表result-list,返回列表用于定义返回值的类型,可以有多个返回值,也可以没有
  • 函数体body,函数体即具体的业务逻辑

如下为一个求两数字最大值的函数

func max(a int, b int) int {
  if a > b {
    return a
  }
  return b
}

2. 参数

2.1 省略声明

在以上max函数中,形参a和b的类型是一样的,均为int类型,这个时候可以只使用一个int关键字用于声明形参a和b的类型,如下所示

func max(a , b int) int {
  if a > b {
    return a
  }
  return b
}

同样的以下函数都是合法的

//形参是int类型的a,int类型的b,以及float64类型的c
func max(a , b int, c float64) int {
}
//形参是int类型的a,int类型的b,以及float64类型的c,float64类型的d
func max(a , b int, c , d float64) int {
}

2.2 变长函数

如果函数的最后一个参数是采用...Type的形式,那么这个函数就是一个变长函数,调用这个函数的时候可以传递该类型的任意个参数,在函数内部可以按照该类型的slice处理这些参数。

func sum(vals ...int) int {
  total := 0
  for _, val := range vals {
    total += val
  }
  return total
}

当调用上述sum函数,并传入一个slice时,需要在slice后使用省略号,如下所示

values := []int{1,3,5,7}
fmt.Println(sum(values...)) //16

3. 返回值

3.1 多值返回

不同于Java、C等语言,Golang支持多值返回,即一个函数可以有多个返回值。当一个函数有多个返回值时,多个返回值类型需要用逗号隔开并用括号括起来。return多个返回值时,返回值的顺序要和函数声明的返回值顺序一致。

如下所示,改造一下取两个数最大值的max函数。

func max(a, b int) (int, error) {
  if a == b {
    return -1, errors.New("a和b不能相等")
  }
  if a > b {
    return a, nil
  }
  return b, nil
}

3.2 命名返回值

不同于Java、C等语言,Golang支持返回值命名,可以像参数一样直接在函数体内使用,且函数体内不能重复声明。如下所示,改造一下取两个数最大值的max函数

func max(a, b int) (max int, err error) {
  //max不能重复声明,因此 max := -1会报错哦
  max = -1
  err = errors.New("a和b不能相等")
  if a == b {
    return max, err
  }
  if a > b {
    max = a
    return max, nil
  }
  max = b
  return max, nil
}

3.3 省略声明

与参数的类型省略声明一样,当返回多个相同类型的返回值时,可以只声明一次即可,如下所示均为合法的函数声明

func get(arg1, arg2 int, arg3 float64) (res1, res2 int) {
  ...
}

func get(arg1, arg2, arg3 int, arg4, arg5, arg6 float64) (res1, res2 float64, res3 int) {
  ...
}

4. 包级函数

无论是自定义的函数,抑或是我们经常使用的Println函数,他们都属于一个包,因此称之为包级函数。我们知道在Java中通过关键字private、public来界定一个函数是私有函数、公有函数。Golang为我们提供了一种非常简单的方式来规定一个函数是私有函数还是公有函数——函数名首字母大小写

  1. 函数名首字母小写代表私有函数(如上述get、max、sum函数),只能在同一个包中才可以被调用;
  2. 函数名首字母大写代表公有函数(如我们经常使用的Println函数),不同的包可以调用。

5. 特殊的函数init

每个包可以有多个任意的init函数,这些init函数都会在main函数开始执行之前先被调用。因此,init函数经常用于初始化包、初始化变量或其他需要在程序运行前优先完成的工作。

5.1 init函数Demo

项目路径如下图所示:

GoProjects
  go.mod
  main.go
      
├─shaolin
      shaolin.go
      
└─wudang
        wudang.go

//wudang.go

package wudang

import "fmt"

func init() {
  fmt.Println("武当派男弟子已经init完毕")
}

func init() {
  fmt.Println("武当派女弟子已经init完毕")
}

func TaiJi() {
  fmt.Println("武当派弟子开始表演太极")
}

//shaolin.go

package shaolin

import (
  "GoProjects/wudang"
  "fmt"
)

func init() {
  fmt.Println("少林派弟子已经init完毕")
}

func ChangQuan() {
  wudang.TaiJi()
  fmt.Println("武当派表演完毕后,少林派弟子开始表演长拳")
}

//main.go

package main

import (
  "GoProjects/shaolin"
  "fmt"
)

func init() {
  fmt.Println("各位观众已经落座")
}

func see() {
  shaolin.ChangQuan()
}
func main() {
  see()
}

执行上述main函数,输出结果如下:

武当派男弟子已经init完毕
武当派女弟子已经init完毕
少林派弟子已经init完毕                 
各位观众已经落座                       
武当派弟子开始表演太极                 
武当派表演完毕后,少林派弟子开始表演长拳

5.2 init函数调用分析

Go语言先从main包开始检查其导入的包,发现导入了shaolin包,然后再检查shaolin包发现shaolin包导入了wudang包。Go编译器由此构建出一个包调用栈,如下所示。


在运行时,先执行所有的init()函数,且最后导入的包会最先执行其包内的init()函数,因此执行init()顺序如下

  • wudang包的第一个init
  • wudang包的第二个init
  • shaolin包的init
  • main包的init

运行完init()函数以后,按照调用栈执行业务代码,执行业务代码的顺序如下

  • wudang包的TaiJi()
  • shaolin包的ChangQuan()
  • main包的main()

6. 函数变量

函数可以赋值给变量、传递给其他函数、或者从其他函数中返回。

6.1 匿名函数&闭包

函数也是一中类型,可以赋值给函数变量,且函数变量可以像其他函数一样直接调用。比如:

func main() {
  max := func(a, b int) int {
    if a > b {
      return a
    }
    return b
  }
  fmt.Println(max(3, 8))
}

我们可以将一个匿名函数赋值给函数变量max,通过max可以实现对匿名函数的调用,上述代码最终运行结果是8,和正常函数的使用其实没有区别。匿名函数可以使在函数内部定义函数称为可能,且内部的匿名函数可以直接使用外部函数的变量,这种方式称之为闭包。

6.2 函数作为参数&回调

函数可以当作其它函数的参数进行传递,然后在其他函数内部可以执行该函数,一般称之为回调

func main() {
  threeSum(1, sum)
}

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

func threeSum(z int, f func(int, int) int) {
  fmt.Println(z + sum(1, 2))
}

如上述代码所示,将求两数之和的sum函数作为参数传递给求三数之和的threeSum函数,且在threeSum函数中可以直接调用执行sum函数。

6.3 函数作为返回值

如下所示,为一个函数作为返回值的例子

func main() {
  sum := returnFun()
  fmt.Println(sum(1, 2))
}

func returnFun() (sum func(int, int) int) {
  sum = func(x, y int) int {
    return x + y
  }
  return
}

从returnFun()函数的声明可以看出,该函数返回值为fun(int,int) int 类型,即返回一个函数sum。sum的参数为两个int,返回值也是一个int,且sum的功能为求两个数的和。

在main函数中首先调用returnFun()函数,将returnFun()函数的返回值赋值给函数变量sum,然后通过函数变量sum调用returnFun()函数返回的函数,执行结果为3。

在main函数中首先调用returnFun()函数,将returnFun()函数的返回值赋值给函数变量sum,然后通过函数变量sum调用returnFun()函数返回的函数,执行结果为3。


7. 错误处理

7.1 error

有一些函数总是成功返回,有一些函数不一定成功返回,比如I/O函数可能受到一些不可控的因素而面对可能的错误。这些错误不会影响到程序的整体运行。对于这种错误,Golang提供了error接口,函数可以返回error给其调用者,由调用者负责处理。

如下为error接口的源码,从error接口的源码可知

  • Error()用于返回错误信息
  • 当error为nil时,表示没有error
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
  Error() string
}

习惯上将error作为最后一个返回值返回,如下所示代码在缓存不命中的情况下通过fmt.Errorf()返回error

func cacheSearch(key int) (val int, err error) {
  //默认返回值
  val = -1
  if key != 1 {
    // fail
    return val, fmt.Errorf("key is not exist")
  }
  //习惯上先判断失败,处理成功的情况时不写入else里的
  val = 1
  return val, nil
}

7.1.1 error的处理方式

分享常用的3种error处理方式

1.将error传递下去,使得子例程中发生的错误变为主例程的错误。

resp, err := http.Get(url)
if err != nil{
  return nil, err
}

2.重试机制,超出一定的重试次数或限定时间后再报错退出。

func httpGet(url string) (resp interface{}, err error) {
  tries := 0
  for ; tries  5; tries++ {
    resp, err := http.Get(url)
    if err == nil {
      return resp, nil
    }
    log.Printf("server not respnding (%s); retrying...", err)
    tries++
  }
  return nil, fmt.Errorf("server %s failed to respond after %d tries", url, tries)
}

3.某些错误只需要记录,不影响程序继续执行

if err := Ping(); err != nil {
  log.Printf("ping failed: %v; networking disabled", err)
}

7.1.2 自定义error

内置的error接口返回的错误信息只能携带一个字符串,如果需要更加丰富的错误信息,则需要自定义error。自定义error需要实现内置的error接口,即实现error中的Error()即可。

如下为一个包含错误码的自定义接口myError

type myError struct {
   code int //错误码
   errMsg string //错误信息
}

//实现error接口
func (e *myError ) Error() string{
   return e.errMsg 
}

用上述接口改写cacheSearch函数

func cacheSearch(key int) (val int, err error) {
  //默认返回值
  val = -1
  if key != 1 {
    // fail
    return val, &myError{
      code:   500,
      errMsg: "key is not exist",
    }
  }
  //习惯上先判断失败,处理成功的情况时不写入else里的
  val = 1
  return val, nil
}

7.2 defer

函数中如果包含defer语句,那么该语句一定在函数执行结束之后执行。不论该函数是通过return正常结束,还是由于panic导致的异常结束。defer语句经常用于处理资源的释放(关闭文件、关闭连接、释放锁),因为不论函数逻辑多么的复杂,defer都能执行,所以可以保证在任何执行路径下,资源得到有效释放。

如下所示为Golang os包的ReadFile函数,该函数可以打开一个文件并返回文件的内容,函数中的defer f.Close()可以保证ReadFile函数执行结束后一定执行f.Close(),保证文件资源一定会被释放。

// ReadFile reads the named file and returns the contents.
// A successful call returns err == nil, not err == EOF.
// Because ReadFile reads the whole file, it does not treat an EOF from Read
// as an error to be reported.
func ReadFile(name string) ([]byte, error) {
  f, err := Open(name)
  if err != nil {
    return nil, err
  }
  defer f.Close()

  var size int
  if info, err := f.Stat(); err == nil {
    size64 := info.Size()
    if int64(int(size64)) == size64 {
      size = int(size64)
    }
  }
  size++ // one byte for final read at EOF

  // If a file claims a small size, read at least 512 bytes.
  // In particular, files in Linux's /proc claim size 0 but
  // then do not work right if read in small pieces,
  // so an initial read of 1 byte would not work correctly.
  if size  512 {
    size = 512
  }

  data := make([]byte, 0, size)
  for {
    if len(data) >= cap(data) {
      d := append(data[:cap(data)], 0)
      data = d[:len(data)]
    }
    n, err := f.Read(data[len(data):cap(data)])
    data = data[:len(data)+n]
    if err != nil {
      if err == io.EOF {
        err = nil
      }
      return data, err
    }
  }
}

7.3 panic与recover

Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起panic异常。不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;

当panic发生时,程序会中断运行并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。panic value是错误描述,堆栈跟踪信息可以为我们定位问题代码提供依据。

func main() {
  defer fmt.Println("defer机制启动")

  fmt.Println("先做一些事情")

  panic("发生了panic")
}

上述代码,运行结果为:

先做一些事情
defer机制启动
panic: 发生了panic

goroutine 1 [running]:
main.main()
        /Users/xxx/GolandProjects/hello/defer.go:12 +0xb8

从运行结果可以看出,运行顺序为

  • 先执行正常的逻辑
  • 然后发生panic之前立即启动defer机制
  • 最后中断程序,并输出panic value和堆栈调用信息

panic会引起程序的崩溃,而我们一般认为程序崩溃是一种bug,因此在实际开发中很少主动使用panic,常用的是Go提供的错误机制error。任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该使用Go的错误机制error优雅的处理r。

通常情况下,我们不对panic异常做任何处理,但是在某些特殊的情况下我们希望从panic中恢复出来。比如当web服务器崩溃时,我们希望在崩溃前将所有的连接关闭,否则客户端会一直处于等待状态,占用资源。Go语言提供了recover机制帮助我们从panic中恢复出来。

在panic异常崩溃的时候,只有被defer修饰的语句才能被执行,因此处理panic异常,需要通过defer+匿名函数+recover实现。如下述例子所示

func main() {
   defer func() {
      if p:=recover();p!=nil{
         fmt.Println(p)
      }
   }()
   panic("发生了panic")
}

运行上述代码,输出为:

发生了panic

从上述代码运行结果可知,recover的返回值p其实就是传入panic函数里的参数

7.4 总结

在error、panic两种异常处理机制中,Go语言更提倡error这样的轻量级错误,而不是panic,因为panic会直接导致程序崩溃。


8.参考

《Go语言圣经中文版》books.studygolang.com/g

《Go语言实战》William Kennedy,Brian Ketelsen,Erik St. Martin著

文章来源于互联网:Golang分享(三):“一等公民”函数

打赏 赞(0) 分享'
分享到...
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

文章目录