• 基础《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建议命名
    这篇写的还是挺详细的

    refs:https://www.cnblogs.com/Survivalist/p/10110439.html

    实现传出参数(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
    ", 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逃逸分析

  • 相关阅读:
    在SQL Server中使用NewID()随机取得某行
    委托和事件:第 3 页 事件的由来
    case when then else 详解
    spring-boot-starter-security Spring Boot中集成Spring Security
    spring-boot-actuator健康监控
    汉字转拼音开源工具包Jpinyin介绍
    JAVA实现汉字转换为拼音 pinyin4j/JPinyin
    Spring MVC 后端接口支持跨域CORS调用
    web开发-CORS支持
    maven POM.xml 标签详解
  • 原文地址:https://www.cnblogs.com/zhenggaoxiong/p/13699723.html
Copyright © 2020-2023  润新知