《Go 语言第一课》课程学习笔记(十二)

函数

Go 函数与函数声明

  • 在 Go 语言中,函数是唯一一种基于特定输入,实现特定任务并可返回任务执行结果的代码块(Go 语言中的方法本质上也是函数)。
  • 在 Go 中,我们定义一个函数的最常用方式就是使用函数声明。
    在这里插入图片描述
    • 第一部分是关键字 func,Go 函数声明必须以关键字 func 开始。
    • 第二部分是函数名。
      • 函数名是指代函数定义的标识符,函数声明后,我们会通过函数名这个标识符来使用这个函数。
      • 在同一个 Go 包中,函数名应该是唯一的。
      • 首字母大写的函数名指代的函数是可以在包外使用的,小写的就只在包内可见。
    • 第三部分是参数列表。
      • 参数列表中声明了我们将要在函数体中使用的各个参数。
      • 参数列表紧接在函数名的后面,并用一个括号包裹。
      • 它使用逗号作为参数间的分隔符,而且每个参数的参数名在前,参数类型在后,这和变量声明中变量名与类型的排列方式是一致的。
      • Go 函数支持变长参数,也就是一个形式参数可以对应数量不定的实际参数。
    • 第四部分是返回值列表。
      • 返回值承载了函数执行后要返回给调用者的结果,返回值列表声明了这些返回值的类型,返回值列表的位置紧接在参数列表后面,两者之间用一个空格隔开。
      • Fprintf 函数的返回值列表不仅声明了返回值的类型,还声明了返回值的名称,这种返回值被称为具名返回值。
      • 多数情况下,我们不需要这么做,只需声明返回值的类型即可。
    • 最后,放在一对大括号内的是函数体,函数的具体实现都放在这里。
      • 不过,函数声明中的函数体是可选的。
      • 如果没有函数体,说明这个函数可能是在 Go 语言之外实现的,比如使用汇编语言实现,然后通过链接器将实现与声明中的函数名链接到一起。
  • 函数声明中的函数名其实就是变量名,函数声明中的 func 关键字、参数列表和返回值列表共同构成了函数类型。而参数列表与返回值列表的组合也被称为函数签名,它是决定两个函数类型是否相同的决定因素。因此,函数类型也可以看成是由 func 关键字与函数签名组合而成的。
    • 通常,在表述函数类型时,我们会省略函数签名参数列表中的参数名,以及返回值列表中的返回值变量名。
      func(io.Writer, string, ...interface{}) (int, error)
      
    • 如果两个函数类型的函数签名是相同的,即便参数列表中的参数名,以及返回值列表中的返回值变量名都是不同的,那么这两个函数类型也是相同类型。
    • 每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例。
    • 函数字面值由函数类型与函数体组成,它特别像一个没有函数名的函数声明,因此我们也叫它匿名函数。

函数参数的那些事儿

  • 函数参数列表中的参数,是函数声明的、用于函数体实现的局部变量。
    • 由于函数分为声明与使用两个阶段,在不同阶段,参数的称谓也有不同。
    • 在函数声明阶段,我们把参数列表中的参数叫做形式参数(Parameter,简称形参),在函数体中,我们使用的都是形参;
    • 而在函数实际调用时传入的参数被称为实际参数(Argument,简称实参)。
      在这里插入图片描述
  • Go 语言中,函数参数传递采用是值传递的方式。
    • 所谓“值传递”,就是将实际参数在内存中的表示逐位拷贝(Bitwise Copy)到形式参数中。
    • 对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。
    • 但是像 string、切片、map 这些类型就不是了,它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为“浅拷贝”。
    • 不过函数参数的传递也有两个例外,当函数的形参为接口类型,或者形参是变长参数时,简单的值传递就不能满足要求了,这时 Go 编译器会介入:对于类型为接口类型的形参,Go 编译器会把传递的实参赋值给对应的接口类型形参;对于为变长参数的形参,Go 编译器会将零个或多个实参按一定形式转换为对应的变长形参。
    • 在 Go 中,变长参数实际上是通过切片来实现的。所以,我们在函数体中,就可以使用切片支持的所有操作来操作变长参数,这会大大简化了变长参数的使用复杂度。

