Golang通脉之函数

Golang通脉之函数,第1张

概述函数是组织好的、可重复使用的、用于执行指定任务的代码块。 Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。 函数定义 Go语言中定义函数使用func关键字,具体格式如下: f

函数是组织好的、可重复使用的、用于执行指定任务的代码块。

Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

函数定义

Go语言中定义函数使用func关键字,具体格式如下:

func 函数名(参数)(返回值){    函数体}

其中:

函数声明:关键字func函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名称不能重名。函数参数:参数由参数变量和参数变量的类型组成,参数变量可以省略,可以有一个参数,也可以有多个,也可以没有;多个参数之间使用,分隔;多个参数时参数变量要么全写,要么全省略;如果多个相邻参数的类型是一样的,可以只保留同一类型最后一个参数的声明。函数返回值:返回值由返回值变量和其变量类型组成,返回值变量可以省略,可以有一个返回值,也可以有多个,也可以没有;多个返回值必须用()包裹,并用,分隔;多个返回值时返回值变量要么全写,要么全省略。函数体:实现指定功能的逻辑。

定义一个求两个数之和的函数:

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

函数的参数和返回值都是可选的,实现一个既不需要参数也没有返回值的函数:

func printf() {	fmt.Println("printf函数")}
函数的调用

定义了函数之后,可以通过函数名()的方式调用函数。 调用上面定义的两个函数:

func main() {	printf()	ret := add(10,20)	fmt.Println(ret)}

注意,调用有返回值的函数时,可以不接收其返回值。

参数参数使用

形式参数:定义函数时,用于接收外部传入的数据,叫做形式参数,简称形参。

实际参数:调用函数时,传给形参的实际的数据,叫做实际参数,简称实参。

函数调用:

A:函数名称必须匹配B:实参与形参必须一一对应:顺序,个数,类型
类型简写

函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:

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

如果多个相邻参数的类型是一样的,可以只保留同一类型最后一个参数的声明,add函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型。

可变参数

函数的参数数量是可变的,比如最常见的 fmt.Println 函数。Go语言中的可变参数通过在参数名后加...来标识。可变参数在函数体中是切片类型

注意:可变参数通常要作为函数的最后一个参数

举个例子:

func add(x ...int) int {	fmt.Println(x) //x是一个切片	sum := 0	for _,v := range x {		sum = sum + v	}	return sum}

调用上面的函数:

ret1 := add()ret2 := add(10)ret3 := add(10,20)ret4 := add(10,20,30)fmt.Println(ret1,ret2,ret3,ret4) //0 10 30 60

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,示例代码如下:

func add(x int,y ...int) int {	fmt.Println(x,y)	sum := x	for _,v := range y {		sum = sum + v	}	return sum}

调用上述函数:

ret5 := add(100)ret6 := add(100,10)ret7 := add(100,10,20)ret8 := add(100,30)fmt.Println(ret5,ret6,ret7,ret8) //100 110 130 160

本质上,函数的可变参数是通过切片来实现的。

参数传递

go语言函数的参数也是存在值传递和引用传递

值传递

func main(){   /* 声明函数变量 */   getSquareRoot := func(x float64) float64 {      return math.Sqrt(x)   }   /* 使用函数 */   fmt.Println(getSquareRoot(9))}

引用传递(涉及指针知识点)

这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内 存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针:

//简单的一个函数,实现了参数+1的 *** 作func add1(a *int) int { // 请注意,    *a = *a+1 // 修改了a的值    return *a // 返回新值} func main() {    x := 3    fmt.Println("x = ",x) // 应该输出 "x = 3"    x1 := add1(&x) // 调用 add1(&x) 传x的地址    fmt.Println("x+1 = ",x1) // 应该输出 "x+1 = 4"    fmt.Println("x = ",x) // 应该输出 "x = 4"}
传指针使得多个函数能 *** 作同一个对象。传指针比较轻量级 (8bytes),只是传内存地址,可以用指针传递体积大的结构体。如果用参数值传递的话,在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以要传递大的结构体的时候,用指针是一个明智的选择。Go语言中slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)返回值

Go语言中通过return关键字向外输出返回值。

多返回值

Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来:

func swap(x,y string) (string,string) {   return y,x}
返回值命名

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回:

func SumAndProduct(a,b int) (add int,multiplIEd int) {    add = a + b    multiplIEd = a * b    return}
返回值补充

当一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。

