[Go语言]函数介绍

Posted jiangwei0512

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Go语言]函数介绍相关的知识,希望对你有一定的参考价值。

函数

函数是Go语言中的“第一公民”,具体表现:

  • 函数是一种类型,函数类型变量可以像其它类型变量一种使用,可以作为其它函数的参数或返回值,也可以直接调用运行;
  • 函数支持多值返回;
  • 支持闭包;
  • 支持可变参数。

基本概念

函数格式:

func funcName(param-list) (return-list) {
	func-body
}

参数列表中的()是必须的,但是参数可以是空;返回值列表中的()在没有返回值、只有一个非命名返回值的情况下可以不需要;{必选在同行的行尾。

函数特性:

  • 函数的首字母大小写决定该函数在其它包的可见性:大写时其它包可见(一个例子就是fmt.Printf()),小写时只有相同的包可以访问。
  • 函数可以没有输入参数,也可以没有返回值:
func func1() {
	// do nothing
}
  • 多个相邻的相同类型的参数可以使用简写模式:
func func2(a, b int) {
	// do nothing
}
  • 支持有名的返回值:
func add(a, b int) (value int) {
	value = a + b
	return // return还是要的,但是不用写return X
}
  • 不支持默认值参数。
  • 不支持函数重载。
  • 不支持函数嵌套,严格地说不支持命名函数的嵌套定义,但是支持嵌套的匿名函数。
  • 支持多个值返回:
func func3(a, b int) (int, int) {
	a++
	b++
	return a, b
}
  • 函数实参到形参的传递永远是值拷贝。
  • 支持不定参数:
func sum(arr ...int) (sum int) {
	for _, v := range arr {
		sum += v
	}
	return
}

arr ...int就是不定参数,arr是参数名,int是参数类型,...不能少。关于不定参数:

  1. 所有的不定参数类型必须是相同的;
  2. 不定参数必须是函数的最后一个参数;
  3. 不定参数名在函数体内相当于切片;
  4. 切片可以作为参数传递给不定参数,切片名后要加上...
func main() {
	s := []int{1, 2, 3}
	fmt.Printf("%d\\n", sum(s...))	// s之后的...不能少
}
  1. 形参为不定参数的函数和形参为切片的函数类型不相同。

匿名函数

函数类型又叫函数签名%T可以用来打印函数签名:

func main() {
	fmt.Printf("%T\\n", sum)	// 打印:func(...int) int
}

两个函数类型相同的条件是:拥有相同的形参列表和返回值列表。

可以使用type定义函数类型:

type Op func(int, int) int // 定义了一个函数类型Op

匿名函数可以看作函数字面值。

func wrapper(op string) func(int, int) int {
	switch op {
	case "add":
		return func(a, b int) int {
			return a + b
		}
	case "sub":
		return func(a, b int) int {
			return a - b
		}
	default:
		return nil
	}
}

这里的返回值就是匿名函数。

defer

defer是Go语言关键字,它用来注册多个延迟调用,这些调用按照FIFO的顺序在函数返回(return,如果是os.Exit()则不会调用)前调用。

defer后接函数或者方法,不能是语句。示例:

func main() {
	defer func() {
		fmt.Printf("First defer\\n")
	}()

	defer func() {
		fmt.Printf("Second defer\\n")
	}()

	fmt.Printf("Call first\\n")

	return
}

注意函数最后的()不能少。打印结果:

Call first
Second defer
First defer

defer常用于保证一些资源最终一定会被回收或释放。

defer的函数的实参在注册时通过值拷贝传递进去。

defer语句的位置不当,有可能导致panic,一般defer语句放在错误检查语句(if err != nil)之后。

defer的一些副作用:

  • defer会导致资源释放延迟,尽量不要放在循环语句中;
  • defer相对于普通函数调用有一定的性能损耗。

defer函数中最好不要对有名返回值参数进行操作,否则会引发匪夷所思的结果。

闭包

闭包 = 函数 + 引用的外部环境。外部环境指的是包含闭包的函数的局部变量(入参和内部变量)和全局变量。

闭包对外部环境变量是直接引用,所以在闭包中的修改会同步到外部。

package main

func fa(a int) func(i int) int {
	return func(i int) int {
		println(&a, a) // a是外部环境变量,所以这里返回的是闭包
		a = a + i
		return a
	}
}

func main() {
	f := fa(1) // f是一个闭包,这里的1其实就是a
	g := fa(1) // g也是一个闭包,也有一个a,但是跟f的a不是同一个

	println(f(1)) // 这里的1是i,是闭包的参数,闭包里面打印0xc000067f60 1,这里打印2
	println(f(1)) // 闭包里面打印0xc000067f60 2,这里打印3

	println(g(1)) // 闭包里面打印0xc000067f58 1,这里打印2
	println(g(1)) // 闭包里面打印0xc000067f58 2,这里打印3
}

以上是包含闭包的函数的局部变量被修改的例子,下面是对全局变量修改的例子:

var a = 1

func fb() func(i int) int {
	return func(i int) int {
		println(&a, a) // a也是外部环境变量,所以这里返回的是闭包
		a = a + i
		return a
	}
}

func main() {
	f := fb()
	g := fb()

	println(f(1)) // 闭包里面打印0x940180 1,这里打印2
	println(f(1)) // 闭包里面打印0x940180 2,这里打印3

	println(g(1)) // 闭包里面打印0x940180 3,这里打印4
	println(g(1)) // 闭包里面打印0x940180 4,这里打印5
}

这里可以看到全局变量a的值会一直增加。

使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。

同一个函数返回的多个闭包共享该函数的局部变量:

func fc(base int) (func(int) int, func(int) int) {
	println(&base, base)

	add := func(i int) int {
		base += i
		println(&base, base)
		return base
	}

	sub := func(i int) int {
		base -= i
		println(&base, base)
		return base
	}

	return add, sub
}

func main() {
	f, g := fc(10)	// 0xc000067f40 10
	println(f(1))	// 闭包内打印0xc000067f40 11,这里打印11
	println(g(1))	// 闭包内打印0xc000067f40 10,这里打印10
}

这里可以看到base的值先增1后减1,且是是通过不同的闭包完成的。

panic和recover

panic()recover()是Go语言的内置函数(builtin\\builtin.go):

// The panic built-in function stops normal execution of the current
// goroutine. When a function F calls panic, normal execution of F stops
// immediately. Any functions whose execution was deferred by F are run in
// the usual way, and then F returns to its caller. To the caller G, the
// invocation of F then behaves like a call to panic, terminating G's
// execution and running any deferred functions. This continues until all
// functions in the executing goroutine have stopped, in reverse order. At
// that point, the program is terminated with a non-zero exit code. This
// termination sequence is called panicking and can be controlled by the
// built-in function recover.
func panic(v interface{})

// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
func recover() interface{}

panic的参数是一个空接口类型interface{},所以任意类型的变量都可以传递给panic()函数。

引发panic的情况:

  • 程序主动调用panic函数:
    • 程序无法正常执行下去时调用;
    • 调试程序时调用;
  • 程序产生运行时错误时由运行时检测并抛出。

发生panic后,会逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,直到被recover捕获或运行到最外层函数而退出。

recover()defer一起使用,recover()只有在defer后面的函数体内被直接调用才能捕获panic终止异常。

package main

import "fmt"

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	defer func() {
		panic("first defer panic")
	}()

	defer func() {
		panic("second defer panic")
	}()

	panic("main body panic")
}

可以有连续多个panic被抛出,连续多个panic的场景只能出现在延迟调用里面,否则不会出现多个panic被抛出的场景,但是只有最后一个panic能被捕获,所以上面程序的打印结果是:

first defer panic

函数不能捕获内部新启动的goroutine所抛出的panic

错误处理

Go语言内置错误接口类型error(builtin\\builtin.go):

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
	Error() string
}

错误处理的最佳实践:

  • 在多个返回值的函数中,error通常作为函数最后的一个返回值;
  • 如果一个函数返回error类型变量,则先用if语句处理error != nil,正常逻辑放到if语句块的后面;
  • defer语句应该放到error判断的后面,不然有可能产生panic
  • 在错误逐级向上传递的过程中,错误信息应该不断地丰富和完善,而不是简单地抛出下层调用的错误。

以上是关于[Go语言]函数介绍的主要内容,如果未能解决你的问题,请参考以下文章

Go语言中init()函数介绍及执行顺序

go语言基础之init函数的介绍

Go语言特殊函数介绍

Go语言基础之函数

Go 函数式编程篇:函数使用入门和常用内置函数介绍

Go语言 可变参数(变参函数)