函数支持多返回值

  • **和其他主流静态类型语言,比如 C、C++ 和 Java 不同,Go 函数支持多返回值。**多返回值可以让函数将更多结果信息返回给它的调用者,Go 语言的错误处理机制很大程度就是建立在多返回值的机制之上的。
  • 函数返回值列表从形式上看主要有三种:
    func foo() // 无返回值
    func foo() error // 仅有一个返回值
    func foo() (int, string, error) // 有2或2个以上返回值
    
    • 如果一个函数没有显式返回值,那么我们可以像第一种情况那样,在函数声明中省略返回值列表。
    • 而且,如果一个函数仅有一个返回值,那么通常我们在函数声明中,就不需要将返回值用括号括起来,如果是 2 个或 2 个以上的返回值,那我们还是需要用括号括起来的。
    • Go 标准库以及大多数项目代码中的函数,都选择了使用普通的非具名返回值形式。但在一些特定场景下,具名返回值也会得到应用。比如,当函数使用 defer,而且还在 defer 函数中修改外部函数返回值时,具名返回值可以让代码显得更优雅清晰。

函数是“一等公民”

  • 函数在 Go 语言中属于“一等公民(First-Class Citizen)”。
    • 特征一:Go 函数可以存储在变量中。
    • 特征二:支持在函数内创建并通过返回值返回。
      • Go 函数不仅可以在函数外创建,还可以在函数内创建。
      • 而且由于函数可以存储在变量中,所以函数也可以在创建后,作为函数返回值返回。
      • 闭包本质上就是一个匿名函数或叫函数字面值,它们可以引用它的包裹函数,也就是创建它们的函数中定义的变量。然后,这些变量在包裹函数和匿名函数之间共享,只要闭包可以被访问,这些共享的变量就会继续存在。
    • 特征三:作为参数传入函数。
    • 特征四:拥有自己的类型。
      // $GOROOT/src/net/http/server.go
      type HandlerFunc func(ResponseWriter, *Request)
      // $GOROOT/src/sort/genzfunc.go
      type visitFunc func(ast.Node) ast.Visitor
      

Go 语言是如何进行错误处理的?

  • Go 函数增加了多返回值机制,来支持错误状态与返回信息的分离,并建议开发者把要返回给调用者的信息和错误状态标识,分别放在不同的返回值中。
  • 虽然,在 Go 语言中,我们依然可以像传统的 C 语言那样,用一个整型值来表示错误状态,但 Go 语言惯用法,是使用 error 这个接口类型表示错误,并且按惯例,我们通常将 error 类型返回值放在返回值列表的末尾。

error 类型与错误值构造

  • error 接口是 Go 原生内置的类型,它的定义如下:
    // $GOROOT/src/builtin/builtin.go
    type interface error {
    	Error() string
    }
    
    • 任何实现了 error 的 Error 方法的类型的实例,都可以作为错误值赋值给 error 接口变量。
    • 那这里,问题就来了:难道为了构造一个错误值,我们还需要自定义一个新类型来实现 error 接口吗?
    • Go 语言的设计者显然也想到了这一点,他们在标准库中提供了两种方便 Go 开发者构造错误值的方法: errors.New 和 fmt.Errorf。
      err := errors.New("your first demo error")
      errWithCtx = fmt.Errorf("index %d is out of bounds", i)
      
    • 这两种方法实际上返回的是同一个实现了 error 接口的类型的实例,这个未导出的类型就是 errors.errorString,它的定义是这样的:
      // $GOROOT/src/errors/errors.go
      type errorString struct {
      	s string
      } 
      func (e *errorString) Error() string {
      	return e.s
      }
      
    • 大多数情况下,使用这两种方法构建的错误值就可以满足我们的需求了。虽然这两种构建错误值的方法很方便,但它们给错误处理者提供的错误上下文(Error Context)只限于以字符串形式呈现的信息,也就是 Error 方法返回的信息。
    • 但在一些场景下,错误处理者需要从错误值中提取出更多信息,帮助他选择错误处理路径,显然这两种方法就不能满足了。这个时候,我们可以自定义错误类型来满足这一需求。
      // $GOROOT/src/net/net.go
      type OpError struct {
      	Op string
      	Net string
      	Source Addr
      	Addr Addr
      	Err error
      }
      

