您的位置:首页 > 编程语言 > Go语言

Go语言学习笔记 - 第五章 函数(The Go Programming Language)

2020-01-13 06:07 471 查看

第五章 函数

5.1函数声明

划重点

  • 函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func name(parameter-list) (result-list) {
body
}
  • 形参为局部变量
  • 没有返回值或者一个无名变量时,返回的括号可以省略。
  • 返回值也可以像形式参数一样被命名,并被声明成一个局部变量。
  • 如果形参或返回值有相同的类型,参数类型可以省略
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
  • 函数的类型被称为函数的标识符
  • Go语言没有默认参数值
  • 函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。
  • 实参通过值的方式传递,因此函数的形参是实参的拷贝
  • 对形参进行修改不会影响实参。引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会被修改
  • 没有函数体的函数声明,这表示该函数不是以Go实现的
package math
func Sin(x float64) float //implemented in assembly language

5.2递归

划重点

  • golang.org/x/… 目录下存储了一些由Go团队设计、维护,对网络编程、国际化文件处理、移动平台、图像处理、加密解密、开发者工具提供支持的扩展包
  • 大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等,递归深度会导致栈溢出。
  • Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。

常用库及方法

  • golang.org/x/net/html
  • html.Parse
    html.ElementNode
    html.Node.FirstChild
    html.Node.NextSibling
    html.NewTokenizer

5.3多返回值

划重点

  • 在Go中,一个函数可以返回多个值
  • 许多标准库中的函数返回2个值,一个是期望得到的返回值,另一个是函数出错时的错误信息
  • 通过
    fmt.Errorf(§7.8)
    输出详细的错误信息
  • resp.Body
    需要确保被关闭,释放网络资源。虽然Go的垃圾回收机制会回收不被使用的内存,但是这不包括操作系统层面的资源,比如打开的文件、网络连接。因此我们必须显式的释放这些资源。
  • 函数调用者必须显式的将返回值分配给变量
  • 对于可以接受多参数的函数,可将多返回值的函数作为该函数的参数
  • bare return
    返回值除了类型还带有变量名,那么该函数的
    return
    语句可以省略操作数。

常用库及方法

5.4错误

划重点

  • panic
    是来自被调函数的信号,表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。
  • 通过最后一个返回值,来传递错误信息。如果错误原因只有一个,可以设置为布尔值,这时可命名为ok
  • 内置的
    error
    是接口类型
  • error
    类型可能是
    nil
    或者
    non-nil
    error
    Error
    可以打印错误
    string
  • Go有别于那些将函数运行失败看作是异常(exception)的语言,这些语言会将错误当做异常处理,并输出堆栈信息,无法帮助定位错误。
  • Go使用控制流机制(如if和return)处理异常

常用库及方法

  • strings.Contains
  • strconv.FormatBool
  • time.Date
    time.Time
  • cache.Lookup
  • error.Error

5.4.1错误处理策略

