Go语言笔记-函数

函数

定义

关键字func用于定义函数。 - 无须前置声明。 - 不支持命名嵌套类型(nested)。 - 不支持同名参数重载(overload)。 - 不支持默认参数。 - 支持不定长变参。 - 支持多返回值。 - 支持命名返回值。 - 支持命名函数和闭包。 函数属于第一类对象,具备相同签名的函数视作同一类型。 > 第一类对象指可在运行期间创建,可做函数参数或返回值,可存入变量的实体。 函数只能判断是否为nil,不支持其他比较。 从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存。

##参数 函数调用时,必须按照签名顺序传参,以_命名的参数也不能省略。 在参数列表里,相邻的同类型参数可以合并。

func test(x,y int,s string,_ bool) *int{
	return nil
}

func main(){
	test(1,2,"abc")//error:not enough arguments in call to test
}

形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参类似函数局部变量,而实参则是函数外部对象。

在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝给形参内存。

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

output

pointer: 0xc04206a018,target: 0xc04204c080
pointer: 0xc04206a028,target: 0xc04204c080

从输出结果来看,尽管形参和实参都指向同一目标,但是指针传递依然被复制。所以可以认为值传递和指针传递都是一种值拷贝传递。 如果函数参数过多,可以将其重构为一个结构体,也算变相实现可选参数和命名的功能。

变参

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

func test(s string, a ...int) {
	fmt.Printf("%T, %v\n", a, a)
}

func main() {
	test("abc", 1, 2, 3, 4)
}

output

[]int, [1 2 3 4]

切片作为变参时,必须进行展开操作。如果是数组,先将其转换为切片。

func test(a ...int) {
	fmt.Println(a)
}

func main() {
	a := [3]int{10, 20, 30}//a是数组
	test(a[:]...)//将数组转换为slice后展开
}
func test(a ...int) {
	fmt.Println(a)
}

func main() {
	a := []int{10, 20, 30}//a是切片
	test(a...)
}

因为变参是切片,所以参数复制的只是切片自身,并不包括底层数组。

返回值

有返回值的额函数,必须有明确的return语句,除非有panic或者是无break的死循环。 错误写法:

func test(x int) int {
	if x>0{
		return 1
	} else if x<0{
		return -1
	}
	//error:missing return at the end of function
}

正确写法:

func test(x int) int {
	var a int
	if x > 0 {
		a = 1
	} else if x < 0 {
		a = -1
	}
	return a
}

函数支持多返回值,例如常见的error模式:

func div(x, y int) (int, error){
	if y==0{
		return 0,errors.New("division by zero")
	}	else {
		return x/y,nil
	}
}

命名返回值

命名返回值和命名参数一样,也可以作为局部变量来使用,最后由return隐式返回。

func div(x, y int) (z int, err error) {
	if y == 0 {
		err = errors.New("division by zero")
	} else {
		z = x / y
		err = nil
	}
	return
}

参数返回时,需要对所有命名值全部返回。 ##匿名函数 匿名函数是没有定义名字符号的函数。 匿名函数可以直接调用,保存到变量,作为参数或返回值。 直接使用:

func main(){
	func (s string) {
		println(s)
	} ("hello world")
}

赋值给变量:

func main(){
	add := func (x, y int) int {
		return x+y
	}
	println(add(1,2))
}

作为参数:

func test(f func()){
	f()
}

func main(){
	test(func() {
		println("hello world")
	})
}

作为返回值:

func test() func(int,int) int {
	return func (x,y int) int {
		return x+y
	}
}

func main() {
	add := test()
	println(add(1,2))
}

普通函数和匿名函数都可作为结构体字段,或经通道传递。

闭包

闭包是函数和引用环境的组合体。 闭包直接引用原环境变量。 闭包延迟求值的特性:

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

output:

0xc042040000 2
0xc042040000 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
}

func main() {
	for _, f := range test() {
		f()
	}
}

output:

0xc042008038 0
0xc042008040 1

延迟调用

defer向当前函数注册,等当前函数结束前才被执行。延迟调用常用于资源释放,解除锁定,以及错误处理等操作。 多个defer按照FILO。 returnpanic都会终止当前函数,引发deferreturn执行前会先更新返回值。

func test() (z int) {
	defer func() {
		println("defer:",z)
		z+=100//修改命名返回值
	}()
	return 100//实际执行次序 z=100,call defer return z
}

func main(){
	println("test:",test())
}

性能

延迟调用花费代价很大,此过程包括注册、调用等操作,还有额外的缓存开销。所以性能要求高压力大的算法,应避免使用延迟调用。

错误处理

error

官方推荐做法是返回error状态。 标准库将error定义为接口类型,以便实现自定义错误类型。

type error interface {
	Error() string
}

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

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

某些时候需要自定义错误类型。

type DivErr struct {
	x, y int
}

func (DivErr) Error() string {
	return "division by zero"
}

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, DivErr{x, y}
	}
	return x / y, nil
}

func main() {
	z, err := div(5, 0)
	if err != nil {
		switch e := err.(type) {
		case DivErr:
			fmt.Println(e, e.x, e.y)
		default:
			fmt.Println(e)
		}
		log.Fatalln(err)
	}
	println(z)
}

panic,recover

func panic(v interface{})
func recover() interface{}

panic/recover在使用上更接近try/catch结构化异常。他们是内置函数而非语句。panic会立即中断当前 函数流程,执行延迟调用。而在延迟调用中,recover可捕获并返回panic提交的错误对象。

func main() {
	defer func() {
		if err := recover(); err != nil {//捕获错误
			log.Fatalln(err)
		}
	}()
	panic("i am dead")//引发错误
	panic("exit")//不会执行
}

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

func test() {
	defer println("test.1")
	defer println("test.2")
	panic("i am dead")
}
func main() {
	defer func() {
		log.Println(recover())
	}()
	test()
}

output

test.2
test.1
i am dead

连续调用panic,仅最后一个会被recover捕获。

func main() {
	defer func() {
		for {
			if err := recover(); err != nil {
				log.Println(err)
			} else {
				log.Fatalln("fatal")
			}
		}
	}()
	defer func() {
		panic("you are dead") // 类似重新抛出异常( rethrow)
	}() // 可先 recover捕获,包装后重新抛出
	panic("i am dead")
}

output

you are dead
fatal

recover必须在延迟调用函数中才执行能正常工作。

func catch() {
	log.Println("catch ",recover())
}

func main(){
	defer log.Println(recover())
	defer catch()
	panic("i am ")
}

output

catch i am
nil
func catch() {
	log.Println("catch ", recover())
}

func main() {
	defer catch()
	defer log.Println(recover())
	panic("i am ")
}

output

nil
catch i am