• 【Go语言学习笔记】函数


    函数

    定义函数条件

    关键字func用于定义函数

    • 无须前置声明;
    • 不支持命名嵌套定义(nested);
    • 不支持同名函数重载(overload);
    • 不支持默认参数
    • 支持不定长变参
    • 支持多返回值
    • 支持命名返回值
    • 支匿名函数和闭包

    函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。

    第一类对象是指可在运行期间创建,可用作函数参数或返回值,可存入变量的实体,最常见的用法就是匿名函数

    // 定义函数类型
    type FormatFunc func(string, ...interface{}) (string, error)
    
    func format(f FormatFunc, s string, a ...interface{}) (string, error) {
    	return f(s, a...)
    }
    
    // 函数只能判断其是否为nil,不支持其他操作
    func a() {
    
    }
    
    func b() {
    
    }
    
    func main() {
    	println(a == nil)
    	println(a == b) // invalid operation: a == b (func can only be compared to nil)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存。

    建议命名规则

    • 通常是动词和介词加上名词,例如scan Words。
    • 避免不必要的缩写,printError要比printErr更好一些
    • 避免使用类型关键字,比如buildUserStruct看上去会很别扭。
    • 避免歧义,不能有多种用途的解释造成误解。
    • 避免只能通过大小写区分的同名函数。
    • 避免与内置函数同名,这会导致误用。
    • 避免使用数字,除非是特定专有名词,例如UTF8。
    • 避免添加作用域提示前缀。
    • 统一使用camel/pascal case拼写风格。
    • 使用相同术语,保持一致性。
    • 使用习惯用语,比如init表示初始化,is/has返回布尔值结果。
    • 使用反义词组命名行为相反的函数,比如get/set、min/max等。

    参数

    Go对参数的处理偏向保守,不支持有默认值的可选参数,不支持命名实参。调用时必须按前ing顺序传递指定类型和数量的实参,就算以_命名的参数也不能忽略。
    在参数列表中,相邻的同类型参数可以合并;
    参数可视为函数局部变量,因此不能在相同层次定义同名变量。

    不管是指针、引用类型还是其他类型参数,都是只拷贝传递,在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存

    func test(x *int) {
    	fmt.Printf("pointer: %p, target: %v\n", &x, x) // 形参x的地址
    }
    
    func main() {
    	a := 100
    	p := &a
    	fmt.Printf("pointer: %p, target: %v\n", &p, p) // 实参p的地址·
    	test(p)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    从结果可以看出,尽管实参和形参都指向同一目标,但是传递指针是依然被复制。

    变参

    变参本质上就是一个切片,只能接受一到多个同类型参数,且必须放在列表尾部。

    func test1(s string, a ...int) {
    	fmt.Printf("%T, %v\n", a, a) // 显示类型和值
    }
    []int, [1 2 3 4]
    
    func main() {
    
    	test1("hello", 1, 2, 3, 4)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    值传递和指针传递的区别

    // 指针传递
    func test2(x *int) {
    	*x += 1
    	//fmt.Printf("point: %p, target:%v\n", &x, x)
    }
    
    func main() {
    	//a := [3]int{1, 2, 3}
    	x := 1
    	fmt.Printf("point: %p, target:%v\n", &x, x)
    	test2(&x)
    	fmt.Printf("point: %p, target:%v\n", &x, x)
    
    point: 0xc000018088, target:1
    point: 0xc000018088, target:2
    
    // 值传递
    func test2(x int) {
    	x += 1
    	//fmt.Printf("point: %p, target:%v\n", &x, x)
    }
    
    func main() {
    	//a := [3]int{1, 2, 3}
    	x := 1
    	fmt.Printf("point: %p, target:%v\n", &x, x)
    	test2(x)
    	fmt.Printf("point: %p, target:%v\n", &x, x)
    point: 0xc000018088, target:1
    point: 0xc000018088, target:1
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    返回值

    有返回值的函数,必须有明确的return终止语句,除非有panic,或者无break的死循环,则无须return终止语句。

    匿名函数

    匿名函数是指没有定义名字符号的函数,除了没有名字外,匿名函数和普通函数完全相同,最大的区别是,在函数内部定义匿名函数,形成类似嵌套效果,匿名函数可直接调用,保存到变量,作为参数或返回值。

    闭包

    全局变量特点:常驻内存、污染全局
    局部变量特点:不常驻内存,不污染全局

    闭包可以做到让变量常驻内存并且不污染全局。是指有权访问另一个函数作用于中的变量的函数,创建闭包的最常见方式就是在一个函数内部创建另一个函数,但是过度使用闭包可能会占用更多内存,导致程序性能下降。

    func add(a int) func() {
    	b := 10
    	fmt.Printf("add point:%p\n", &a)
    	return func() {
    		fmt.Printf("lamda point:%p\n", &a)
    		println(a + b)
    	}
    }
    
    func main() {
    	a := 10
    	fmt.Printf("main point:%p\n", &a)
    	f := add(a)
    	f()
    }
    main point:0xc000018088
    add point:0xc0000180c0
    lamda point:0xc0000180c0
    20  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在add函数中定义的匿名函数可以引用add函数的形参a以及add函数中定义的变量b,并且无拷贝。

    闭包的原理
    外部函数调用时,会创建相应的作用域链,函数执行完毕,其作用域链销毁,内部函数的作用域链仍然在引用这个活动对象,内部函数将外部函数的活动对象加到自己的作用链中,只有内部函数被销毁后,活动对象才会被销毁。

    优缺点
    缺点:占内存,使用不当会导致内存泄漏;
    优点:防止变量污染,内部函数可以访问外部函数的变量。

    延迟调用

    语句defer 向当前函数注册稍后执行的函数调用,这些调用被称为延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作。

    延迟调用注册的是调用,必须提供执行所需参数,参数值在注册时被复制并缓存起来,如对状态敏感,可改用指针或闭包。

    func main() {
    	x, y := 1, 2
    	defer func(a int) {
    		println("defer x,y = ", a, y) // y为闭包引用
    	}(x) // 注册时复制调用参数
    
    	defer func(b int) {
    		println("defer x,y = ", x, b) // x为闭包引用
    	}(y)
    
    	x += 100 // 对x的修改不会影响延迟调用
    	y += 100
    	println(x, y)
    
    	101 102
    	defer x,y =  101 2
    	defer x,y =  1 102
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    多次延迟注册按FILO次序执行。
    编译器通过插入额外指令来实现延迟调用,而return和panic语句都会终止当前函数流程,引发延迟调用。

    func test() (z int) {
    	defer func() {
    		println("defer: ", z)
    		z += 100 // 修改命名返回值
    	}()
    	return 100  // 实际执行次序 z=100 call defer ret
    }
    func main() {
    	println("test: ", test())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    误用

    延迟调用在函数结束时才被执行,不合理的使用方式会浪费更多资源,甚至造成逻辑错误。

    func main() {
    	for i := 0; i < 10000; i++ {
    		path:=fmt.Sprintf("./log/%d.txt",i)
    		f,err := os.Open(path)
    		if err!=nil{
    			log.Println(err)
    			continue
    		}
    		
    		// 这个关闭操作在main函数结束时才会执行,而不是当前循环中执行
    		// 无端延长了逻辑结束时间和f的生命周期,平白多消耗了内存等资源
    		defer f.Close()
    		
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    应该直接调用或重构为函数,将循环和处理算法分离

    func main() {
    	
    	do := func(n int) {
    		path := fmt.Sprintf("./log/%d.txt", i)
    		f, err := os.Open(path)
    		if err != nil {
    			log.Println(err)
    		}
    
    		// 该延迟调用在do函数结束时执行,而非main
    		defer f.Close()
    	}
    	
    	for i := 0; i < 10000; i++ {
    		do(i)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    错误处理

    error

    标准库将error定义为接口类型,以便实现自定义错误类型

    type error interface{
    	Error() string
    }
    
    • 1
    • 2
    • 3

    按惯例,error总是最后一个返回参数,标准库提供了相关创建函数,可方便创建包含简单错误文本的error对象。

    import (
    	"errors"
    	"log"
    )
    
    var errDivByZero = errors.New("division by zero")
    
    func div(x, y int) (int, error) {
    	if y == 0 {
    		return 0, errDivByZero
    	}
    	return x / y, nil
    }
    
    func main() {
    	z, err := div(5, 0)
    	if err == errDivByZero {
    		log.Fatalln(err)
    	}
    	println(z)
    
    	2022/09/18 15:40:15 division by zero
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    错误变量通常以err作为前缀,且字符串内容全部小写,没有结束标点,以便嵌入到其他格式化字符串中输出。

    基于错误类型判断

    type DivError struct {
    	x, y int
    }
    
    func (DivError) Error() string {
    	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(e, e.x, e.y)
    		default:
    			fmt.Println(e)
    		}
    		log.Fatalln(err)
    	}
    	println(z)
    	
    	// division by zero 5 0
    	//2022/09/18 15:48:50 division by zero
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    panic,recover

    与error相比,panic/recover在使用方法上更接近try/catch结构化异常。panic会立即中断当前函数流程,执行延迟调用,而在延迟调用函数中,recover可捕获并返回panic提交的操作对象。

    func main() {
    	defer func() {
    		if err := recover(); err != nil { // 捕获错误
    			log.Fatalln(err)
    		}
    	}()
    
    	panic("i am dead") // 引发错误
    	println("exit.")   // 永远不会执行
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    无论是否执行recover,所有延迟调用都会被执行,但是中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃。

    func test() {
    	defer println("test.1")
    	defer println("test.2")
    	panic("i am dead")
    }
    
    func main() {
    	defer func() {
    		log.Println(recover())
    	}()
    
    	defer func() {
    		panic("you are dead")
    	}()
    	test()
    
    test.2
    test.1
    2022/09/18 16:10:51 you are dead
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    若连续调用panic,仅最后一个会被recover捕获,并且在延迟函数中panic,不会影响后续延迟调用执行,而recover之后的panic,可被再次捕获,另外,recover必须在延迟调用函数中执行才能正常工作。

  • 相关阅读:
    双点重发布+路由策略实验
    安全学习DAY23_Cookie&Session&Token
    【栈与队列面试题】有效的括号(动图演示)
    IDEA JAVA项目 导入JAR包,打JAR包 和 JAVA运行JAR命令提示没有主清单属性
    光学3D表面轮廓仪微纳米三维形貌一键测量
    原生Javascript(数组操作方法总结)-更新
    GR5515 使用心得纪录片
    Android 10.0 禁用插入耳机时弹出的保护听力对话框
    【C语言】32个关键字
    Spring MVC Http Event Stream
  • 原文地址:https://blog.csdn.net/mashaokang1314/article/details/126909765