划重点

  • 常用的5中处理错误的方式 传播错误,调用者直接返回被调用函数的返回值
  • 偶发性或者不可预知的错误,使用重新尝试失败的操作
  • 输出错误信息并结束程序,
    os.Exit()
    ,注意该策略只应在
    main
    中执行
  • 输出错误信息,不中断程序,使用
    log
    包或
    fmt.Fprintf(os.Stderr...
  • 直接忽略掉错误
  • fmt.Errorf
    函数使用
    fmt.Sprintf
    格式化错误信息并返回
  • log
    包中的所有函数会为没有换行符的字符串增加换行符
  • 常用库及方法

    • fmt.Errorf
    • os.Exit()
      os.RemoveAll
    • log.Fatalf
      log.SetPrefix()
      log.SetFlags()
    • ioutil.TempDir

    5.4.2文件结尾错误(EOF)

    划重点

    • io
      包中任何由文件结束引起的读取失败都返回同一个错误——
      io.EOF

    5.5函数值

    划重点

    • 函数可以看做是一种值,第一类值(first-class values)
    • 函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回
    • 函数类型的零值是nil。调用值为nil的函数值会引起panic错误
    • 函数值可以与nil比较
    • 函数值之间是不可比较的,也不能用函数值作为map的key

    常用库及方法

    • strings.Map

    5.6 匿名函数

    划重点

    • 匿名函数存在变量引用,这是函数值属于引用类型和函数值不可比较的原因。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。
    • 当匿名函数需要被递归调用时,必须首先声明一个变量,再将匿名函数赋值给这个变量,必须分两步,无法直接绑定,无法递归调用。
    visitAll := func(items []string) {
    // ...
    visitAll(m[item]) // compile error: undefined: visitAll
    // ...
    }
    • append
      的参数“
      f(item)...
      ”,会将
      f
      返回的一组元素一个个添加到
      worklist
      中。
      worklist = append(worklist, f(item)...)

    常用库及方法

    • sort.Strings
    • http.StatusOK
    • resp, err := http.Get(url)
      resp.Request.URL.Parse

    5.6.1 警告:捕获迭代变量

    划重点

    • 这是,Go词法作用域的一个陷阱。
    • 下面的例子是错误的,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值,for循环已完成,dir中存储的值等于最后一次迭代的值。
    var rmdirs []func()
    for _, dir := range tempDirs() {
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
    os.RemoveAll(dir) // NOTE: incorrect!
    })
    }

    解决这个问题可以:

    for _, dir := range tempDirs() {
    dir := dir // declares inner dir, initialized to outer dir
    // ...
    }
    • 如果你使用go语句或者defer语句会经常遇到此类问题

    5.7可变参数

    划重点

    • 参数数量可变的函数称为为可变参数函数,典型的是
      fmt.Printf
    • 在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。
    • 调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调函数
    • 如果原始参数已经是切片类型,只需在最后一个参数后加上省略符
      ...
      ,下面的调用是一致的:
    fmt.Println(sum(1, 2, 3, 4)) // "10"
    //----
    values := []int{1, 2, 3, 4}
    fmt.Println(sum(values...)) // "10"
    • 可变参数函数和以切片作为参数的函数是不同的,虽然看起来类似
    func f(...int) {}
    func g([]int) {}
    fmt.Printf("%T\n", f) // "func(...int)"
    fmt.Printf("%T\n", g) // "func([]int)"

    5.8Deferred函数

    划重点

    • resp.Body.close
      关闭网络连接
    • defer
      用于延迟执行操作,具有以下特性:
      defer
      后面的函数会被延迟执行
    • defer
      后的函数会在包含该defer的函数执行完毕执行
    • 无论
      return
      panic
      defer
      都会执行
    • 一个函数中可以有多个
      defer
      ,执行顺序和声明顺序相反
  • defer
    语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁
  • 调试复杂程序时,
    defer
    机制也常被用于记录何时进入和退出函数
  • 一个有意思的用法defer trace(“bigSlowOperation”)():
  • func bigSlowOperation() {
    defer trace("bigSlowOperation")() // don't forget the extra >parentheses
    // ...lots of work…
    time.Sleep(10 * time.Second) // simulate slow operation by >sleeping
    }
    
    func trace(msg string) func() {
    start := time.Now()
    log.Printf("enter %s", msg)
    return func() {
    log.Printf("exit %s (%s)", msg,time.Since(start))
    }
    }
    • 因为
      defer
      return
      后执行,所以可以通过匿名函数拿到包括返回值在内的所有变量的值。
    • 被延迟执行的匿名函数甚至可以修改函数返回给调用者的返回值
    • 在循环体中的
      defer
      语句需要注意,他们不会在循环体中针对某一次循环执行,而是在整个函数执行完毕后,才会执行。比较有效的做法是把其中的逻辑独立出来形成一个函数。
    • 在关闭文件时,没有对
      f.close
      采用
      defer
      机制,因为这会产生一些微妙的错误。许多文件系统,尤其是NFS,写入文件时发生的错误会被延迟到文件关闭时反馈。如果没有检查文件关闭时的反馈信息,可能会导致数据丢失,而我们还误以为写入操作成功。
      如果要使用
      defer f.close()
      ,可以使用上面的通过
      defer 匿名函数
      的方式,在defer里面改写返回的
      error
      信息
      defer func() {
      // Close file, but prefer error from Copy, if any.
      if closeErr := f.Close(); err == nil {
      err = closeErr
      }
      }()

    常用库及方法

    • resp.Header.Get("Content-Type")
      resp.Body.close
    • strings.HasPrefix
    • sync.Mutex
      sync.Mutex.Lock
      sync.Mutex.Unlock
    • resp.Request.URL.Path
    • path.Base
    • os.Create
      f.Close()
    • io.Copy

    5.9Panic异常

    划重点

    • panic
      异常是指只能在运行时检查,如数组访问越界、空指针引用等
    • panic
      异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制),程序崩溃并输出日志信息
    • 日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。
    • panic异常除了来自运行时,也可以直接调用内置的panic函数,panic函数接受任何值作为参数
    • Go的panic机制类似于其他语言的异常,但panic的适用场景有一些不同。golang建议尽量使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。
    • 为了方便诊断问题,runtime包允许程序员输出堆栈信息
    • 在Go的panic机制中,延迟函数的调用在释放堆栈信息之前

    常用库及方法

    • panic()
    • regexp.Compile
      regexp.MustCompile
    • template.Must
    • runtime.Stack
      os.Stdout.Write

    5.10Recover捕获异常

    划重点

    • 对panic正常不应该做处理,但有时可以从异常panic中恢复以帮助在程序崩溃前做一些操作。比如,web服务器可以在崩溃前关闭连接。
    • 如果要恢复
      panic
      ,比如在deferred函数中调用了内置函数
      recover
      ,并且定义该
      defer
      语句的函数发生了
      panic
      异常,
      recover
      会使程序从
      panic
      中恢复,并返回
      panic value
      。导致
      panic
      异常的函数不会继续运行,但能正常返回。在未发生
      panic
      时调用
      recover
      recover
      会返回
      nil
    • panic
      异常的恢复处理要慎重,
      panic
      后的恢复无法保证包级变量不出问题。。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。
    • 不该试图去恢复其他包引起的panic;
    • 公有的API应该将函数的运行失败作为error返回,而不是panic
    • 不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的
    • 有时我们很难完全遵循规范,举个例子,net/http包中提供了一个web服务器,不能因为某个用户的处理Handler函数引发的panic异常,杀掉整个进程;web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。
    • 有些情况下,我们无法恢复。某些致命错误会导致Go在运行时终止程序,如内存不足。

    常用库及方法

    • recover()
    • 点赞
    • 收藏
    • 分享
    • 文章举报
    rabbit0206 发布了13 篇原创文章 · 获赞 3 · 访问量 132 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: