在编程中经常会调用相同或者类似的操作,这些相同或者类似的操作由同一段代码完成,函数的出现,可以避免重复编写这些代码。函数的作用就是把相对独立的某个功能抽象出来,使之成为一个独立的实体

例如,开发一个支持人与人之间进行对话的社交网站,对话这个功能比较复杂,可以将它封装为一个函数,每次调用该函数就可以发起对话;大型网站都有日志功能,对所有重要操作都会记录日志,而日志处理需要由多行Go文件操作相关代码组成,将这些代码组装为函数,则每次写日志时调用此函数即可。

Go语言函数支持的特性包括:

  • 参数数量不固定(可变参数)。
  • 匿名函数及其闭包。
  • 函数本身作为值传递。
  • 函数的延迟执行。
  • 把函数作为接口调用。

函数的声明以关键字func为标识,具体格式如下:

1
2
3
func 函数名(参数列表) (返回参数列表){
函数体
}
  • 函数名:函数名由字母、数字和下划线构成,但是函数名不能以数字开头;在同一个包内,函数名不可重复。
    注意:可暂时简单地将一个包理解为一个文件夹。
  • 参数列表:参数列表中的每个参数都由参数名称和参数类型两部分组成,参数变量为函数的局部变量。如果函数的参数数量不固定,Go语言函数还支持可变参数。
  • 返回参数列表:返回参数列表中的每个参数由返回的参数名称和参数类型组成,也可简写为返回值类型列表。
  • 函数体:函数体指函数的主体代码逻辑,若函数有返回参数列表,则函数体中必须有return语句返回值列表。

使用标准格式定义一个名为add的函数,其功能是进行两个整型数字的加法,并返回结果。

1
2
3
4
func add(x int,y int) (sum int) {
sum = x + y
return sum
}

函数参数简写
在参数列表中,如果相邻的变量为同类型,则不必重复写出类型。

1
2
3
4
func add(x,y int) (sum int) {
sum = x + y
return sum
}

函数返回值简写
如果函数的返回值都是同一类型,在返回值列表中可将返回参数省略。

1
2
3
4
5
func returnValue() (int, int) {
return 0,1
}

//可能会降低代码的可读性,无法区分每个返回值的实际意义。

带有变量名的函数返回值
使用带有变量名的返回值时,返回默认值为类型的默认值,函数结束处直接调用return即可。

1
2
3
4
5
func defaultValue() (a int,b string,c bool) {
return
}

//调用后会返回:0、空字符串和false。

如果return后跟返回值列表也是允许的:

1
2
3
func defaultValue() (a int,b string,c bool) {
return 1,"a",true
}

定义函数后,可通过对函数的调用使用函数,函数体内的代码逻辑执行完毕后,程序将继续执行被调用函数后的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

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


func main() {
i:=10
q:=7
add,sub := addSub(i,q)
fmt.Println(i,"+",q,"=",add)
fmt.Println(i,"-",q,"=",sub)
}

// 10 + 7 = 17
// 10 - 7 = 3

以上程序中,我们一般将传入函数的变量i和q称为实参,将函数中的x和y称为形参。变量i和q通过值传递的方式将值赋给形参x和y。
注意:

  • addSub函数中的形参x和y作用域仅限于函数体内。
  • main函数中定义的变量add和sub与addSub函数中定义的局部变量sum和sub完全无关,函数体内定义的变量作用域仅限于函数体内。
  • 若不想接收函数的某个返回值,可用匿名变量“_”,但是不能所有返回值都用匿名变量代替。

函数也是一种类型,我们可以将其保存在变量中。
函数变量的声明格式如下:

1
var 变量名称 func()

函数变量的声明和初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

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


func main() {
i:=10
q:=7
var f1 func(x int,y int) (add int,sub int)
f1 = addSub
add,sub := f1(i,q)
fmt.Println(i,"+",q,"=",add)
fmt.Println(i,"-",q,"=",sub)
}

// 10 + 7 = 17
// 10 - 7 = 3

函数变量f1声明后其值初始化为nil,在将addSub函数赋值给f1后,所有对f1的调用即为对addSub函数的调用。

函数变量也可用短变量格式进行声明和初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

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


func main() {
i:=10
q:=7
f1 := addSub
add,sub := f1(i,q)
fmt.Println(i,"+",q,"=",add)
fmt.Println(i,"-",q,"=",sub)
}

// 10 + 7 = 17
// 10 - 7 = 3

Go语言支持可变参数的特性,即函数声明时可以没有固定数量的参数。
可变参数的函数格式如下:

1
2
3
func 函数名 (固定参数列表,v ...T ) (返回参数列表) {
函数体
}
  • 可变参数一般放在函数参数列表的末尾,也可不存在固定参数列表。
  • “v …T”代表的其实就是变量v为T类型的切片,v和T之间为三个“.”。

add函数只能对两个int类型的数进行加法计算,现在我们可以使用可变参数实现任意个int类型数的加法运算,最后返回加法结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func add(slice ... int) (int) {
sum := 0
for _,value:= range slice {
sum = sum + value
}
return sum
}

func main() {
fmt.Println("1+2+3...+9+10=",add(1,2,3,4,5,6,7,8,9,10))
}

// 1+2+3...+9+10= 55

使用可变参数的函数体中,常常会使用for循环来对切片中的项进行操作。

Go语言中许多内置函数的参数都用了可变参数,比如最常用的fmt包中的Println函数和Printf
函数。
fmt包中的Println函数源码如下:

1
2
3
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}

Printf函数源码如下,第一个参数指定了需要打印的格式:

1
2
3
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}