func someFunc(x string) []int {	if x == "" {		return nil // 没必要返回[]int{}	}	...}
空白标识符

_ 是Go中的空白标识符。它可以代替任何类型的任何值。

比如rectProps函数返回的结果是面积和周长,如果只要面积,不要周长,就可以使用空白标识符:

func rectProps(length,wIDth float64) (float64,float64) {      var area = length * wIDth    var perimeter = (length + wIDth) * 2    return area,perimeter}func main() {      area,_ := rectProps(10.8,5.6) // perimeter is discarded    fmt.Printf("Area %f ",area)}
函数进阶变量作用域

作用域:变量可以使用的范围。

全局变量

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 所有的函数都可以使用,而且共享这一份数据。

//定义全局变量numvar num int64 = 10func testGlobalVar() {	fmt.Printf("num=%d\n",num) //函数中可以访问全局变量num}func main() {	testGlobalVar() //num=10}
局部变量

局部变量又分为两种: 变量在哪里定义,就只能在哪个范围使用,超出这个范围,变量就被销毁了:

func testLocalVar() {	//定义一个函数局部变量x,仅在该函数内生效	var x int64 = 100	fmt.Printf("x=%d\n",x)}func main() {	testLocalVar()	fmt.Println(x) // 此时无法使用变量x}

如果局部变量和全局变量重名,优先访问局部变量。

package mainimport "fmt"//定义全局变量numvar num int64 = 10func testNum() {	num := 100	fmt.Printf("num=%d\n",num) // 函数中优先使用局部变量}func main() {	testNum() // num=100}

语句块定义的变量:通常会在if条件判断、for循环、switch语句上使用这种定义变量的方式。

func testLocalVar2(x,y int) {	fmt.Println(x,y) //函数的参数也是只在本函数中生效	if x > 0 {		z := 100 //变量z只在if语句块生效		fmt.Println(z)	}	//fmt.Println(z)//此处无法使用变量z}

for循环语句中定义的变量,也是只在for语句块中生效:

func testLocalVar3() {	for i := 0; i < 10; i++ {		fmt.Println(i) //变量i只在当前for语句块中生效	}	//fmt.Println(i) //此处无法使用变量i}
函数类型与变量定义函数类型

可以使用type关键字来定义一个函数类型,具体格式如下:

type calculation func(int,int) int

上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。

简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。

func add(x,y int) int {	return x + y}func sub(x,y int) int {	return x - y}

add和sub都能赋值给calculation类型的变量。

var c calculationc = add
函数类型变量

声明函数类型的变量并且为该变量赋值:

func main() {	var c calculation               // 声明一个calculation类型的变量c	c = add                         // 把add赋值给c	fmt.Printf("type of c:%T\n",c) // type of c:main.calculation	fmt.Println(c(1,2))            // 像调用add一样调用c	f := add                        // 将函数add赋值给变量f1	fmt.Printf("type of f:%T\n",f) // type of f:func(int,int) int	fmt.Println(f(10,20))          // 像调用add一样调用f}
高阶函数

高阶函数分为函数作为参数和函数作为返回值两部分。

函数作为参数

函数可以作为参数:

func add(x,y int) int {	return x + y}func calc(x,y int,op func(int,int) int) int {	return op(x,y)}func main() {	ret2 := calc(10,add)	fmt.Println(ret2) //30}
函数作为返回值

函数也可以作为返回值:

func do(s string) (func(int,int) int,error) {	switch s {	case "+":		return add,nil	case "-":		return sub,nil	default:		err := errors.New("无法识别的 *** 作符")		return nil,err	}}
匿名函数和闭包匿名函数

函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

func(参数)(返回值){    函数体}

匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

func main() {	// 将匿名函数保存到变量	add := func(x,y int) {		fmt.Println(x + y)	}	add(10,20) // 通过变量调用匿名函数	//自执行函数:匿名函数定义完加()直接执行	func(x,y int) {		fmt.Println(x + y)	}(10,20)}

匿名函数多用于实现回调函数和闭包。

闭包

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境

func adder() func(int) int {	var x int	return func(y int) int {		x += y		return x	}}func main() {	var f = adder()	fmt.Println(f(10)) //10	fmt.Println(f(20)) //30	fmt.Println(f(30)) //60	f1 := adder()	fmt.Println(f1(40)) //40	fmt.Println(f1(50)) //90}

变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。 闭包进阶示例1:

func adder2(x int) func(int) int {	return func(y int) int {		x += y		return x	}}func main() {	var f = adder2(10)	fmt.Println(f(10)) //20	fmt.Println(f(20)) //40	fmt.Println(f(30)) //70	f1 := adder2(20)	fmt.Println(f1(40)) //60	fmt.Println(f1(50)) //110}

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {	return func(name string) string {		if !strings.HasSuffix(name,suffix) {			return name + suffix		}		return name	}}func main() {	jpgFunc := makeSuffixFunc(".jpg")	txtFunc := makeSuffixFunc(".txt")	fmt.Println(jpgFunc("test")) //test.jpg	fmt.Println(txtFunc("test")) //test.txt}

闭包进阶示例3:

func calc(base int) (func(int) int,func(int) int) {	add := func(i int) int {		base += i		return base	}	sub := func(i int) int {		base -= i		return base	}	return add,sub}func main() {	f1,f2 := calc(10)	fmt.Println(f1(1),f2(2)) //11 9	fmt.Println(f1(3),f2(4)) //12 8	fmt.Println(f1(5),f2(6)) //13 7}

闭包其实并不复杂,只要牢记闭包=函数+外层变量的引用

defer语句

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

func main() {	fmt.Println("start")	defer fmt.Println(1)	defer fmt.Println(2)	defer fmt.Println(3)	fmt.Println("end")}

输出结果:

startend321

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

defer执行时机

在Go语言的函数中return语句在底层并不是原子 *** 作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值 *** 作后,RET指令执行前。具体如下图所示:

defer经典案例

阅读下面的代码,写出最后的打印结果。

func f1() int {	x := 5	defer func() {		x++	}()	return x}func f2() (x int) {	defer func() {		x++	}()	return 5}func f3() (y int) {	x := 5	defer func() {		x++	}()	return x}func f4() (x int) {	defer func(x int) {		x++	}(x)	return 5}func main() {	fmt.Println(f1()) //5	fmt.Println(f2()) //6	fmt.Println(f3()) //5	fmt.Println(f4()) //5}
defer面试题
func calc(index string,a,b int) int {	ret := a + b	fmt.Println(index,b,ret)	return ret}func main() {	x := 1	y := 2	defer calc("AA",x,calc("A",y))	x = 10	defer calc("BB",calc("B",y))	y = 20}

问,上面代码的输出结果是?(提示:defer注册要延迟执行的函数时该函数所有的参数都需要确定其值)

defer注意点
当外围函数中的语句正常执行完毕时,只有其中所有的延迟函数都执行完毕,外围函数才会真正的结束执行。当执行外围函数中的return语句时,只有其中所有的延迟函数都执行完毕后,外围函数才会真正返回。当外围函数中的代码引发运行恐慌时,只有其中所有的延迟函数都执行完毕后,该运行时恐慌才会真正被扩展至调用函数。
内置函数介绍@H_301_469@
内置函数介绍
close主要用来关闭channel
len用来求长度,比如string、array、slice、map、channel
new用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make用来分配内存,主要用来分配引用类型,比如chan、map、slice
append用来追加元素到数组、slice中
panic和recover用来做错误处理
panic/recover

Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。 panic可以在任何地方引发,但recover只有在defer调用的函数中有效。 首先来看一个例子:

func funcA() {	fmt.Println("func A")}func funcB() {	panic("panic in B")}func funcC() {	fmt.Println("func C")}func main() {	funcA()	funcB()	funcC()}

输出:

func Apanic: panic in Bgoroutine 1 [running]:main.funcB(...)        .../code/func/main.go:12main.main()        .../code/func/main.go:20 +0x98

程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候就可以通过recover将程序恢复回来,继续往后执行。

func funcA() {	fmt.Println("func A")}func funcB() {	defer func() {		err := recover()		//如果程序出出现了panic错误,可以通过recover恢复过来		if err != nil {			fmt.Println("recover in B")		}	}()	panic("panic in B")}func funcC() {	fmt.Println("func C")}func main() {	funcA()	funcB()	funcC()}

注意:

recover()必须搭配defer使用。defer一定要在可能引发panic的语句之前定义。 总结

以上是内存溢出为你收集整理的Golang通脉之函数全部内容,希望文章能够帮你解决Golang通脉之函数所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/langs/1249937.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-06-07
下一篇2022-06-07

发表评论

登录后才能评论

评论列表(0条)

    保存