【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)
}

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

建议命名规则

  • 通常是动词和介词加上名词,例如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)
}

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

变参

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

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)

值传递和指针传递的区别

// 指针传递
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

返回值

有返回值的函数,必须有明确的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  

在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

}

多次延迟注册按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())
}
误用

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

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()
		
	}
}

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

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)
	}
}

错误处理

error

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

type error interface{
	Error() string
}

按惯例,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

}

错误变量通常以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
}

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.")   // 永远不会执行
}

无论是否执行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

}

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值