Go语言defer的一些神奇规则示例详解

目录
  • 测试题
  • 分析
    • 规则一当defer被声明时,其参数就会被实时解析
    • 规则二 defer可能操作主函数的具名返回值
    • 规则三 延迟函数执行按后进先出顺序执行
  • 坑实例

测试题

defer有一些规则,如果不了解,代码实现的最终结果会与预期不一致。对于这些规则,你了解吗?

这是关于defer使用的代码,可以先考虑一下返回值。

package main
import (
	"fmt"
)
/**
 * @Author: Jason Pang
 * @Description: 快照
 */
func deferFuncParameter1() {
	var aInt = 1
	defer fmt.Println(aInt)
	aInt = 2
	return
}
/**
 * @Author: Jason Pang
 * @Description: 快照
 */
func deferFuncParameter2() {
	var aInt = 1
	defer func(t int) {
		fmt.Println(t)
	}(aInt)
	aInt = 2
	return
}
/**
 * @Author: Jason Pang
 * @Description: 动态
 */
func deferFuncParameter3() {
	var aInt = 1
	defer func() {
		fmt.Println(aInt)
	}()
	aInt = 2
	return
}
/**
 * @Author: Jason Pang
 * @Description: 影响返回值
 * @return ret
 */
func deferFuncReturn1() (ret int) {
	ret = 10
	defer func() {
		ret++
		fmt.Println("-----", ret)
	}()
	return 2
}
/**
 * @Author: Jason Pang
 * @Description: 不影响返回值
 * @return ret
 */
func deferFuncReturn2() (ret int) {
	ret = 10
	defer func(ret int) {
		ret++
		fmt.Println("-----", ret)
	}(ret)
	return 2
}
/**
 * @Author: Jason Pang
 * @Description: defer顺序
 */
func deferFuncSeq1() {
	var aInt = 1
	defer fmt.Println(aInt)
	aInt = 2
	defer fmt.Println(aInt)
	return
}
func main() {
	fmt.Println("快照")
	deferFuncParameter1()
	deferFuncParameter2()
	deferFuncParameter3()
	fmt.Println("返回值")
	fmt.Println(deferFuncReturn1())
	fmt.Println(deferFuncReturn2())
	fmt.Println("执行顺序")
	deferFuncSeq1()
}

正确输出为:

➜ myproject go run main.go

快照

1

1

2

返回值

----- 3

3

----- 11

2

执行顺序

2

1

分析

defer有几条重要规则,上面的结果都能从这些规则中找到答案。

规则一当defer被声明时,其参数就会被实时解析

当defer被声明的时候,如果直接使用了参数,此时的参数就会使用快照值,在整个生命周期内不会变化。如deferFuncParameter1、deferFuncParameter2,虽然aInt在defer声明后被变更,但defer里的值不会再变了。

func deferFuncParameter1() {
	var aInt = 1
	defer fmt.Println(aInt)
	aInt = 2
	return
}
func deferFuncParameter2() {
	var aInt = 1
	defer func(t int) {
		fmt.Println(t)
	}(aInt)
	aInt = 2
	return
}

与之相反的是deferFuncParameter3,随aInt的变化而变化。

func deferFuncParameter3() {
	var aInt = 1
	defer func() {
		fmt.Println(aInt)
	}()
	aInt = 2
	return
}

规则二 defer可能操作主函数的具名返回值

defer有可能更改函数的返回值,这是最容易导致错误的地方。

关键字_return_不是一个原子操作,实际上_return_只代理汇编指令_ret_,即将跳转程序执行。比如语句 return i ,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。return i的执行过程如下所示:

result = i 
执行defer
return

所以基于这个规则,对于deferFuncReturn1,

func deferFuncReturn1() (ret int) {
	ret = 10
	defer func() {
		ret++
		fmt.Println("-----", ret)
	}()
	return 2
}

执行过程为:

ret = 2
ret++
fmt.Println("-----", ret)
return

所以最终ret的值为3。

对于deferFuncReturn2,因为defer声明的时候直接使用了参数,所以使用的是快照,不会影响ret的返回值。

规则三 延迟函数执行按后进先出顺序执行

即先出现的 defer最后执行

这个规则大家都很熟悉,defer按照栈的顺序执行。

坑实例

举一个错误使用defer的实例。在go中使用事务时,有一种推荐写法:将Rollback放到defer中,通过判断函数是否有报错或者panic,来判断是否要回滚。

