函数

Go语言中支持函数、匿名函数和闭包。

函数在Go语言中可以作为变量、参数、返回值等等。

定义

1
2
3
func 函数名([参数列表]) [返回值列表] {
函数体
}

返回值

一个函数可以有0个或者多个返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//简单的函数
func sum(a, b int) int{
return a+b
}

//在返回值列表定义返回变量
func sum(a, b int) (c int){
c = a+b
return //这样做,在return处就只需要一个return即可
}

//多返回值,求商与余数
func divi(a, b int) (int, int){
return a/b, a%b
}

在调用有返回值的函数时,我们可以选择不接受它的返回值。

当我们一个函数返回值为silce时,nil可以看做是一个silce,直接返回nil即可,不需要返回一个[]int{}

1
2
3
func nilFunc() []int{
return nil //不需要返回[]int{}
}

参数

可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数类型前加...来标识。

注意:可变参数一般放在函数参数最后面。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
ret := sum(10, 20, 30)
fmt.Println(ret) //output:60
}

func sum(x ...int) (sum int) {
fmt.Println(x) //output:[10 20 30]
for _, v := range x {
sum = sum + v
}
return
}

从输出的结果可以看出,可变参数x在函数体内被是视作一个int类型的切片;

但是在函数体外,可变参数不能被视作一个切片

1
2
3
4
func f1(...int){}
func f2([]int){}
fmt.Println(f1) //output:func(...int)
fmt.Println(f2) //output:func([]int)

可以看出,两种参数输出的结果是不一样的,因此我们不能直接将切片作为参数传入函数。

如果我们需要将切片传入一个可变参数的函数中,我们则需要将切片拆解为参数

1
2
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) //output:10

函数进阶

变量作用域

全局变量

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

1
2
3
4
5
6
7
8
9
package main

import "fmt"

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

func testGlobalVar() {
fmt.Println(num) //函数中可以访问全局变量num
}

局部变量

局部变量就是仅在自己的代码块中可用的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func localVar() {
var x int = 100 //定义一个函数局部变量x,仅在该函数内生效
fmt.Println(x)
if x > 0 {
y := 10
fmt.Println(y)
}
//fmt.Println(y) 此时无法使用变量y
}

func main() {
localVar()
//fmt.Println(x) 此时无法使用变量x
}

函数类型与变量

定义函数类型

使用type关键字可以定义一个函数类型

1
type calcFuncType func(int, int) int

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

简而言之,凡是满足接收两个int类型的参数并且返回一个int类型的返回值都是calcFuncType类型

1
2
3
4
5
6
7
func add(x, y int) int {
return x+y
}

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

上面add和sub都是calcFuncType类型

函数类型变量

我们可以声明函数类型的变量并且为该变量赋值

1
2
3
4
5
6
7
8
9
10
func main() {
var func1 calcFuncType
func1 = add
fmt.Printf("Type of func1:%T\n", func1) //output:Type of func1:main.calcFuncType
fmt.Println(func1(1, 2))//可以像普通函数一样去调用

func2 := sub
fmt.Printf("Type of func2:%T\n", func2) //output:Type of func2:func(int, int) int
fmt.Println(func2(2, 1))
}

高阶函数

高阶函数分为

  • 函数作参数
  • 函数作返回值

函数作参数

1
2
3
4
5
6
7
8
9
func calc(x, y int/*第一第二个参数*/, op func(int, int) int/*第三个参数*/) int/*返回值*/ {
return op(x, y)
}

func main() {
ret1 := calc(10, 20, add)
ret2 := calc(20, 10, sub)
fmt.Println(ret1, ret2)
}

该函数第一第二个参数为操作数,第三个参数为操作。

操作的函数沿用了上面例子的两个函数。

函数作返回值

1
2
3
4
5
6
7
8
9
10
11
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
}
}

匿名函数

函数内部是不能再定义一个函数的,但是匿名函数是个例外

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

匿名函数由于没有函数名,因此不能像普通函数一样调用,它需要保存到某个变量,或者立即执行

1
2
3
4
5
6
7
8
9
10
11
//保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20)//跟普通函数一样执行

//立即执行并返回值
ret := func(x, y int) int {
return x + y
}(10, 20)//直接在后面加上实参
fmt.Println(ret)

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

闭包

闭包简而言之就是函数+引用环境

闭包的引用环境包括了:闭包函数内部的非全局变量、常量、函数等。

下面是闭包的一个简单使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
//把匿名函数作为返回值
func func1() func() {
str := "world"
return func() {
fmt.Println("hello"+str)
//函数体会现在内层寻找变量,找不到则会向外层找
}
}

func main() {
f1 := func1() //f1相当于是一个闭包,包含了匿名函数本身和外层的str
f1()
}

这个例子非常简单,但是没能体现出闭包函数的作用。

接下来我们展示一个添加后缀名的简单函数,体会一下闭包在其中发挥的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func addSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) { //检测是否有后缀
return name + suffix //suffix为外层函数定义的
}
return name
}
}

func main() {
addJPG := addSuffixFunc(".jpg")
fmt.Println(addJPG("Hello")) //output:Hello.jpg
addMP3 := addSuffixFunc(".mp3")
fmt.Println(addMP3("World")) //output:World.mp3
}

再看一个更复杂一点的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func AddSub(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() {
a, s := AddSub(100)
ret1 := a(100) //base = 100(base)+100(i)
fmt.Println(ret1) //output:200
ret2 := s(150) //base = 200(base)-150(i)
fmt.Println(ret2) //output:50
}

延迟处理defer

defer关键字可以让后面跟随的语句进行延迟处理。多个defer遵循栈的顺序,即先进后出的顺序。

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

执行结果如下

1
2
3
4
5
start
end
3
2
1

Go语言中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两部。而defer语句的执行实际便是在赋值完成后,RET之前,即先完成计算然后压入栈后等待返回。

由于是将返回值提前压入栈,后续对给返回值赋值的变量作出任何修改都不会对返回值发生任何影响。

1
2
3
4
5
x := 10
defer func(a int) {
fmt.Println(a)
}(x)
x++

defer延迟调用的特性往往会用在处理资源释放问题上:资源清理、文件关闭、解锁及记录时间等。

推荐文章:https://blog.csdn.net/m0_46251547/article/details/123762669

defer有关面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}

func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}

执行结果是什么?

答案:

1
2
3
4
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4

由于defer注册要延迟执行的函数时该函数所有的参数都需要确定其值,因此执行步骤如下

  1. 执行calc("A", x, y)
  2. calc("AA", x, calc("A", x, y))压入栈
  3. 执行calc("B", x, y)
  4. calc("BB", x, calc("B", x, y))压入栈
  5. calc("BB", x, calc("B", x, y))出栈
  6. calc("AA", x, calc("A", x, y))出栈

内置函数

内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理