可变参数本质上是一个切片,如果要在多个函数中传递可变参数,可在传递时添加“…”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func addall(slice ... int) (int) {
sum := 0
for _,value:= range slice {
sum = sum + value
}
return sum
}

func add(num ... int)(int){
return addall(num...)
}

func main() {
fmt.Println("1+2+3...+9+10=",add(1,2,3,4,5,6,7,8,9,10))
}

// 1+2+3...+9+10= 55

如果想传递可变参数本身,可将addall函数的可变参数改为切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func addall(slice []int) (int) {
sum := 0
for _,value:= range slice {
sum = sum + value
}
return sum
}

func add(num ... int)(int){
return addall(num)
}

func main() {
fmt.Println("1+2+3...+9+10=",add(1,2,3,4,5,6,7,8,9,10))
}

// 1+2+3...+9+10= 55

匿名函数即在需要函数时定义函数,匿名函数能以变量方式传递,它常常被用于实现闭包。
匿名函数的格式如下:

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

匿名函数的调用有两种方式:

  • 定义并同时调用匿名函数。
  • 将匿名函数赋值给变量。

可以在匿名函数后添加“()”直接传入实参:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
func (data string) {
fmt.Println("Hello "+data)
}("World!")
}

// Hello World!

将匿名函数赋值给一个变量,之后再进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
f1 := func(data string){
fmt.Println("Hello "+data)
}
f1("World!")
}

// Hello World!

闭包就是包含了自由变量的匿名函数,其中的自由变量即使已经脱离了原有的自由变量环境也不会被删除,在闭包的作用域内可继续使用这个自由变量,同一个匿名函数和不同的引用环境组成了不同的闭包。
闭包就如同有“记忆力”一般,可对作用域内的变量的引用进行修改。

闭包的“记忆力”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 例1
package main

import "fmt"

func main() {
num:=1
fmt.Printf("%p\n",&num)
func() {
num++
fmt.Println(num)
fmt.Printf("%p\n",&num)
}()
func() {
num++
fmt.Println(num)
fmt.Printf("%p\n",&num)
}()
}

//0xc00000a0b0
//2
//0xc00000a0b0
//3
//0xc00000a0b0

匿名函数由于在函数体内部引用了外部的自由变量num而形成了闭包。闭包每次对num变量的加1操作都是对变量num引用的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 例2
package main

import "fmt"

func addOne(i int) func() int {
return func() int {
i++
return i
}
}

func main() {
a1 := addOne(0)
fmt.Println(a1()) //0+1=1
fmt.Println(a1()) //1+1=2
a2 := addOne(10)
fmt.Println(a2())
fmt.Print("a1闭包的地址为:")
fmt.Printf("%p\n",&a1)
fmt.Print("a2闭包的地址为:")
fmt.Printf("%p\n",&a2)
}

//1
//2
//11
//a1闭包的地址为:0xc000006028
//a2闭包的地址为:0xc000006038

addOne函数返回了一个闭包函数,通过定义a1和a2变量,创建了两个闭包的实例(引用环境不同导致)。

每次调用闭包实例,i的值都会在原有的基础上加1。从打印的结果可以看到,两个闭包实例的地址完全不同,两个闭包的调用结果互不影响。

Go语言中存在一种延迟执行的语句,由defer关键字标识,格式如下:

1
defer 任意语句

defer后的语句不会被马上执行,在defer所属的函数即将返回时,函数体中的所有defer语句将会按出现的顺序被逆序执行,即函数体中的最后一个defer语句最先被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main(){
fmt.Println("start now")
defer fmt.Println("这是第一句defer语句")
defer fmt.Println("这是第二句defer语句")
defer fmt.Println("这是第三句defer语句")
fmt.Println("end")
}

//start now
//end
//这是第三句defer语句
//这是第二句defer语句
//这是第一句defer语句

由于defer语句是在当前函数即将返回时被调用,所以defer常常被用来释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"net"
)

func tcpSend() {
conn,err := net.Dial("tcp","127.0.0.1:80")
if err == nil {
defer conn.Close()
fmt.Println("remote address:",conn.RemoteAddr())
}
fmt.Println("error:", err)
}

func main() {
tcpSend()
}


//remote address: 127.0.0.1:80
//error: <nil>

由于127.0.0.1对应的远端IP的80端口处于开启状态,在本机与该地址建立连接后和函数退出前,需要对打开的文件描述符conn进行关闭。

小结

  • Go语言中,函数的声明以关键字func为标识。
  • Go语言支持可变参数的特性,即函数声明时可以没有固定数量的参数。
  • 匿名函数即在需要函数时定义函数,匿名函数能以变量方式传递,它常常被用于实现闭包。
  • 闭包就是包含了自由变量的匿名函数,其中的自由变量即使已经脱离了原有的自由变量环境也不会被删除,在闭包的作用域内可继续使用这个自由变量。
  • defer语句是在当前函数即将返回时被调用,所以defer常常被用来释放资源。

两个基本概念: 值传递引用传递

  • 值传递:将变量的一个副本传递给函数,函数中不管如何操作该变量副本,都不会改变原变量的值。
  • 引用传递:将变量的内存地址传递给函数,函数中操作变量时会找到保存在该地址的变量,对其进行操作,会改变原变量的值。

Go语言函数传入参数时使用的始终是值传递,对于值传递,Go语言主要分为以下两种情况:

  1. 对于int、string和bool等值类型变量,传递的是原变量的副本,对副本的操作不会影响原变量。
  2. 对于指针、切片、map和channel(通道)引用类型变量,传递的是原变量指针的一份副本,该副本指向了原变量地址,因此对该副本的操作会影响原变量,从而达到了其他编程语言中类似于引用传递的效果。