go语言的函数

go语言的函数

函数基础

简介

在任何编程语言中,函数就是对功能的封装,是组织好的,可重复使用,用于执行指定功能的代码块。在go语言中,函数属于一等公民。

定义

在go语言当中,用func定义一个函数,格式:

func 函数名 (参数) (返回值) {
    函数体
}
  • 函数名:由数字、字母、下划线组成。但是不能以数字开头,在同一个包内,函数名不能重复。
  • 参数:一个参数由参数变量和参数类型组成,多个参数之间用,分隔。
  • 返回值:可以是返回值类型,也可以是返回值变量和返回值类型组成。多个返回值用()包裹。并,分隔。
  • 函数体:实现指定功能的代码块。

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

package main

import (
	"fmt"
)

// 定义一个求两数之和的函数
func add(a int, b int) int {
	return a + b
}

func main() {
    // 调用
	res := add(100, 200)
	// 300
	fmt.Println(res)
}

参数

参数简写

当两个相邻参数的类型相同时,可以简写,例如:

func add(a, b int) int {
	return a + b
}

可变参数

有时候我们不知道我们传的参数有多少个,可以通过不固定参数来解决。在参数名后面加...来标识。类似python中的 *args**kwargs

格式:

func 函数名 (固定参数, v ... T) (返回值) {
				函数体
			}
  • v:可变参数变量,类型为[]T ,也就是拥有多个元素的类型切片 ,v 和 t 之间用 ...
  • T:可变参数变量类型,当类型为interface{}的时候,传入的值可以是任意值。
可变参数为多个字符串
package main

import (
	"fmt"
)

func studentInfo(name string, age int, args ...string) {
	fmt.Println("固定参数:", name, age)
	fmt.Println("不固定参数:", args)

	// 通过遍历获取每一个参数
	for index, arg := range args {
		fmt.Printf("索引:%d  参数值:%s\n", index, arg)
	}
}
func main() {
	studentInfo("江子牙", 21, "江西省", "萍乡市", "莲花县", "坊楼镇", "小江村")
}

执行结果:

固定参数: 江子牙 21
不固定参数: [江西省 萍乡市 莲花县 坊楼镇 小江村]
索引:0  参数值:江西省
索引:1  参数值:萍乡市
索引:2  参数值:莲花县
索引:3  参数值:坊楼镇
索引:4  参数值:小江村
可变参数为空接口
package main

import (
	"fmt"
)

func studentInfo(name string, args ...interface{}) {
	fmt.Println("固定参数", name)
	fmt.Println("不固定参数", args)

	// 通过遍历获取每一个参数的类型
	for k, v := range args {

		fmt.Printf("索引:%d  \t参数的类型:%T\t参数值:%v\n", k, v, v)
	}

}
func main() {
	studentInfo("江子牙", 22, 99.9, "sex", true, map[string]string{
		"className": "高三(1)班",
		"score":     "99",
	})
}

执行结果:

固定参数 江子牙
不固定参数 [22 99.9 sex true map[className:高三(1)班 score:99]]
索引:0  	参数的类型:int	参数值:22
索引:1  	参数的类型:float64	参数值:99.9
索引:2  	参数的类型:string	参数值:sex
索引:3  	参数的类型:bool	参数值:true
索引:4  	参数的类型:map[string]string	参数值:map[className:高三(1)班 score:99]

本质上,可变参数是根据切片来实现的。

返回值

多返回值

go语言是支持多个返回值,如果有多个返回值就要用()包裹。例如:

package main

import "fmt"

// 定义一个求两数之和、两数之差的函数
func cacl(a, b int) (int, int) {
	sum := a + b
	sub := a - b
	return sum, sub
}

func main() {
	sum, sub := cacl(10, 10)
	// 两数之和 20
	fmt.Println("两数之和", sum)
	// 两数之差 0
	fmt.Println("两数之差", sub)
}

返回值命名

返回值可以和参数一样,由返回值变量和返回值变量类型组成。最后由return关键字返回。

package main

import "fmt"

// 定义一个求两数之和、两数之差的函数
func cacl(a, b int) (sum, sub int) {
	sum = a + b
	sub = a - b
	return
}

func main() {
	sum, sub := cacl(10, 10)
	// 两数之和 20
	fmt.Println("两数之和", sum)
	// 两数之差 0
	fmt.Println("两数之差", sub)
}

函数进阶

变量作用域

全局变量

