go-函数与方法 4

一、函数特点

  • 无需声明原型。
  • 支持不定 变参。
  • 支持多返回值。
  • 支持命名返回参数。
  • 支持匿名函数和闭包。
  • 函数也是一种类型,一个函数可以赋值给变量。
  • 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
  • 不支持 重载 (overload)
  • 不支持 默认参数 (default parameter)。

二、函数声明

函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。函数可以没有参数或接受多个参数。

语法

func function_name( [parameter list] ) [return_types] { 
	函数体
}

栗子

func main() {
	a, b := test(1, 2, "sum")
	fmt.Println("a,b : ", a, b)
	dome()
}
func test(x, y int, s string) (int, string) {
	// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
	n := x + y
	return n, s
}
func dome() {
	fmt.Println("dome")
}

注意类型在变量名之后 。

当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。函数可以返回任意数量的返回值。使用关键字 func 定义函数,左大括号依旧不能另起一行。

package main
import "fmt"
func test(fn func() int) int {
	return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string

func format(fn FormatFunc, s string, x, y int) string {
	return fn(s, x, y)
}

func main() {
	s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
	s2 := format(func(s string, x, y int) string {
		return fmt.Sprintf(s, x, y)
	}, "%d, %d", 10, 20)
	
	println(s1)
	println(s2)
}
100
10,20

三、参数

函数参数
函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

func swap(x, y int) int {
	// ... ...
}

引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

package main
import (
	"fmt"
)
/* 定义相互交换值的函数 */
func swap(x, y *int) {
	var temp int
	temp = *x /* 保存 x 的值 */
	*x = *y /* 将 y 值赋给 x */
	*y = temp /* 将 temp 值赋给 y*/
}
func main() {
	var a, b int = 1, 2
	/* 调用 swap() 函数
	&a 指向 a 指针,a 变量的地址
	&b 指向 b 指针,b 变量的地址
	*/
	swap(&a, &b)
	fmt.Println(a, b)
}

2 1

在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
注意2:map、slice、chan、指针、interface默认以引用的方式传递。

不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。
在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可

func myfunc(args ...int) { //0个或多个参数
}
func add(a int, args…int) int { //1个或多个参数
}

栗子

package main
import "fmt"
func main() {
	fmt.Println(test(1, 2, 3, 4))
}
func test(args ...int) string {
	return fmt.Sprintf("args : %d", args)
}

注意:其中args是一个slice,我们可以通过args[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.

任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。
用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。

func myfunc(args ...interface{}) {
}

返回值

** "_"标识符,用来忽略函数的某个返回值 **

Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。返回值的名称应当具有一定的意义,可以作为文档使用。没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。

Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 “_” 忽略。

package main
import (
	"fmt"
)
func calc(a,b int) (num,avg int) {
	num = a + b
	avg = (a+b)/2
	return
}
func main() {
	var a, b int = 1, 2
	_,avg := calc(a,b)
	fmt.Println(avg)
}

1

四、匿名与闭包

匿名函数

在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

n := func(a int) int {
	a++
	return a
}
fmt.Println(n)

0x187740

碰巧匿名函数与上了切片

fns := [](func(x int) int){ //(func(x int) int) 匿名函数类型
	func(x int) int {return x+1},    //0
	func(x int) int {return x+2},    //1
}
fmt.Println(fns[1](10))

12

闭包

一个令人熟悉的闭包

<?php
function test() {
	return function () {
		var_dump("闭包");
	};
}
test()();
?>

go闭包操作

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}
func main() {
	te := adder()
	for i:=0;i<5;i++{
		fmt.Println(te(i))
	}
}

0
1
3
6
10

作用域问题

func a() func(){
	i := 0
	b := func() {
		i++
		fmt.Println(i)
	}
	return b
}
func main(){
	c := a()
	c()
	c()
	c()
}
1
2
3

解释
当函数a()的内部函数b()被函数a()外的一个变量引用的时候,就创建了一个闭包。 在上面的例子中,由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次执行c(),i都是自加1后的值。 从上面可以看出闭包的作用就是在a()执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a()所占用的资源,因为a()的内部函数b()的执行需要依赖a()中的变量i。

在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。 下面来想象另一种情况,如果a()返回的不是函数b(),情况就完全不同了。因为a()执行完后,b()没有被返回给a()的外界,只是被a()所引用,而此时a()也只会被b()引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用a();是页面并没有信息输出。

下面来说闭包的另一要素引用环境。c()跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数a()每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。这和c()和c()的调用顺序都是无关的。

五、延迟调用(defer)

介绍defer

defer 特性

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

defer用途

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放

go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。
defer 是先进后出
这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。

测试

func main() {
	def()
}
func def() {
	defer func() {
		fmt.Println("延迟输出")
	}()
	fmt.Println("正常输出")
}

正常输出
延迟输出

defer 与 结构体

这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子.

package main
import "fmt"
type Asd struct {
	name string
}
func (t *Asd) Aaa() {
	fmt.Println(t.name)
}
func main() {
	tt := []Asd{
		{"a"},
		{"b"},
		{"c"},
	}
	for _,val := range tt{
		defer val.Aaa()
	}
}
c
c
c

这个输出并不会像我们预计的输出c b a,而是输出c c c可是按照前面的go spec中的说明,应该输出c b a才对啊.
那我们换一种方式来调用一下.

package main
import "fmt"
type Asd struct {
	name string
}
func (t *Asd) Aaa() {
	fmt.Println(t.name)
}
func demo(t Asd) {
	t.Aaa()
}

func main() {
	tt := []Asd{
		{"a"},
		{"b"},
		{"c"},
	}
	for _,val := range tt{
		defer demo(val)
	}
}
c
b
a

这个时候输出的就是c b a当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a 看似多此一举的声明

package main
import "fmt"
type Asd struct {
	name string
}
func (t *Asd) Aaa() {
	fmt.Println(t.name)
}

func main() {
	tt := []Asd{
		{"a"},
		{"b"},
		{"c"},
	}
	for _,val := range tt{
		v := val
		defer v.Aaa()
	}
}
c
b
a

通过以上例子,结合

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.

这句话。可以得出下面的结论:
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。


func ww(x int) {
	defer fmt.Println("q")
	defer fmt.Println("w")

	defer func() {
		fmt.Println(100/x)
	}()

	defer fmt.Println("e")
}
ww(0)

e
w
q
panic: runtime error: integer divide by zero

六、异常处理

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

panic

1、内置函数
2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
4、直到goroutine整个退出,并报告错误

recover

1、内置函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议
a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error

注意:

1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。

栗子

defer func() {
	if err := recover(); err != nil {
		fmt.Println(err.(string))
	}
}()
panic("panic error !")

panic error !

底层recover、panic都是interface{}类型,所以可以跑出任何异常对象

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

如果defer也需要抛出异常咋办?

	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err.(string))
		}
	}()
	defer func() {
		panic("defer panic error !")
	}()
	panic("panic error !")
	
defer panic error !

我们也可以实现一个go类型try

/**
* fun 正常的代码执行的方法
*
* handler 出现异常执行的操作
*/
func Try(fun func(), handler func(interface{})) {
	defer func() {
		if err := recover(); err != nil {
			handler(err)
		}
	}()
	fun()
}
func main() {
	Try(func() {
		panic("test try panic")
	}, func(err interface{}) {
		fmt.Println(err)
	})
}

下一篇:go设计模式 5-1
上一篇:结构体和面对对象 3

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值