func  Update() (resp *baseinfo.Resp, err error) {
	//开启事务
	panicked := true
	tx, err := db.TXBegin()
	if err != nil {
		return resp, nil
	}
	defer func() {
		if panicked || err != nil {
			tx.Rollback()
		}
	}()
	//更新
	err = h.update(shopId, tx)
	if err != nil {//失败返回
		return resp, nil
	}
	panicked = false
	err = tx.Commit().Error
	if err != nil { //失败返回
		return resp, nil
	}
	return
}

判断回滚的err正是函数的具名返回值,在有报错的情况下,返回值被赋值为nil,这意味如果有失败,Rollback也不会被执行。

之所以不将err直接返回,而是使用nil,是因为框架设计的问题,业务错误通过resp返回,如果直接返回err,框架会认为是RPC错误。

以上就是Go语言defer的一些神奇规则示例详解的详细内容,更多关于Go语言defer规则的资料请关注我们其它相关文章!

(0)

相关推荐

  • Dragonfly P2P 传输协议优化代码解析

    目录 优化背景 相关代码分析 优化方案 优化实现 优化结果 优化背景 此前 Dragonfly 的 P2P 下载采用静态限流策略,相关配置项在 dfget.yaml 配置文件中: # 下载服务选项. download: # 总下载限速. totalRateLimit: 1024Mi # 单个任务下载限速. perPeerRateLimit: 512Mi 其中 perPeerRateLimit 为单个任务设置流量上限, totalRateLimit 为单个节点的所有任务设置流量上限. 静态限流策略

  • go variant底层原理深入解析

    目录 varint benchmarks struct variant 为什么 variant 要比 plainstruct 快 variant 可能的优化? varint 今天本来在研究 OpenTelemetry 的基准性能测试 github.com/zdyj3170101…,测试不同网络协议:grpc, grpc-stream, http, websocket 在发送不同大小数据包时消耗 cpu,吞吐 的区别,由 tigrannajaryan 这位大神所写. 好奇翻了翻该大神的 githu

  • Golang Copier入门到入坑探究

    目录 正文 安装 快速入门 入坑 再探坑出坑 再盘一盘坑 结语 正文 github: https://github.com/jinzhu/copier 由于 golang 没有对复杂结构体的 clone 方法,所以,就需要有 copier 这样的工具库. 它看起来很简单,但实际使用中,有些“坑”还是要注意! 本文: 入门为辅,探“坑”为主, 看完再划走,CS我没有. 安装 go get github.com/jinzhu/copier 快速入门 好的,来一段代码快速了解 copier packa

  • Android Studio gradle配置packagingOptions打包so库重复

    目录 正文 pickFirst 匹配 doNotStrip 设置 merge 将匹配的文件都添加到APK中 exclude 过滤 正文 在安卓开发中,通常会使用到gradle来编译,在安卓项目的app目录下的build.gradle中是用来对编译进行配置的,packagingOptions 是其中的一个打包配置,常见的设置项有exclude.pickFirst.doNotStrip.merge. 在日常代码开发中,我们需要知其然,而知其所以然,本文章知识也是Android日常瘦身的的必备知识.

  • Go语言编程通过dwarf获取内联函数

    目录 dwarf组成 如何将 addr 转换为行号 内联函数 如何展开内联函数 使用 parca 展开内联函数 parca 输出有以下问题 dwarf组成 dwarf 由 The Debugging Information Entry . type Entry struct { Offset Offset Tag Tag // 描述其类型 Children bool Field []Field // 包含的字段 } 不同的 entry 有不同的类型: tag compile unit, 在 go

  • Go语言defer的一些神奇规则示例详解

    目录 测试题 分析 规则一当defer被声明时,其参数就会被实时解析 规则二 defer可能操作主函数的具名返回值 规则三 延迟函数执行按后进先出顺序执行 坑实例 测试题 defer有一些规则,如果不了解,代码实现的最终结果会与预期不一致.对于这些规则,你了解吗? 这是关于defer使用的代码,可以先考虑一下返回值. package main import ( "fmt" ) /** * @Author: Jason Pang * @Description: 快照 */ func de

  • Go语言中的字符串处理方法示例详解

    1 概述 字符串,string,一串固定长度的字符连接起来的字符集合.Go语言的字符串是使用UTF-8编码的.UTF-8是Unicode的实现方式之一. Go语言原生支持字符串.使用双引号("")或反引号(``)定义. 双引号:"", 用于单行字符串. 反引号:``,用于定义多行字符串,内部会原样解析. 示例: // 单行 "心有猛虎,细嗅蔷薇" // 多行 ` 大风歌 大风起兮云飞扬. 威加海内兮归故乡. 安得猛士兮守四方! ` 字符串支持转义

  • go语言编程之select信道处理示例详解

    目录 select信道处理 fibonacci数列监听 select监听协程 select信道处理 注意:有default就不会阻塞 package main func main() { var chan1 = make(chan int) var chan2 = make(chan int) select { case <-chan1: // 如果chan1成功读到数据,则进行该case处理语句 case chan2: // 如果chan2成功读到数据,则进行该case处理语句 default

  • Sentinel热点规则示例详解分析

    目录 概念 @SentinelResource 小试牛刀 TestController.java defaultFallback fallback 流量控制 熔断降级 热点参数限流 高级选项 概念 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流. 热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源

  • Go语言基础设计模式之策略模式示例详解

    目录 概述 针对同一类型问题的多种处理方式 一.不使用策略模式 二.策略模式 UML 总结 示例 概述 定义一系列算法,将每个算法封装起来.并让它们能够相互替换.策略模式让算法独立于使用它的客户而变化. 针对同一类型问题的多种处理方式 一.不使用策略模式 package main import "fmt" type User struct { Name string } func (this User) travel(t string) { switch t { case "

  • Go语言基础函数基本用法及示例详解

    目录 概述 语法 函数定义 一.函数参数 无参数无返回 有参数有返回 函数值传递 函数引用传递 可变参数列表 无默认参数 函数作为参数 二.返回值 多个返回值 跳过返回值 匿名函数 匿名函数可以赋值给一个变量 为函数类型添加方法 总结 示例 概述 函数是基本的代码块,用于执行一个任务 语法 函数定义 func 函数名称( 参数列表] ) (返回值列表]){ 执行语句 } 一.函数参数 无参数无返回 func add() 有参数有返回 func add(a, b int) int 函数值传递 fu

  • Go语言基础go fmt命令使用示例详解

    go fmt 命令主要是用来帮你格式化所写好的代码文件[很多第三方集成软件都是使用了go fmt命令] 一.使用: go fmt <文件名>.go 使用go fmt命令,更多时候是用gofmt,而且需要参数 -w,否则格式化结果不会写入文件.gofmt -w src,可以格式化整个项目. 二.参数介绍 -l 显示那些需要格式化的文件 -w 把改写后的内容直接写入到文件中,而不是作为结果打印到标准输出. -r 添加形如"a[b:len(a)] -> a[b:]"的重写规

  • Go语言基础go install命令使用示例详解

    目录 go install 一.使用 二.包名和目录名的关系 三.注意 go install 编译并安装代码包,对于库,会生成目标库文件,并且放置到GOPATH/pgk目录下. 对于可执文件,会生成目标可执行文件,并且放置到GOPATH/bin目录下 一.使用 命令 描述 go install lib 编译安装package lib,会为main包在bin下生成可执行exe文件 go install lib2 lib/util 同时编译安装lib2和lib/util两个package. 二.包名

  • Go语言基础类型及常量用法示例详解

    目录 基础类型 概述 按类别有以下几种数据类型 数值类型 派生类型 变量 概述 单个变量声明 多个变量声明 基础类型 概述 在 Go 编程语言中,数据类型用于声明函数和变量.数据类型的出现时为了把数据分成所需要用大数据的时候才需要申请大内存,这样可以充分的列用内存. 按类别有以下几种数据类型 数值类型 布尔型 bool:布尔型的值只可以是常量 true 或者 false,默认值为 false. 字符串类型 string:编码统一为 UTF-8 编码标识 Unicode 文本,默认值为空字符串.

  • Go语言基础枚举的用法及示例详解

    目录 概述 一.普通枚举 二.自增枚举 注意 代码 概述 将变量的值一一列举出来,变量只限于列举出来的值的范围内取值 Go语言中没有枚举这种数据类型的,但是可以使用const配合iota模式来实现 一.普通枚举 const ( cpp = 0 java = 1 python = 2 golang = 3 ) 二.自增枚举 iota只能在常量的表达式中使用 fmt.Println(iota) //undefined: iota 它默认开始值是0,const中每增加一行加1 const ( a =

随机推荐