Go 语言的几种错误处理的惯用策略

  • 策略一:透明错误处理策略
    • Go 语言中的错误处理,就是根据函数 / 方法返回的 error 类型变量中携带的错误值信息做决策,并选择后续代码执行路径的过程。
    • 最简单的错误策略莫过于完全不关心返回错误值携带的具体上下文信息,只要发生错误就进入唯一的错误处理执行路径:
      err := doSomething()
      if err != nil {
      	// 不关心err变量底层错误值所携带的具体上下文信息
      	// 执行简单错误处理逻辑并返回
      	... ...
      	return err
      }
      
    • 这也是 Go 语言中最常见的错误处理策略,80% 以上的 Go 错误处理情形都可以归类到这种策略下。
  • 策略二:“哨兵”错误处理策略
    • 当错误处理方不能只根据“透明的错误值”就做出错误处理路径选取的情况下,错误处理方会尝试对返回的错误值进行检视,于是就有可能出现下面代码中的反模式:
      data, err := b.Peek(1)
      if err != nil {
      	switch err.Error() {
      	case "bufio: negative count":
      		// ... ...
      		return
      	case "bufio: buffer full":
      		// ... ...
      		return
      	case "bufio: invalid use of UnreadByte":
      		// ... ...
      		return
      	default:
      		// ... ...
      		return
      	}
      }
      
    • 反模式就是,错误处理方以透明错误值所能提供的唯一上下文信息(描述错误的字符串),作为错误处理路径选择的依据。但这种“反模式”会造成严重的隐式耦合。这也就意味着,错误值构造方不经意间的一次错误描述字符串的改动,都会造成错误处理方处理行为的变化,并且这种通过字符串比较的方式,对错误值进行检视的性能也很差。
    • Go 标准库采用了定义导出的(Exported)“哨兵”错误值的方式,来辅助错误处理方检视(inspect)错误值并做出错误处理分支的决策。
      // $GOROOT/src/bufio/bufio.go
      var (
      	ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
      	ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
      	ErrBufferFull = errors.New("bufio: buffer full")
      	ErrNegativeCount = errors.New("bufio: negative count")
      )
      data, err := b.Peek(1)
      if err != nil {
      	switch err {
      	case bufio.ErrNegativeCount:
      		// ... ...
      		return
      	case bufio.ErrBufferFull:
      		// ... ...
      		return
      	case bufio.ErrInvalidUnreadByte:
      		// ... ...
      		return
      	default:
      		// ... ...
      		return
      	}
      }
      
  • 策略三:错误值类型检视策略
    • 由于错误值都通过 error 接口变量统一呈现,要得到底层错误类型携带的错误上下文信息,错误处理方需要使用 Go 提供的类型断言机制(Type Assertion)或类型选择机制(Type Switch),这种错误处理方式,称之为错误值类型检视策略。
    • 一般自定义导出的错误类型以XXXError的形式命名。和“哨兵”错误处理策略一样,错误值类型检视策略,由于暴露了自定义的错误类型给错误处理方,因此这些错误类型也和包的公共函数 / 方法一起,成为了 API 的一部分。
    • 一旦发布出去,开发者就要对它们进行很好的维护。而它们也让使用这些类型进行检视的错误处理方对其产生了依赖。
    • 标准库 errors 包提供了As函数给错误处理方检视错误值。As函数类似于通过类型断言判断一个 error 类型变量是否为特定的自定义错误类型。
  • 策略四:错误行为特征检视策略
    • 在 Go 标准库中,我们发现了这样一种错误处理方式:将某个包中的错误类型归类,统一提取出一些公共的错误行为特征,并将这些错误行为特征放入一个公开的接口类型中。这种方式也被叫做错误行为特征检视策略。
    • 错误处理方只需要依赖这个公共接口,就可以检视具体错误值的错误行为特征信息,并根据这些信息做出后续错误处理分支选择的决策。

