• 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返回;

  • 相关阅读:
    DNS记录类型介绍(A记录、MX记录、NS记录等)
    mybatis调用视图和存储过程
    项目里总结出来的log4j模板
    shiro的使用2 灵活使用shiro的密码服务模块
    反射获取对象的属性值
    MySQL 性能调优之查询优化
    利用BeanUtils在对象间复制属性
    这里整理了基于java平台的常用资源
    各种编程语言如何杀死一条龙营救公主!
    Eclipse vs. IDEA快捷键对比大全
  • 原文地址:https://www.cnblogs.com/chengmuyu/p/13487169.html
Copyright © 2020-2023  润新知