一、函数的简介
1. 函数介绍
函数在 Go 中十分重要,是 Go 语言的 “一等公民”。我们常常将重复的功能代码抽取出来并封装成一个函数,可以实现对该功能的重复调用,从而减少代码文件的冗余,提高代码的可维护性。
2. 函数的使用
2.1 定义函数
Go 语言中使用 func
关键字来定义一个函数,具体格式如下:
func 函数名(参数名 类型)(返回值 类型){
函数体
return 返回值
}
一个完整的函数主要由以下几个部分组成:
- 函数名:由字母、数字、下划线组成。函数名的第一个字母不能是数字,且在同一个包内,函数名不能重名
- 参数:由参数变量和参数变量的类型组成,多个参数之间使用英文逗号分隔
- 返回值:由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用英文小括号包裹,并用英文逗号分隔
- 函数体:实现指定功能的代码块
函数定义实例:定义一个实现两数相加功能的函数。
package main
import "fmt"
// 两个数字相加
func addNum(x int, y int) int {
ret := x + y
return ret
}
2.2 调用函数
可以使用 函数名+英文括号 来实现对函数的调用。
package main
import "fmt"
// 两个数字相加
func addNum(x int, y int) int {
ret := x + y
return ret
}
func main() {
// 调用函数上述定义的函数
addRet := addNum(10, 5)
fmt.Println("相加的和为:", addRet)
}
2.3 变量作用域
变量作用域指的是变量可以生效的范围。主要分为以下两种:
2.3.1 全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
package main
import "fmt"
// 声明一个全局的变量
var age int
func main() {
age = 18 // 函数内部使用全局的变量
fmt.Println(age)
}
2.3.2 局部变量
局部变量又可以分为以下两类:
- 函数内定义的变量,该变量无法在全局或其他函数内部使用
package main
import "fmt"
func testLocalVar(){
var name string
name = "cdc"
fmt.Println(name)
}
// 在全局中无法使用局部变量
name = "ctt"
func main() {
testLocalVar()
// 在函数内部无法使用其他函数内部的变量
name = "ctt"
}
- 语句块中的局部变量,该变量是条件判断和循环中临时定义的变量,只在判断和循环语句中生效
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
fmt.Println(i) // 脱离for循环,i 变量不生效
if j:=10; j < 100 {
fmt.Println(j)
}
fmt.Println(j) // 脱离if判断,j 变量不生效
switch k:=2; k{
case 1:
fmt.Println("aa")
case 2:
fmt.Println("bb")
default:
fmt.Println("cc")
}
fmt.Println(k) // 脱离switch判断,k 变量不生效
}
2.3.3 查找顺序
当全局变量和局部变量重名时,优先使用局部变量,局部内找不到变量时,再去全局找。
package main
import "fmt"
var address = "南京"
var country = "中国"
func main() {
var address = "上海"
fmt.Println(address) // 上海
fmt.Println(country) // 中国
}
二、函数的构成
1. 函数的参数
函数可以接收0个或多个参数,有时不确定具体的参数个数时,可以使用可变长参数接收,参数需要指定数据类型。定义函数时的参数叫形参,调用函数时传入的参数叫实参。函数的参数是以传值方式传参的,传入到函数内部的参数相当于对原值进行一次拷贝,即在函数内部修改传入的参数值,不会影响原来的值。
1.1 普通传参
package main
import "fmt"
func say(name string) {
fmt.Println("hello, ", name)
}
func main() {
say("cdc")
}
1.2 传入多个参数
package main
import "fmt"
func say2(age int, name string) {
fmt.Printf("hello, my name is %v and I'm %v years old\n", name, age)
}
func main() {
say2(18, "cdc") // hello, my name is cdc and I'm 18 years old
}
对于相同类型的参数,可以进行类型简写
package main
import "fmt"
func say3(name, address string, age int) {
fmt.Printf("hello, my name is %v and I'm %v years old\n", name, age)
fmt.Println(address)
}
func main() {
say3("cdc", "南京", 18)
}
1.3 可变长参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加 ...
来标识,接收的参数会作为元素存储在一个切片中。可变参数要作为函数的最后一个参数且参数的数据类型要一致。
package main
import "fmt"
func say4(age int, args ...string){
fmt.Println(args)
for index, value := range args {
fmt.Println(index, value)
}
}
func main() {
say4(18, "cdc", "南京", "中国")
}
1.4 参数是传值方式传递的
package main
import "fmt"
func do(array [4]int) {
array[0] = 100
fmt.Println("do内部的array:", array) // do内部的array: [100 2 3 4]
}
func main() {
numArray := [4]int{1, 2, 3, 4}
do(numArray)
fmt.Println("main中的array:", numArray) // main中的array: [1 2 3 4]
}
注意:map
、slice
、channel
、interface
等类型本身是属于引用类型,对于这些类型而言,拷贝的实际上是这些类型指向底层数据结构的指针,因此对这些类型变量进行修改,可能会导致原来的值也跟着改变。例如:
package main
import "fmt"
func do2(slice []int) {
slice[0] = 100
fmt.Println("do内部的slice:", slice) // do内部的slice: [100 2 3 4]
}
func main() {
numSlice := []int{1, 2, 3, 4}
do2(numSlice)
fmt.Println("main中的slice:", numSlice) // main中的slice: [100 2 3 4]
}
2. 函数的返回值
2.1 无返回值
函数无返回值时不能使用变量接收,强行接收编译不通过
package main
import "fmt"
func demo1() {
fmt.Println("aaa")
}
func main() {
// demo1() doesn't return a value
ret1 := demo1()
}
2.2 有一个返回值
package main
import "fmt"
func demo2() int {
age := 18
return age
}
func main() {
ret := demo2()
fmt.Println(ret)
}
2.3 有多个返回值
package main
import "fmt"
// 返回多个值
func demo3() (int, string, string) {
age := 18
name := "cdc"
address := "南京"
return age, name, address
}
func main() {
ret1, ret2, ret3 := demo3()
fmt.Println("age: ", ret1)
fmt.Println("name: ", ret2)
fmt.Println("address: ", ret3)
}
2.4 指定返回内容
指定返回内容相当于声明了变量,并将返回值赋值给了变量,因此指定的返回值变量可以使用,不用声明。
package main
import "fmt"
// 指定返回的内容,变量不需要声明
func demo4() (name string, age int) {
age = 18
name = "cdc"
return name, age
}
func main() {
ret1, ret2 := demo4()
fmt.Println("name: ", ret1)
fmt.Println("age: ", ret2)
}
2.5 指定返回内容,但是未使用
package main
import "fmt"
// 指定返回内容,但是未使用
func demo5() (name string, age int) {
age = 18
name = "cdc"
return // 等价于 return name, age
}
func main() {
ret1, ret2 := demo5()
fmt.Println("name: ", ret1)
fmt.Println("age: ", ret2)
}
2.6 覆盖命令返回值
package main
import "fmt"
// 覆盖命令返回值
func demo6() (name string, age int) {
a := 18
b := "cdc"
return b, a
}
func main() {
ret1, ret2 := demo6()
fmt.Println("name: ", ret1)
fmt.Println("age: ", ret2)
}
三、函数的高阶用法
1. 函数类型与函数变量
1.1 定义函数类型
可以使用 type
关键字来声明一个函数类型,例如:
type cal func(int, int) int
上述语句定义了一个 cal
类型,它是一种函数类型,该类型需要接收两个整型数据,并返回一个整型数据。从理论上来说,只要能满足上述条件的函数,都是 cal
类型的函数。
1.2 函数变量
以下示例中,add
函数和 sub
函数都是满足 cal
函数类型的,因此可以将两个函数作为值赋值给 cal
类型的变量。
package main
import "fmt"
type cal func(int, int) int
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func main() {
var func1 cal
var func2 cal
func1 = add // add函数满足cal类型,所以可以将add赋值给cal类型变量func1
ret1 := func1(10, 5)
fmt.Println(ret1)
func2 = sub // sub函数满足cal类型,所以可以将sub赋值给cal类型变量func2
ret2 := func2(10, 5)
fmt.Println(ret2)
}
2. 高阶函数
2.1 函数作为参数
函数可以作为其他函数的参数:
package main
import "fmt"
// 函数可以作为其他函数的参数
func add(x, y int) int {
return x + y
}
// 接收三个参数:两个整型数据,一个函数
func f(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
ret := f(10, 5, add)
fmt.Println(ret)
}
2.2 函数作为返回值
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
// 函数可以作为其他函数的返回值
func cal(op string) func(int, int) int {
switch op {
case "add":
return add
case "sub":
return sub
default:
fmt.Println("操作不合法")
return nil
}
}
func main() {
f := cal("sub")
if f != nil{
ret := f(10, 5)
fmt.Println(ret)
}
}
3. 递归函数
函数内部调用自身的函数称为递归函数。使用递归函数时,一定要有可以让函数调用停止的条件,否则函数会一直调用下去,从而进入死循环。
递归的错误用法:
package main
import "fmt"
func recursionErrorDemo() {
fmt.Println("aaaaa")
recursionErrorDemo()
}
func main() {
recursionErrorDemo()
}
递归示例1:斐波那契数列,计算公式为 f(n) = f(n-1) + f(n-2)
,且 f(2) = f(1) = 1
package main
import "fmt"
// 斐波那契数列
func fibDemo(n int) int {
if n == 1 || n == 2 {
return 1
}
return fibDemo(n-1) + fibDemo(n-2)
}
func main() {
ret := fibDemo(5)
fmt.Println("斐波那契的值:", ret)
}
递归示例2:求阶乘
package main
import "fmt"
// 求阶乘
func factorialDemo(n int) int {
if n == 0 {
return 1
}
return n * factorialDemo(n-1)
}
func main() {
ret := factorialDemo(5)
fmt.Println("阶乘计算结果:", ret)
}
4. 匿名函数
Go 语言中,在函数内部无法再次定义函数,因此想要实现函数的嵌套,必须借助匿名函数。所谓匿名函数,即没有函数名的函数,没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数。匿名函数多用于实现回调函数和闭包。
匿名函数的签名如下:
func(参数)(返回值){
函数体
}
匿名函数的使用示例如下:
package main
func main() {
// 使用变量接收匿名函数
add := func(x, y int) int {
return x + y
}
add(10, 5)
// 匿名函数作为立即执行函数使用
func(x, y int) int {
return x + y
}(10, 20)
}
5. 函数闭包
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
。例如:
package main
import "fmt"
func add(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
f := add(10)
fmt.Println(f(10)) // 20
fmt.Println(f(20)) // 40
fmt.Println(f(30)) // 70
fmt.Println(f(40)) // 110
}
我们对上述的示例进行一个解析,add
函数内部返回了一个匿名函数,匿名函数内部使用到了外部的 add
函数接收到的参数 x
,因此内部的匿名函数满足闭包条件。调用函数时,我们用变量 f
去接收了 add
函数的返回值,即此时的变量 f
就是内部的那个匿名函数,因此在 f
的生命周期内,外层的 add
函数接收的变量 x
会一直生效。
闭包进阶示例1:判断文件名是否带指定后缀,不带则补全后缀。
package main
import (
"fmt"
"strings"
)
func makeSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
txtJudgeFunc := makeSuffix(".txt")
jpgJudgeFunc := makeSuffix(".jpg")
ret1 := txtJudgeFunc("test")
ret2 := jpgJudgeFunc("test")
fmt.Println(ret1) // test.txt
fmt.Println(ret2) // test.jpg
}
闭包进阶示例2:对基础数进行运算
package main
import s"fmt"
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)) // 11
fmt.Println(f1(2)) // 13
fmt.Println(f2(3)) // 10
fmt.Println(f2(4)) // 6
}
6. defer 语句
defer
语句会将其后面跟随的语句进行延迟处理。在Go语言的函数中,return
语句在底层并不是原子操作,它分为给返回值赋值和RET
指令两步。而 defer
语句执行的时机就在返回值赋值操作后,RET
指令执行前。
defer
语句使用示例:
package main
import "fmt"
func main() {
fmt.Println("start connect ...")
defer fmt.Println("close connect")
fmt.Println("do something ....")
}
/*
执行结果:
start connect ...
do something ....
close connect
*/
如果有多个 defer
,按定义的逆序进行执行,也就是说,先被 defer
的语句最后被执行,最后被 defer
的语句,最先被执行。
package main
import "fmt"
func deferFun() {
fmt.Println("statrt ...")
defer fmt.Println("step1")
defer fmt.Println("step2")
defer fmt.Println("step3")
defer fmt.Println("step4")
fmt.Println("end ...")
}
func main() {
deferFun()
}
/*
执行结果:
statrt ...
end ...
step4
step3
step2
step1
*/
7. init 函数
Go 语言中有一个特殊的函数 init
,该函数主要有以下特点:
- 先于
main
函数自动执行,且不能被其他函数调用; - 没有输入参数和返回值;
- 每个包/每个源文件下可以有多个
init
函数; - 同一个包的
init
执行顺序,Go 中没有明确定义,编程时要注意程序不要依赖这个执行顺序; - 不同包的
init
函数按照包导入的依赖关系决定执行顺序。
init
函数使用示例:
package main
import "fmt"
func init(){
fmt.Println("init1...")
}
func init() {
fmt.Println("init2...")
}
func init() {
fmt.Println("init3...")
}
func main() {
fmt.Println("这里是main函数...")
}
/*
执行结果:
init1...
init2...
init3...
这里是main函数...
*/
8. panic/recover
Go 语言中常常使用 panic/recover
模式来处理代码中的异常场景, panic
可以在任何地方引发,但 recover
只有在 defer
调用的函数中有效,因此 recover()
必须搭配 defer
使用,且 defer
一定要在可能引发 panic
的语句之前定义。
package main
import "fmt"
func A() {
fmt.Println("function A")
}
func B() {
panic("panic in function B")
}
func C() {
fmt.Println("function C")
}
func main() {
A()
B()
C()
}
上述代码会引发 panic
,程序崩溃,异常退出:
function A
panic: panic in function B
goroutine 1 [running]:
main.B(...)
F:/GitProject/Code/GoCode/04_函数/10_panic_and_recover/main.go:10
main.main()
F:/GitProject/Code/GoCode/04_函数/10_panic_and_recover/main.go:19 +0xa5
可以通过 recover
将程序恢复回来,继续往后执行:
package main
import "fmt"
func A() {
fmt.Println("function A")
}
func B() {
defer func(){
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in function B")
}
}()
panic("panic in function B")
}
func C() {
fmt.Println("function C")
}
func main() {
A()
B()
C()
}
结果输出:
function A
recover in function B
function C