怎么让函数更简洁健壮?

  • 健壮性的“三不要”原则
    • 原则一:不要相信任何外部输入的参数。
      • 为了保证函数的健壮性,函数需要对所有输入的参数进行合法性的检查。
      • 一旦发现问题,立即终止函数的执行,返回预设的错误值。
    • 原则二:不要忽略任何一个错误。
      • 在我们的函数实现中,也会调用标准库或第三方包提供的函数或方法。
      • 对于这些调用,我们不能假定它一定会成功,我们一定要显式地检查这些调用返回的错误值。
      • 一旦发现错误,要及时终止函数执行,防止错误继续传播。
    • 原则三:不要假定异常不会发生。
      • 异常不是错误。错误是可预期的,也是经常会发生的,我们有对应的公开错误码和错误处理预案,但异常却是少见的、意料之外的。
      • 虽然异常发生是“小众事件”,但是我们不能假定异常不会发生。所以,函数设计时,我们就需要根据函数的角色和使用场景,考虑是否要在函数内设置异常捕捉和恢复的环节。

Go 函数的异常处理设计

  • 在 Go 语言中,异常这个概念由 panic 表示。
    • panic 指的是 Go 程序在运行时出现的一个异常情况。
    • 如果异常出现了,但没有被捕获并恢复,Go 程序的执行就会被终止,即便出现异常的位置不在主 Goroutine 中也会这样。
  • 在 Go 中,panic 主要有两类来源,一类是来自 Go 运行时,另一类则是 Go 开发人员通过 panic 函数主动触发的。
    • 无论是哪种,一旦 panic 被触发,后续 Go 程序的执行过程都是一样的,这个过程被 Go 语言称为 panicking。
    • 当函数 F 调用 panic 函数时,函数 F 的执行将停止。不过,函数 F 中已进行求值的 deferred 函数都会得到正常执行,执行完这些 deferred 函数后,函数 F 才会把控制权返还给其调用者。
    • 对于函数 F 的调用者而言,函数 F 之后的行为就如同调用者调用的函数是 panic 一样,该 panicking过程将继续在栈上进行下去,直到当前 Goroutine 中的所有函数都返回为止,然后 Go 程序将崩溃退出。
  • Go 也提供了捕捉 panic 并恢复程序正常执行秩序的方法,我们可以通过 recover 函数来实现这一点。
    func bar() {
    	defer func() {
    		if e := recover(); e != nil {
    			fmt.Println("recover the panic:", e)
    		}
    	}()
    	println("call bar")
    	panic("panic occurs in bar")
    	zoo()
    	println("exit bar")
    }
    
    • 我们在一个 defer 匿名函数中调用 recover 函数对 panic 进行了捕捉。
    • recover 是 Go 内置的专门用于恢复 panic 的函数,它必须被放在一个 defer 函数中才能生效。
    • 如果 recover 捕捉到 panic,它就会返回以 panic 的具体内容为错误上下文信息的错误值。如果没有 panic 发生,那么 recover 将返回 nil。而且,如果 panic 被 recover 捕捉到,panic 引发的 panicking 过程就会停止。

