go基础4-函数

函数

函数:将业务拆分为小单元,便于重复调用.隐藏实现细节.

函数声明

函数声明:函数名,形式参数列表,返回值列表(可省略),函数体;

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

形参由调用方提供,返回值省略表示没有返回值.返回值也可以像形参一样被命名,这时,返回值将被声明为局部变量,根据声明类型进行零值初始化.
形参和返回值是否命名不影响函数实际运行;
函数调用必须按声明顺序提供实参,go中不提供默认参数值,不允许通过参数名指定形参;
形参和有名返回值作为函数最外层的局部变量,被存储在相同的语法块中;
实参通过值的方式传递,因此函数的形参是实参的拷贝.对形参修改不影响实参.但可以通过引用类型的间接引用来修改实参.引用类型包括:指针,slice,map,function,channel等;

递归

递归:函数可以自己间接或直接的调用自身的行为.递归有利于简化代码,实现抽象业务模型.

go使用可变栈,栈的大小按需增加,不用考虑递归栈溢出和安全问题.

func outline(stack []string, n *html.Node) {
   if n.Type == html.ElementNode {
       stack = append(stack, n.Data) // push tag
       fmt.Println(stack)
    }
      for c := n.FirstChild; c != nil; c = c.NextSibling {
        outline(stack, c)
    }
}

多返回值

go中,一个函数可以返回多个值.

links, err := findLinks(url)

调用多返回值函数时,返回给调用者的是一组值,调用者必须显示的将这些值分配给变量.

links, _ := findLinks(url) // errors ignored

不被使用的值使用_接收

良好的命名很重要.准确的变量名可以传达函数返回值的含义,尤其是相同返回值类型时.如果所有的返回值都有显示命名,则该函数的return语句可以省略操作数,这称为bare return

func CountWordsAndImages(url string) (words, images int, err error) {
    resp, err := http.Get(url)
    if err != nil {
        return
    }
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    if err != nil {
        err = fmt.Errorf("parsing HTML: %s", err)
        return
    }
}

上面每个return等价于:return words, images, err.但是这种写法如果业务复杂时代码不直观,会增加出错几率.

错误

go的异常分为两种:

panic异常和Error错误.    

错误:可能出错的地方出问题叫错误.如打开一个文件,这个文件可能不存在;
异常:不应该出问题的地方出问题.如空指针,数组角标越界;
错误和异常的判断标准就是意料之中还是意料之外.错误在所难免,也因此错误是程序的重要组成部分,程序运行失败仅被认为是几个预期的结果之一,而异常不是;
go中error接口类型是错误处理的标准模式,如果函数要返回错误,必须要声明error;
panic和recover来触发和终止异常处理流程,defer用于延迟执行它之后的内容;
panic异常会造成程序运行中断,停止业务执行.良好设计的程序不应该出现panic异常.实际go推荐在业务处理中通过error来处理非预期值情况,让panic处理不可控异常;当然这种异常处理方式,需要在代码中显示声明error处理,这也是大家比较诟病的地方,因为显示处理基本占了一半的代码量.

异常情形:

  1. 特殊情形.函数根据运行结果返回额外的的布尔值判断执行是否成功,如果为false则表示运行失败,但是这种情形一般不归类为异常,因为失败是在意料中的;
  2. 不影响业务执行的异常error.发生异常的情况下,额外值将会是error类型,便于问题定位.error类型为nil表示函数运行成功,non-nil表示失败;
    non-nil情况下,其他返回值将是undefined未定义类型的值,这些值少数情况下可以起到数据定位的作用.
  3. panic异常.运行中的错误信息go中认为是预期中的值而不是异常,只有未被意料到的错误才是go认为的异常,触发它的异常机制.如果将本身你就可以预知的错误返回给用户,繁复的堆栈信息反而让用户不知所措.

错误处理方式:

  1. 传播.最常用方式,异常逐级上报,调用方进行处理;
  2. 偶然性异常进行重试;
  3. 输出错误信息并结束程序.适合程序无法继续运行时或会造成程序内部不一致时;
  4. 非致命异常,只是日志输出,提示异常信息;
  5. 忽略异常;不影响业务和使用;

文件结尾错误(EOF):io包保证任何由文件结束引起的读取失败都返回的错误.它是一种固定的错误信息.

函数值

go中函数是一等值(first-class values):函数像其他值一样,有类型,可以赋值给变量,传递给函数,从函数中返回;函数值的调用类似函数调用.

函数值之间不能比较,所以不能用作map的key;
函数值不仅可以通过数据来参数化函数,也可通过行为;

func square(n int) int { return n * n }
f := square
fmt.Println(f(3))

匿名函数