全局变量定义在函数外部的变量。它在程序整个运行周期内都有效,在函数中可以访问到全局变量。

package main

import "fmt"

// 定义两个全局变量a 、b
var (
	a = 10
	b = 10
)

// 定义一个求两数之和、两数之差的函数
func cacl() (sum, sub int) {
	fmt.Println("两个全局变量:",a, b)
	sum = a + b
	sub = a - b
	return
}

func main() {
	sum, sub := cacl()
	// 两数之和 20
	fmt.Println("两数之和", sum)
	// 两数之差 0
	fmt.Println("两数之差", sub)
}

局部变量

函数内部定义的局部变量函数外部无法访问
package main

import "fmt"

func testLocalVar() {
	local := "我是函数内部的局部变量,函数外部无法访问我"
	fmt.Println(local)
}

func main() {
	testLocalVar()
	//fmt.Println(local) 访问不到
}

全局变量和局部变量同时存在

如果局部变量和全局变量同名,优先访问局部变量,就近原则。

package main

import "fmt"

//定义全局变量num
var num int64 = 10

func testNum() {
	num := 100
	//函数中优先使用局部变量
	fmt.Println("局部变量的num:", num)
}
func main() {
	testNum()
	fmt.Println("全局变量的num:", num)
}

函数类型与函数变量

函数也是一种类型,也可以赋值给一个变量保存起来。

函数变量
package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func main() {
	f:= add
	fmt.Printf("类型:%T\n", f)
	sum := f(1,2)
	fmt.Println(sum)
}

可以看出函数也是一种类型,就像上面的add函数,用变量名保存起来之后,打印它的类型,func(int, int) int,函数变量名加()也可以调用。

执行结果:

类型:func(int, int) int
3
函数类型

既然函数也是一种类型,那么也可以声明一个函数类型。

package main

import "fmt"

// 声明一个cacl函数类型
type cacl func(int, int) int

func add(a, b int) int {
	return a + b
}

func main() {
	var c cacl
	c = add
	fmt.Printf("类型:%T\n", c)
	sum := c(1, 2)
	fmt.Println(sum)
}

执行结果:

类型:main.cacl
3

高阶函数

满足其中一个条件为高阶函数

  • 函数作为参数传入
  • 函数作为返回值返回

函数作为参数

package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func cacl(a, b int, f func(int, int) int) int {
	sum := f(a, b)
	return sum
}

func main() {
	res := cacl(10, 10, add)
	fmt.Println(res)
}

函数作为返回值

package main

import (
	"errors"
	"fmt"
)

func sum(a, b int) int {
	return a + b
}

func sub(a, b int) int {
	return a - b
}

// 定义一个计算的函数,根据传入的操作字符串执行对应的加减
func cacl(do string) (func(int, int) int, error) {
	switch do {
	case "加":
		return sum, nil
	case "减":
		return sub, nil
	default:
		err := errors.New("不支持该操作")
		return nil, err
	}
}

func main() {
	do1, _ := cacl("加")
	sum := do1(1, 20)
	fmt.Println(sum)

	do2, _ := cacl("减")
	sub := do2(10, 8)
	fmt.Println(sub)

	do2, err := cacl("乘")
	fmt.Println(err)
}

执行结果:

21
2
不支持该操作

匿名函数

在其他语言中,函数内部还可以定义函数,即函数嵌套。但是go语言函数内部不能再像以前那样定义函数了,只能定义匿名函数。顾名思义就是没有函数名字的函数,常用于回调函数和闭包。

格式:

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

// 可以当做没有函数名字的普通函数定义

因为没有函数名了,不能和普通函数那样直接调用。匿名函数需要保存到某个变量或者立即作为函数执行。

保存为变量

定义一个匿名函数之后,保存到变量中。

package main

import "fmt"

func main() {
	add := func(a, b int) int {
		sum := a + b
		return sum
	}
	sum := add(1, 2)
	fmt.Println(sum)
}

立即执行

定义一个匿名函数之后,后面直接加(),立即执行。

package main

import "fmt"

func main() {
	sum := func(a, b int) int {
		sum := a + b
		return sum
	}(1, 2)
	fmt.Println(sum)
}

闭包

函数是编译期静态的概念,闭包是运行期动态的概念。

闭包 = 函数 + 引用环境

闭包示例1
package main

import "fmt"