使用 defer 简化函数实现

  • defer 是 Go 语言提供的一种延迟调用机制,defer 的运作离不开函数。
    • 在 Go 中,只有在函数(和方法)内部才能使用 defer;
    • defer 关键字后面只能接函数(或方法),这些函数被称为 deferred 函数。
    • defer 将它们注册到其所在 Goroutine 中,用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前,按后进先出(LIFO)的顺序被程序调度执行。
      在这里插入图片描述
    • 而且,无论是执行到函数体尾部返回,还是在某个错误处理分支显式 return,又或是出现 panic,已经存储到 deferred 函数栈中的函数,都会被调度执行。所以说,deferred 函数是一个可以在任何情况下为函数进行收尾工作的好“伙伴”。
  • defer 使用的几个注意事项
    • 第一点:明确哪些函数可以作为 deferred 函数
      • 对于自定义的函数或方法,defer 可以给与无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候被自动丢弃。
      • append、cap、len、make、new、imag 等内置函数都是不能直接作为 deferred 函数的,而 close、copy、delete、print、recover 等内置函数则可以直接被 defer 设置为 deferred 函数。
      • 对于那些不能直接作为 deferred 函数的内置函数,我们可以使用一个包裹它的匿名函数来间接满足要求。
    • 第二点:注意 defer 关键字后面表达式的求值时机
      • defer 关键字后面的表达式,是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。
    • 第三点:知晓 defer 带来的性能损耗
      • defer 让我们进行资源释放(如文件描述符、锁)的过程变得优雅很多,也不易出错。但在性能敏感的应用中,defer 带来的性能负担也是我们必须要知晓和权衡的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/102282.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

软件测试/测试开发丨Python 学习笔记 之 链表

点此获取更多相关资料 本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接:https://ceshiren.com/t/topic/26458 链表与数组的区别 复杂度分析 时间复杂度数组链表插入删除O(n)O(1)随机访问O(1)O(n) 其他角度分析 内存连续,利用CPU的机制&#xff0…

中间件环境搭建配置过程解读

中间件环境搭建 目录 中间件环境搭建xampp 搭建环境Tomcat环境配置安装mysql连接mysql 问题解决 xampp 搭建环境 安装xampp服务集成环境工具 官网地址下载项目压缩包,将项目文件夹放在xampp安装目录的htdocs文件夹下初始化xampp:运行目录内的setup_xamp…

idea远程debug调试

