基础《Go学习笔记》读书笔记——函数
writer:zgx
lastmodify:2020年09月26日
第四章——函数
- 无须前置声明
- 不支持命名嵌套编译
- 不支持默认参数
- 支持不定长变参
- 支持多返回值
- 支持命名返回值
- 支持匿名函数和闭包
ERROR
func xxx()
{
}
syntax error: unexpected semicolon or newline before {
函数属于第一类对象,具备相同签名(参数及其返回值列表)的视为同一类型
第一类对象(first-class object)指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。最常见的用法是匿名函数
使用命名类型更加方便
type FormatFunc func(string s, ...interface{}) (string, error)
func format(f FormatFunc, s string, a ...interface{}) (string, error){
return f(s, a...)
}
golang建议命名
这篇写的还是挺详细的
实现传出参数(out),通常建议使用返回值。当然也可使用二级指针
package main
func test(p **int) {
x := 100
*p = &x
}
func main() {
var p *int
test(&p)
println(*p)
}
变参
变参本质上就是一个切片。只能接收一到多个同类型参数,必须放在列表尾部
package main
import "fmt"
func test(s string, a ...int) {
fmt.Printf("%T %v\n", a, a)
}
func main() {
test("xx", 1, 2, 3, 4)
}
go run main.go
[]int [1 2 3 4]
将切片作为变参时,需要进行展开操作。如果是数组,需要先转为切片
package main
import "fmt"
func test(a ...int) {
fmt.Println(a)
}
func test_1(a ...int) {
for i := range a {
a[i] += 100
}
}
func main() {
a := [3]int{1, 2, 3}
test_1(a[:]...)
test(a[:]...)
}
[101 102 103]
返回值
借鉴自动态语言的多返回值模式,函数得以返回更多状态,尤其是error模式
package main
import (
"errors"
"fmt"
)
func div(x, y int) (int, error) { // 多返回值列表必须使用括号
if y == 0 {
return 0, errors.New("division by zero")
}
return x / y, nil
}
func log(v int, e error) {
fmt.Println(v, e)
}
func test() (int, error) {
return div(5, 0)
}
func main() {
log(test())
}
go run main.go
0 division by zero
## 命名返回值
对返回值命名规则和简短变量定义一样,优缺点并存
```golang
package main
import (
"errors"
"fmt"
)
func div(x, y int) (z int, err error) {
if y == 0 {
err = errors.New("divisition by zero")
return
}
z = x / y
return
}
func main() {
z, err := div(5, 0)
fmt.Println(z, err)
}
缺点
- 这种”局部变量”,会被不同层级的同名变量遮蔽
- 必须对所有返回值命令
package main
func div(x, y int) (z int) {
z := x / y
return
}
func main() {
z := div(5, 10)
}
匿名函数
package main
import "fmt"
//作为返回值
func testRet() func(x, y int) int {
return func(x, y int) int {
return x + y
}
}
func main() {
func(s string) {
fmt.Println(s)
}("xxx")
//作为参数
add := func(x, y int) int {
return x + y
}
println(add(1, 2))
//作为返回值
addTwo := testRet()
println(addTwo(2, 2))
}
普通函数和匿名函数都可以作为结构体字段,或经通道传送
package main
//通过结构体传递
func testStruct() {
type calc struct {
mul func(x, y int) int
}
x := calc{
mul: func(x, y int) int {
return x * y
},
}
println(x.mul(2, 3))
}
//通过通道传递
func testChannel() {
c := make(chan func(int, int) int, 2)
c <- func(x, y int) int {
return x * y
}
println((<-c)(3, 3))
}
func main() {
testStruct()
testChannel()
}
闭包
闭包(closure)是在其词法上下文引用了自由变量的函数
package main
func test(x int) func() {
return func() {
println(x)
}
}
func main() {
f := test(100)
f()
}
test返回的匿名函数会引用上下文环境变量。当该函数在main中执行时,依然可以正确读取x的值,这种现象叫闭包
package main
func test(x int) func() {
println(&x)
return func() {
println(&x, x)
}
}
func main() {
f := test(100)
f()
}
0xc000014088
0xc000014088 100
闭包直接引用了环境变量,返回的不仅是匿名函数,还包括所引用的环境变量指针;所以说,闭包是函数和引用环境的组合
其实本质上返回的是一个funcval结构,可在runtime/runtime2.go中找到相关定义
正因为闭包通过指针引用变量,那么可能导致生命周期延长,甚至分配到堆内存。另外,有所谓“延迟求值”的特性
package main
func test() []func() {
var s []func()
for i := 0; i < 2; i++ {
s = append(s, func() { //多个匿名函数添加到列表
println(&i, i)
})
}
return s //返回匿名函数列表
}
func main() {
for _, f := range test() { //迭代执行匿名函数
f()
}
}
0xc000014088 2
0xc000014088 2
for循环复用了局部变量i,那么每次添加的匿名函数和引用自然为同一变量;添加操作仅仅是匿名存入列表,并没有执行,
当执行这些函数时候,读取的是环境变量i最后一次循环时的值
解决办法是每次用不同的环境变量或传参复制,让各自闭包环境各不相同
func test() []func() {
var s []func()
for i := 0; i < 2; i++ {
x := i //修改的地方
s = append(s, func() { //多个匿名函数添加到列表
println(&x, x)
})
}
return s //返回匿名函数列表
}
0xc000014088 0
0xc0000140a0 1
多个匿名函数引用同一环境变量,也会让事情变得复杂,任何修改行为都会影响其他函数值,在并发下可能需要同步处理
package main
func test(x int) (func(), func()) {
return func() {
println(x)
x += 100
}, func() {
println(x)
}
}
func main() {
a, b := test(0)
a()
b()
}
0
100
闭包可以不用传递参数就可以读取或修改环境变量,当然也要为此付出额外的代价。对于性能要求高的,慎用
延迟调用
语句defer向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常常用于资源释放、解除锁定、错误处理等操作
延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制缓存起来。如对状态敏感,可改用指针或闭包
package main
func main() {
x, y := 1, 2
defer func(a int) {
println("defer x, y = ", a, y)
}(x)
x += 100
y += 100
}
defer x, y = 1 102
多个defer注册按FIFO次序执行(栈)
package main
func main() {
defer println(1)
defer println(2)
}
2
1
** 对性能要求高且压力大的算法,应避免使用延迟调用**
- 考虑到recover的特性,如果要保护代码片段,只能将其重构为函数调用
- 调试阶段,可使用runtime/debug.PrintStack 函数输出完整的堆栈信息
package main
import "runtime/debug"
func test(x, y int) {
z := 0
func() {
defer func() {
if recover() != nil {
z = 0
//调试阶段,可以使用runtime/debug.PrintStack函数输出完整的堆栈信息
debug.PrintStack()
}
}()
z = x / y
}()
println("x / y = ", z)
}
func main() {
test(5, 0)
}
tips:除非是不可恢复、导致系统无法正常工作的错误,否则不建议使用panic
如:文件系统没有操作权限、服务端口被占用、数据库未启动等情况..
编译器通过插入额外指令来实现延迟调用执行,而return和panic语句都会终止当前函数流程,引发延迟调用
package main
import "fmt"
//z = 100 -> call defer -> return
func test() (z int) {
defer func() {
fmt.Println("defer", z)
z += 100
}()
return 100
}
func main() {
fmt.Println("test:", test())
}
defer 100
test: 200
误用
千万注意,延迟调用在函数结束时候才被执行。不合理的时候会浪费更多的资源,甚至造成逻辑错误
eg:循环处理多个日志文件,不恰当的defer导致文件关闭时间延长
这个关闭操作在main函数执行完成后才会执行;
//伪代码
func main() {
for i := 0; i < 10000; i++ {
//do something
defer xxx
}
}
这里应该重构成函数,循环和处理进行分离
延迟调用在匿名函数结束时执行,非main函数
func main() {
do := func(n int){
//do something
defer xxx
}
for i := 0; i < 100000; i++{
do(i)
}
}
性能
相比直接用CALL汇编指令调用函数,延迟调用需要花费更大的代价;包括注册、调用等,还有额外的缓存开销
package main
import (
"sync"
"testing"
)
var m sync.Mutex
func call() {
m.Lock()
m.Unlock()
}
func deferCall() {
m.Lock()
defer m.Unlock()
}
func BenchmarkCall(b *testing.B) {
for i := 0; i < b.N; i++ {
call()
}
}
func BenchmarkDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
deferCall()
}
}
chp4_func % go test -test.bench=".*" -count=5
goos: darwin
goarch: amd64
pkg: orzgx/chp4_func
BenchmarkCall-8 94289460 11.7 ns/op
BenchmarkCall-8 100000000 11.7 ns/op
BenchmarkCall-8 100000000 11.7 ns/op
BenchmarkCall-8 100000000 11.7 ns/op
BenchmarkCall-8 100000000 11.7 ns/op
BenchmarkDefer-8 83509653 13.8 ns/op
BenchmarkDefer-8 77678026 13.9 ns/op
BenchmarkDefer-8 86204041 13.9 ns/op
BenchmarkDefer-8 85700616 13.8 ns/op
BenchmarkDefer-8
85161574 14.1 ns/op
PASS
ok orzgx/chp4_func 12.092s
defer性能明显要差
性能要求高,压力大的算法,应避免使用延迟
错误处理
error
将error定义为接口类型,以便实现自定义错误类型
type error interface{ Error() string }
一般是这样
func f() (type, errror){
var xxxx tepe
//errror定义可以拉到最上面,以便后续调用
retrun xxxx, errors.New("error xxx")
}
一般错误变量命名以err为前缀,且error内容全部小写,没有结束标点;以便于嵌入其他格式化字符串中
可自定义错误类型,以容纳更多上下文状态信息
package main
import (
"fmt"
"log"
)
type DivError struct {
x, y int
}
func (DivError) Error() string { //实现error接口方法
return "division by zero"
}
func div(x, y int) (int, error) {
if y == 0 {
return 0, DivError{x, y}
}
return x / y, nil
}
func main() {
z, err := div(5, 0)
if err != nil {
switch e := err.(type) {
case DivError:
fmt.Println("DivError")
fmt.Println(e, e.x, e.y)
default:
fmt.Println("default")
fmt.Println(e)
}
log.Fatalln(err)
}
println(z)
}
自定义错误类型通常是以Error为名称后缀,在用switch时候,注意case的顺序,应该把自定义类型放在前面,优先匹配更具体的类型。
panic, recover
相比error,与try/expect结构异常接近
func panic(v interface{})
func recover() interface{}
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil { //捕获错误
fmt.Println(err)
}
}()
panic("I'm dead") //引发错误
panic("exit") //永远不执行
}
I'm dead
panic会中断当前函数流程,执行延迟调用。
延迟调用函数中,recover会捕获并返回panic提交的错误对象。
中断性错误会沿调用堆栈向外传递,要么被外层捕获、要么导致进程崩溃
package main
import "fmt"
func test() {
defer fmt.Println("test 1")
defer fmt.Println("test 2")
panic("i am dead")
}
func main() {
defer func() {
fmt.Println(recover())
}()
test()
}
test 2
test 1
i am dead
连续调用panic,仅最后一个会被recover捕获
package main
import "log"
func main() {
defer func() {
for {
if err := recover(); err != nil {
log.Println(err)
} else {
log.Fatalln("fatal")
}
}
}()
defer func() {
panic("xxx") //类似重新抛出rethrow
}()
panic("qqq")
}
延迟函数中panic,不会影响后续延迟调用。而recover后panic,会再次获取。recover在延迟函数中执行才能正常工作
Draft
可以学下gdb调试Golang程序
refs:
https://blog.csdn.net/huwh_/article/details/77140752
https://blog.csdn.net/fengshenyun/article/details/107466329
Golang逃逸分析
基础《Go学习笔记》读书笔记——函数的更多相关文章
- 鸟哥Linux私房菜 基础学习篇读书笔记(10):Linux磁盘和文件系统管理(3)
本文总结了Linux操作系统来管理我们的硬盘和文件系统需要使用命令.当我们在系统中增加一个硬盘驱动器.什么是我们需要去通过这个硬盘就可以真正使用步骤?下面步骤: (1)对磁盘进行分区,新建能够使用的分 ...
- 鸟哥的Linux私房菜 基础学习篇读书笔记(9):Linux磁盘与文件系统管理(2)
上一篇文章主要从理论上分析了Linux的Ext2文件系统.这一篇主要解说怎样查看Linux的文件系统的容量以及解说Linux文件系统中的连接文件. 能够通过df和du命令来查看磁盘与文件夹的容量.df ...
- 人体和电脑的关系——鸟哥的LINUX私房菜基础学习篇读书笔记
CUP=脑袋: 每个人会做的事情都不一样(指令集的差异),但主要都是通过脑袋来判断与控制身体各部分的行动 内存=脑袋中存放正在思考的数据区块: 在实际活动过程中,我们的脑袋需要有外界刺激的数据(例如光 ...
- 《分布式Java应用之基础与实践》读书笔记三
对于大型分布式Java应用与SOA,我们可以从以下几个方面来分析: 为什么需要SOA SOA是什么 eBay的SOA平台 可实现SOA的方法 为什么需要SOA 第一个现象是系统多元化带来的问题,可 ...
- 《分布式Java应用之基础与实践》读书笔记一
分布式Java应用的体系结构知识简单分为: 网络通信:包括协议和IO 消息方式的系统间通信:包括基于Java包.基于开源框架.性能角度 远程调用方式的系统间通信:包括基于Java包.基于开源框架.性能 ...
- 《分布式Java应用之基础与实践》读书笔记二
远程调用方式就是尽可能地使系统间的通信和系统内一样,让使用者感觉调用远程同调用本地一样,但其实没没有办法做到完全透明,例如由于远程调用带来的网络问题.超时问题.序列化/反序列化问题.调式复杂的问题等. ...
- 《分布式Java应用之基础与实践》读书笔记四
Java代码作为一门跨操作系统的语言,最终是运行在JVM中的,所以对于JVM的理解就变得非常重要了.整体上,我们可以从三个方面来深入理解JVM. Java代码的执行 内存管理 线程资源同步和交互机制 ...
- 人生效率手册:如何卓有成效地过好每一天--By张萌姐姐--读书笔记
读书笔记:<人生效率手册>:如何卓有成效地过好每一天--By张萌姐姐... 整本书看完的感受: 这本书主要讲的是生活中我们需要给自己一个目标,然后通过自己的努力去实现这个目标,书中说的很多 ...
- 深度学习读书笔记之RBM(限制波尔兹曼机)
深度学习读书笔记之RBM 声明: 1)看到其他博客如@zouxy09都有个声明,老衲也抄袭一下这个东西 2)该博文是整理自网上很大牛和机器学习专家所无私奉献的资料的.具体引用的资料请看参考文献.具体的 ...
- C++ 11学习和掌握 ——《深入理解C++ 11:C++11新特性解析和应用》读书笔记(一)
因为偶然的机会,在图书馆看到<深入理解C++ 11:C++11新特性解析和应用>这本书,大致扫下,受益匪浅,就果断借出来,对于其中的部分内容进行详读并亲自编程测试相关代码,也就有了整理写出 ...
随机推荐
- 记录一下opencv-contrib的编译使用
一.来由 公司需求进行多图拼接算法,在opencv提供的Stitcher类当中默认的算子是ORB,我想尝试使用SIFT和SURF算子,经过一番查找发现这两个算子需要opencv的超集库支持--&quo ...
- 本地代理web端口
先配置 使用ssh 通过ProxyCommand:利用跳板机让不在同一局域网的机器ssh直连 代理访问 ssh -qTfnND 127.0.0.1:$代理端口 代理机器主机名
- 实现无感刷新Token技术:.Net Web API与axios的完美结合
这是我之前分享在星球里面的课程,下面整理下,分享下这个无感刷新Token技术方案. 我们都知道Token是有设置有效期的,为了安全都不会设置过长的有效期:但设置有效期太短,又会导致经常需要重新登录. ...
- STM32的中断刨析(完结)
STM32中断刨析 一直以来,学习了 stm32 和 freertos 但在思考 RTOS 的任务调度时,涉及到 stm32 的中断相关的 PendSV 就感觉糊里糊涂.本篇记录刨析 stm32 的中 ...
- apisix启动报错undefined symbol: EVP_KDF_ctrl, version OPENSSL_1_1_1b
报错内容 2024/08/06 16:56:13 [error] 154236#154236: *7039 [lua] plugin.lua:110: load_plugin(): failed to ...
- 通向架构师的道路(第五天)之tomcat集群-群猫乱舞
一.为何要集群 单台App Server再强劲,也有其瓶劲,先来看一下下面这个真实的场景. 当时这个工程是这样的,tomcat这一段被称为web zone,里面用spring+ws,还装了一个jbos ...
- JDK7新特性之G1 GC
Garbage-first garbage collector,简称G1 GC,是最终将用于代替Concurrent Mark-Sweep garbage collector(CMS GC)的新一代垃 ...
- 代码质量审查工具之SonarQube8.9(LTS)与gitlab CI集成使用
官网地址: https://docs.sonarqube.org/8.9/analysis/scan/sonarscanner/ 目标:在push时自动触发GitLab CI/CD pipeline ...
- 数据同步之DataX
目前业务中需要进行数据同步, 考虑使用datax数据同步方式替换掉现有的同步方式 业务场景: 即将业务中每天生成的日志表中的数据部分字段同步到自己的库中,进行后台数据的查询 起因: 之前"大 ...
- PowerShell 重命名文件夹及删除空文件夹
数据来源 $urldata 中的倒数第2列(子文件夹名称列)包含 /.\ 等特殊字符 某个文件夹重命名脚本 foreach ($i in 0..100) { # 提取路径部分 $basePath = ...