函数表达式要求拥有函数名的函数只能在包级语法块中被声明.因此我们如果不声明函数名,那么函数就可以被定义在其他地方,而没有函数名的函数称为匿名函数.
匿名函数可以访问完整的词法环境,即函数中定义的内部函数可以引用该函数的变量.

// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}
func main() {
    f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
}

squares中定义的匿名内部函数可以访问和更新squares的局部变量.这意味着匿名函数和squares中存在变量引用,这就是函数值属于引用类型和函数值不能比较的原因.Go使用闭包技术实现函数值,因为函数值也叫做闭包.
因此我们发现变量的生命周期不由它的作用域决定,外部引用可以改变变量声明周期,这种情况我们称为变量逃逸.
循环操作要注意变量值的引用,实际函数值的执行会在循环结束后执行.

var rmdirs []func()
for _, dir := range tempDirs() {
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir) // NOTE: incorrect!
    })
}

这段代码想先新增目录然后删除.但是该代码无法删除目录.原因在于循环变量的作用域.for循环语句的作用域中,所有函数值共享循环变量,这也就意味着,当循环结束时,dir目录存储的值才是实际被执行的值,造成循环内每次都删除同一个值.

可变参数

可变参数函数:参数数量可变的函数.使用省略符号"..."表示该函数可接收任意数量该类型的参数.

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

执行过程,调用者会隐式创建一个数组,将原始数据复制到数组中,再把数组的一个切片作为参数传给被调用函数.如果调用者将切片传给可变参数函数,需要在最后一个参数上加上省略符.

fmt.Println(sum())
fmt.Println(sum(1, 2, 3, 4))
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // 切片类型

可变参数函数常用于格式化字符串.interface{}表示函数的最后一个参数可以接收任意类型

deferred函数

deffered函数用于保障函数执行结束时,才执行的处理操作.

defer resp.Body.Close()

用法是只需要在普通函数或方法前加上defer关键字.函数直到结束时才会执行defer语句内容,无论函数是否正常结束都会执行,类似java的finally语句.多条defer声明,声明顺序与执行顺序相反.
通常defer被用于处理成对的操作,如打开,关闭;连接,断开连接;加锁和释放锁;
defer还用于记录函数进入和退出时点.

defer trace("bigSlowOperation")()

后面的括号是进入时触发标记.
defer和匿名函数结合,可以读取和修改函数的返回值;

Panic异常

panic异常是对程序运行有重大影响,应让程序中断的异常.这包括影响程序执行的情形以及未预期异常如数组访问越界,空指针引用等;panic异常发生时,程序会中断运行,并立即执行该goroutine中被延迟的函数(deferred函数).随后,程序崩溃并输出日志信息.
日志信息包括:panic value(异常信息)和函数调用堆栈跟踪信息;
go的异常处理很简单,由于panic异常会造成程序崩溃,所以除非严重错误,如程序内部逻辑不一致.其他的都应该有error错误机制处理.
panic机制中,延迟函数(defer后函数)的调用在释放堆栈信息之前.可以利用这个特点在退出时输出异常信息;

  1. Error类型使用建议
    1. 失败原因只有一个时,使用boolean,而不是error
    2. 没有失败不使用error
    3. error放在返回值列表最后
    4. 统一定义错误值,避免管理混乱
    5. 逐层传递时,层层都加日志,便于追踪
    6. 错误处理使用defer
    7. 增加重试机制
    8. 按需返回错误
    9. 发生错误时,不忽略有用的返回值
  2. Panic使用建议
    1. 开发中,快速失败
    2. 生产中,避免panic
    3. 实际影响业务执行的错误,使用panic处理

如何正确地 抛出 错误 和 异常(error/panic/recover)?

Recover捕获异常

panic异常发生时会造成系统崩溃,这时必须在退出前关闭所有连接,否则客户端会一直处于等待状态;
同时业务上并不是所有panic都要让程序崩溃.
所以defer和recover搭配可以解决panic异常中恢复的问题.

在deferred函数中调用内置函数recover,如果发生panic异常,recover会使程序从panic中恢复,并返回panic value.
导致panic异常的函数不会继续运行,但能正常返回.在未发生panic时调用recover,返回nil.

func Parse(input string) (s *Syntax, err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p)
        }
    }()
    // ...parser...
}

该方法用于文本解析,如果发生panic异常,会将panic value赋值给error返回.

不能武断的将所有panic都恢复.因为panic之后,无法保证包级变量的状态仍然和预期一致.
如:数据结构更新不完善;文件或网络连接未关闭;锁未释放;

panic使用原则:
共有api应将函数运行失败作为error返回,而不是panic;
不应该恢复其他包引起的panic;
不应该恢复别人开发的函数引起的panic;
选择性进行recover.将应该恢复的panic,将它的panic value设置成特殊类型,转换为error返回;

posted @ 2020-08-11 22:56  橙木鱼  阅读(129)  评论(0编辑  收藏  举报