背景 有时候我们线上/测试环境出现了问题,我们本地跑却无法复现问题,使用idea的远程debug功能可以很好的解决该问题 配置 远程debug的服务,我们使用Springboot项目为例(SpringCloud作为微服务项目我们可以可以使用本地注册到远程项目&…

QT day1登录界面设计

要设计如下图片: 代码如下: main.cpp widget.h widget.cpp 运行效果: 2,思维导图

关于 MySQL、PostgresSQL、Mariadb 数据库2038千年虫问题

MySQL 测试时间:2023-8 启动MySQL服务后,将系统时间调制2038年01月19日03时14分07秒之后的日期,发现MySQL服务自动停止。 根据最新的MySQL源码(mysql-8.1.0)分析,sql/sql_parse.cc中依然存在2038年千年虫…

黑马 大事件项目 笔记

学习视频:黑马 Vue23 课程 后台数据管理系统 - 项目架构设计 在线演示:https://fe-bigevent-web.itheima.net/login 接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835 接口根路径: http:/…

三维点云转换为二维图像

文章目录 前言原理代码总结与反思实验结果展示 前言 目的:将三维点云转换为二维图像 作用: a.给点云赋予彩色信息,增强点云所表达物体或对象的辨识度; b.将三维点云中绘制的目标物体通过映射关系绘制到二维图像中,这个…

报错处理:Disk space full

报错环境: Linux 具体报错: No space left on device,磁盘空间已满 排错思路: 当磁盘空间耗尽时,会出现磁盘空间已满的错误。这可能是由于磁盘上的文件过多或者某个文件系统占用了过多磁盘空间。 解决方法:…

UE5- c++ websocket客户端写法

# 实现目标 ue5 c 实现socket客户端,读取服务端数据,并进行解析 #实现步骤 {projectName}.Build.cs里增加 "WebSockets","JsonUtilities", "Json"配置信息,最终输出如下: using UnrealBuildTool;…

深入探讨梯度下降:优化机器学习的关键步骤(二)

文章目录 🍀引言🍀eta参数的调节🍀sklearn中的梯度下降 🍀引言 承接上篇,这篇主要有两个重点,一个是eta参数的调解;一个是在sklearn中实现梯度下降 在梯度下降算法中,学习率&#xf…

Maven 基础之安装和命令行使用

Maven 的安装和命令行使用 1. 下载安装 下载解压 maven 压缩包(http://maven.apache.org/) 配置环境变量 前提:需要安装 java 。 在命令行执行如下命令: mvn --version如出现类似如下结果,则证明 maven 安装正确…

无涯教程-Android - ImageButton函数

ImageButton是一个AbsoluteLayout,可让您指定其子级的确切位置。这显示了带有图像(而不是文本)的按钮,用户可以按下或单击该按钮。 Android button style set ImageButton属性 以下是与ImageButton控件相关的重要属性。您可以查看Android官方文档以获取属性的完整列表以及可以…

webrtc 的Bundle group 和RTCP-MUX

1,最近调试程序的时候发现抱一个错误 max-bundle configured but session description has no BUNDLE group 最后发现是一个参数设置错误 config.bundle_policy webrtc::PeerConnectionInterface::BundlePolicy::kBundlePolicyMaxBundle; 2,rtcp-mu…

迈向无限可能, ATEN宏正领跑设备切换行业革命!

随着互联网在各个领域的广泛应用,线上办公这一不受时间和地点制约、不受发展空间限制的办公模式开始广受追捧,预示着经济的发展正朝着新潮与活跃的方向不断跃进。当然,在互联网时代的背景下,多线程、多设备的线上办公模式也催生了许多问题:多设备间无法进行高速传输、切换;为保…

SpringCloud(十)——ElasticSearch简单了解(一)初识ElasticSearch和RestClient

文章目录 1. 初始ElasticSearch1.1 ElasticSearch介绍1.2 安装并运行ElasticSearch1.3 运行kibana1.4 安装IK分词器 2. 操作索引库和文档2.1 mapping属性2.2 创建索引库2.3 对索引库的查、删、改2.4 操作文档 3. RestClient3.1 初始化RestClient3.2 操作索引库3.3 操作文档 1. …

A Mathematical Framework for Transformer Circuits—Part (1)

A Mathematical Framework for Transformer Circuits 前言Summary of ResultsREVERSE ENGINEERING RESULTSCONCEPTUAL TAKE-AWAYS Transformer OverviewModel SimplificationsHigh-Level ArchitectureVirtual Weights and the Residual Stream as a Communication ChannelVIRTU…

Tomcat安装与配置

文章目录 一,说明二,安装三:运行四,配置(若本地一个tomcat服务,可配置,若多个,可忽略)五:修改端口六:启动多tomcat(举例两个)七:Idea关联tomcat(由于老项目不是SpringBoot,这里贴下设置)八:启动服务CMD窗口和Idea关联启动中文乱码九:Linux环境下的部署流程 一,说明 本文主要介…

本地启动若依微服务版本

前置工作: 1.导入sql文件 2.安装完nacos 3.安装完redis 启动步骤: 1.开启nacos,在bin目录下 startup.cmd -m standalone 注意:在这之前要配置nacos持久化,修改conf/application.properties文件,增加支持…

BTC价格预测:灰度突如其来的胜利是否会打破“九月魔咒”?

加密市场即将进入第三季度交易的最后阶段,由于9月份被视为是比特币的下跌时期,大多数投资者都预测加密货币之王将会进一步下跌。然而,事情却发生了逆转,灰度突如其来的胜利是否会打破“九月魔咒”? 受该事件影响&#…

【从0学习Solidity】合约入门 Hello Web3

【学习Solidity的基础】入门智能合约开发 Hello Web3 📱不写代码没饭吃上架主页 在强者的眼中,没有最好,只有更好。我们是全栈开发领域的优质创作者,同时也是阿里云专家博主。 ✨ 关注我们的主页,探索全栈开发的无限…