在编程中经常会调用相同或者类似的操作,这些相同或者类似的操作由同一段代码完成,函数的出现,可以避免重复编写这些代码。函数的作用就是把相对独立的某个功能抽象出来,使之成为一个独立的实体。
例如,开发一个支持人与人之间进行对话的社交网站,对话这个功能比较复杂,可以将它封装为一个函数,每次调用该函数就可以发起对话;大型网站都有日志功能,对所有重要操作都会记录日志,而日志处理需要由多行Go文件操作相关代码组成,将这些代码组装为函数,则每次写日志时调用此函数即可。
Go语言函数支持的特性包括:
- 参数数量不固定(可变参数)。
- 匿名函数及其闭包。
- 函数本身作为值传递。
- 函数的延迟执行。
- 把函数作为接口调用。
声明函数
函数的声明以关键字func为标识,具体格式如下:
func 函数名(参数列表) (返回参数列表){
函数体
}- 函数名:函数名由字母、数字和下划线构成,但是函数名不能以数字开头;在同一个包内,函数名不可重复。 注意:可暂时简单地将一个包理解为一个文件夹。
- 参数列表:参数列表中的每个参数都由参数名称和参数类型两部分组成,参数变量为函数的局部变量。如果函数的参数数量不固定,Go语言函数还支持可变参数。
- 返回参数列表:返回参数列表中的每个参数由返回的参数名称和参数类型组成,也可简写为返回值类型列表。
- 函数体:函数体指函数的主体代码逻辑,若函数有返回参数列表,则函数体中必须有return语句返回值列表。
使用标准格式定义一个名为add的函数,其功能是进行两个整型数字的加法,并返回结果。
func add(x int,y int) (sum int) {
sum = x + y
return sum
}函数参数简写 在参数列表中,如果相邻的变量为同类型,则不必重复写出类型。
func add(x,y int) (sum int) {
sum = x + y
return sum
}函数返回值简写 如果函数的返回值都是同一类型,在返回值列表中可将返回参数省略。
func returnValue() (int, int) {
return 0,1
}
//可能会降低代码的可读性,无法区分每个返回值的实际意义。带有变量名的函数返回值 使用带有变量名的返回值时,返回默认值为类型的默认值,函数结束处直接调用return即可。
func defaultValue() (a int,b string,c bool) {
return
}
//调用后会返回:0、空字符串和false。如果return后跟返回值列表也是允许的:
func defaultValue() (a int,b string,c bool) {
return 1,"a",true
}使用函数
定义函数后,可通过对函数的调用使用函数,函数体内的代码逻辑执行完毕后,程序将继续执行被调用函数后的代码。
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完全无关,函数体内定义的变量作用域仅限于函数体内。
- 若不想接收函数的某个返回值,可用匿名变量“_”,但是不能所有返回值都用匿名变量代替。
函数变量
函数也是一种类型,我们可以将其保存在变量中。 函数变量的声明格式如下:
var 变量名称 func()函数变量的声明和初始化:
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函数的调用。
函数变量也可用短变量格式进行声明和初始化:
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语言支持可变参数的特性,即函数声明时可以没有固定数量的参数。 可变参数的函数格式如下:
func 函数名 (固定参数列表,v ...T ) (返回参数列表) {
函数体
}- 可变参数一般放在函数参数列表的末尾,也可不存在固定参数列表。
- “v ...T”代表的其实就是变量v为T类型的切片,v和T之间为三个“.”。
可变参数函数的使用
add函数只能对两个int类型的数进行加法计算,现在我们可以使用可变参数实现任意个int类型数的加法运算,最后返回加法结果。
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函数源码如下:
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}Printf函数源码如下,第一个参数指定了需要打印的格式:
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}可变参数的传递
可变参数本质上是一个切片,如果要在多个函数中传递可变参数,可在传递时添加“...”。
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函数的可变参数改为切片:
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匿名函数和闭包
匿名函数即在需要函数时定义函数,匿名函数能以变量方式传递,它常常被用于实现闭包。 匿名函数的格式如下:
func (参数列表) (返回参数列表) {
函数体
}匿名函数的调用有两种方式:
- 定义并同时调用匿名函数。
- 将匿名函数赋值给变量。
定义并同时调用匿名函数
可以在匿名函数后添加“()”直接传入实参:
package main
import "fmt"
func main() {
func (data string) {
fmt.Println("Hello "+data)
}("World!")
}
// Hello World!将匿名函数赋值给变量
将匿名函数赋值给一个变量,之后再进行调用:
package main
import "fmt"
func main() {
f1 := func(data string){
fmt.Println("Hello "+data)
}
f1("World!")
}
// Hello World!闭包
闭包就是包含了自由变量的匿名函数,其中的自由变量即使已经脱离了原有的自由变量环境也不会被删除,在闭包的作用域内可继续使用这个自由变量,同一个匿名函数和不同的引用环境组成了不同的闭包。 闭包就如同有“记忆力”一般,可对作用域内的变量的引用进行修改。
闭包的“记忆力”
# 例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引用的修改。
# 例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闭包的地址为:0xc000006038addOne函数返回了一个闭包函数,通过定义a1和a2变量,创建了两个闭包的实例(引用环境不同导致)。
每次调用闭包实例,i的值都会在原有的基础上加1。从打印的结果可以看到,两个闭包实例的地址完全不同,两个闭包的调用结果互不影响。
延迟执行语句
Go语言中存在一种延迟执行的语句,由defer关键字标识,格式如下:
defer 任意语句defer后的语句不会被马上执行,在defer所属的函数即将返回时,函数体中的所有defer语句将会按出现的顺序被逆序执行,即函数体中的最后一个defer语句最先被执行。
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常常被用来释放资源。
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语言主要分为以下两种情况:
- 对于int、string和bool等值类型变量,传递的是原变量的副本,对副本的操作不会影响原变量。
- 对于指针、切片、map和channel(通道)引用类型变量,传递的是原变量指针的一份副本,该副本指向了原变量地址,因此对该副本的操作会影响原变量,从而达到了其他编程语言中类似于引用传递的效果。