func adder() func(int) int {
	var x int
	return func(y int) int {
		x += y
		return x
	}
}
func main() {
    // f是一个函数,应用了其外部作用域的x变量,此时f就是一个闭包,在f的声明周期内,x变量一直有效
	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
}

闭包示例2
package main

import "fmt"

func main() {
	// 定义一个字符串变量
	str1 := "hello world"
	fmt.Printf("修改之前---%s\n", str1)

	// 定义一个匿名函数
	func() {
		str1 = "hello go"
	}()
	fmt.Printf("修改后---%s\n", str1)

}

匿名函数中并没有定义str1,也不是通过参数传递的方式。就算通过传递参数话的方式。由于字符串不可变。不会对原有字符串发生改变。但是却对str1进行了修改。

str1 的定义在匿名函数之前,此时,str1就被引用到匿名函数中形成了闭包。

闭包示例3
package main

import "fmt"

// 定义一个函数,返回值为一个匿名函数,匿名函数与原函数形成闭包,把name 、hp 返回
func Gen(name string) func() (string, int) {
	hp := 150
	return func() (string, int) {
		return name, hp
	}
}

func main() {
	// 调用函数
	gen := Gen("天使彦")
	// 通过匿名函数和闭包,返回天使彦的名字和血量
	name, hp := gen()
	fmt.Println(name, hp)
}

可以看出闭包还具有一定的封装性,血量是无法从外部修改的,与面向对象的封装类似,限制外部内内部的访问权限。

闭包很灵活,记住一句话,闭包 = 函数 + 引用环境

defer语句

这是go语言独有的特性,延时执行语句。

延时语句会在所在函数结束时进行。函数结束可以是正常返回时,也可以是发生宕机时。

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

类似于栈,先进后出。先defer的语句最后执行,后defer的语句最先执行。

package main

import "fmt"

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

执行结果:

----start----
----end----
3
2
1

宕机(panic)和宕机恢复(recover)

  • panic:终止程序运行
  • recover:防止程序崩溃

go语言目前还没有异常机制,但使用panic/recover模式来处理错误,panic可以在任何地方引发,但recover只在defer调用的函数中有效。

可以手动触发宕机。让程序崩溃。开发者能及时的发现错误,同时减少可能的损失。

示例1:手动触发宕机

package main

import "fmt"

func testPanic()  {
	fmt.Println("start")
	panic("宕机")
	fmt.Println("end")
}

func main() {
	testPanic()
}

执行结果:

start
panic: 宕机

goroutine 1 [running]:
main.testPanic()
	D:/Study/Go_Study/src/golang/study/day04、函数/test.go:7 +0x9d
main.main()
	D:/Study/Go_Study/src/golang/study/day04、函数/test.go:12 +0x27

Process finished with exit code 2

示例2:宕机后执行defer语句

package main

import "fmt"

func testPanic()  {
	fmt.Println("start")
	// 在宕机时触发延迟执行语句
	defer fmt.Println("宕机要做的第二件事")
	defer fmt.Println("宕机要做的第一件事")
	panic("宕机")
}

func main() {
	testPanic()
}

执行结果:

start
宕机要做的第一件事
宕机要做的第二件事
panic: 宕机

goroutine 1 [running]:
main.testPanic()
	D:/Study/Go_Study/src/golang/study/day04、函数/test.go:10 +0x151
main.main()
	D:/Study/Go_Study/src/golang/study/day04、函数/test.go:14 +0x27

Process finished with exit code 2

示例3:宕机恢复

无论是代码运行错误,抛出的宕机错误,还是主动触发的宕机错误,都可以配合defer和recover实现错误捕捉和恢复,让代码崩溃后继续运行。

package main

import "fmt"

func testRecover() {

	// 延时语句捕捉宕机
	defer func() {
		fmt.Println("开始捕捉")
		err := recover()
		fmt.Println("捕捉成功,错误为:", err)
	}()

	panic("发生宕机了,之后执行defer语句,defer语句执行匿名函数,函数内部recover()捕捉错误,继续执行")

}

func main() {
	testRecover()
}

执行结果:

开始捕捉
捕捉成功,错误为: 发生宕机了,之后执行defer语句,defer语句执行匿名函数,函数内部recover()捕捉错误,继续执行

注意:

  • recover()必须搭配defer使用。
  • defer一定要在可能引发panic的语句之前定义。
posted @ 2019-07-17 14:08  爬呀爬Xjm  阅读(251)  评论(0编辑  收藏  举报