- 函数是组织良好且可重复使用的,用来实现单一或相关功能的代码块,用于提高模块化和复用性。
- 编写函数的目的是为了将需要多行代码实现的复杂问题分解为一系列简单的任务来解决。
Go语言是编译型的,因此函数的顺序和位置是无关紧要的,鉴于可读性推荐将main
主函数编写在文件前面,其它函数按照一定逻辑顺序向下编写。
Go语言支持普通函数、匿名函数、闭包,从设计上对函数进行了优化和改进。
Go语言中函数是一等公民(first-class),函数本身可作为值来传递,函数支持匿名函数和闭包,函数可以满足接口。
声明函数
函数构成了代码执行的逻辑结构,是程序的基本模块。
Go语言中函数由关键字func
、函数名、参数列表、返回值、函数体、返回语句构成。
func fnname(argslist)(retlist){
//fnbody
}
- 形参列表
argslist
描述了函数的参数名以及参数类型,参数属于局部变量,参数值由调用者提供。 - 返回值列表
retlist
描述了函数返回值的名称以及类型,若无返回值或返回一个无名的变量,返回值列表的括号可省略。 - 若函数声明中不包括返回值列表则函数体执行完毕后不会有返回值
例如:求直角三角形斜边,勾股定理,勾三股四弦五。
package main
import (
"fmt"
"math"
)
func hypot(x, y float64) float64{
return math.Sqrt(x*x + y*y)
}
func main() {
fmt.Println(hypot(3, 4))//5
}
若形参或返回值具有相同类型,不必针对每个形参都添加参数类型。
func fn(i, j, k int, s, t string){}
形参可使用_
空白标识符用来强调参数未被使用
package main
import "fmt"
func add(x, y int) int {return x + y}
func sub(x, y int) (z int) {return x - y}
func first(x, _ int) int {return x}
func zero(int, int) int {return 0}
func main() {
fmt.Printf("x + y = %d\n", add(1, 2))//x + y = 3
fmt.Printf("x - y = %d\n", sub(1, 2))//x - y = -1
fmt.Printf("first = %d\n", first(1, 2))//first = 3
fmt.Printf("zero = %d\n", zero(1, 2))//zero = 0
}
当函数执行到代码块最后一行之前或是return
语句时退出
函数类型
Go语言中函数是一种类型,因此可以和其他类型一样保存在变量中。
package main
import "fmt"
//定义函数
func fire(){
fmt.Printf("fire")
}
func main() {
//将变量声明为函数类型func(),变量fn即回调函数,此时fn的值为nil。
var fn func()
fmt.Printf("fn = %v, type = %T\n", fn, fn)//fn = <nil>, type = func()
//将fire()函数作为值赋给函数变量fn,此时fn的只也就是fire()函数。
fn = fire
fmt.Printf("fn = %v, type = %T\n", fn, fn)//fn = 0x1076fe0, type = func()
//使用函数变量fn执行函数调用,实际调用的是fire()函数
fn()//fire
}
函数类型又称为函数的标识符
package main
import "fmt"
func fn(x, y int) int {return x + y}
func main() {
fmt.Printf("%T\n", fn)//func(int, int) int
}
若两个函数形参列表和返回值列表中的变量类型一一对应,则这两个函数被认为具有相同的类型和标识符。
package main
import "fmt"
func add(x, y int) int {return x + y}
func sub(x, y int) int {return x - y}
func main() {
fmt.Printf("%T\n", add)//func(int, int) int
fmt.Printf("%T\n", sub)//func(int, int) int
}
形参和返回值的变量名并不会影响函数标识符,也不会影响参数类型的省略。
函数在每次调用时必须按照声明时的顺序为所有参数提供实参(参数值)
Go语言中形参没有默认的参数值,也没有任何方法通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言是没有意义的。
函数中实参通过值传递的方式进行赋值,因此函数形参是实参的拷贝,因此对形参进行修改不会影响到实参。不过若实参包含引用类型,比如指针、切片、映射、函数、通道等,实参则可能会由于函数的间接引用而被修改。
多返回值
Go语言中函数支持多返回值,多返回值能够方便地获得函数执行后的多个参会参数。
Go语言会使用多返回值的最后一个返回参数,返回函数执行中可能发生的错误。
与其它语言的返回值相比
- C/C++ 语言中只支持一个返回值,当需要多值返回时可使用结构体。也可在参数中使用指针变量,然后在函数内部修改外部传入的变量值,实际返回计算结果。
- C++ 语言为了安全性,建议在参数返回数据时使用引用替代指针。
- C# 语言中没有多返回值,C#语言后期加入的
ref
和out
关键字能够通过函数的调用参数以获得函数体中修改的数据。 - Lua语言中虽然没有指针但支持多返回值,特别适用于大块数据。
Go语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更加方便。
- 同一类型返回值
若返回值是同一类型,则可使用括号()
将多个返回值类型括起来,用逗号,
分隔每个返回值的类型。在函数体内return
语句返回时,值列表的顺序需要和函数声明的返回值类型保持一致。
package main
import "fmt"
func swap(first, last string) (string, string){
return last, first
}
func main() {
surname, name := swap("Mahesh", "Kumar")
fmt.Printf("%s %s\n", surname, name)// Kumar Mahesh
}
纯类型的返回值对于代码的可读性并不是很友好,特别是在同类型的返回值出现时,会无法区分每个返回值参数的含义。
- 带变量名的返回值
Go语言支持对返回值进行命名,这样返回值和参数一样拥有参数变量名称和类型。
待命名的返回值变量的默认值是其类型的默认值
例如:根据矩形的长和宽计算周长和面积
package main
import "fmt"
//对函数返回值进行命名
func rect(length, width float64) (area, perimeter float64){
//已命名返回值的变量与函数局部变量一样,可以对返回值变量进行赋值和值获取。
area = length * width
perimeter = (length + width) * 2
//当函数使用命名返回值时在return中可以不填写返回值列表,若填写也可以。
return
}
func main() {
area, perimeter := rect(10, 20)
fmt.Printf("area = %f, perimeter = %f\n", area, perimeter)//area = 200.000000, perimeter = 60.000000
}
同一类型返回值和命名返回值两种类型,只能二选一不能混用,混用时将会发生编译错误。
// syntax error: mixed named and unnamed function parameters
func rect(length, width float64) (area, perimeter float64, float64){
area = length * width
perimeter = (length + width) * 2
return
}
语法错误:在函数参数中混合使用了命名和非命名参数
函数调用
函数定义后可通过调用的方式让当前代码跳转到被调用的函数中去执行,调用前的函数局部变量会被保存起来不会丢失,被调用的函数运行结束后,会恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。
函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放因此会失效。
Go语言中函数调用的格式
返回值变量列表 = 函数名(参数列表)
- 变量名:需要调用的函数的名称
- 参数列表:参数变量以逗号分隔,尾部无须使用分号。
- 返回值变量列表:多个返回值时需使用逗号分隔
匿名函数
Go语言支持匿名函数,即在需要使用函数时定义的函数,匿名函数不包含函数名,可用于创建内联函数。
Go语言中匿名函数可形成闭包,匿名函数又称为函数字面量。
匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量。
匿名函数往往会以变量的方式进行传递,与C语言的回调函数类似,不同的是Go语言支持随时在代码中定义匿名函数。
匿名函数不需要定义函数名称的一种函数实现方式,由一个不带函数名的函数声明和函数体组成。
定义匿名函数
func(参数列表) (返回值列表) {
函数体
}
匿名函数的定义简单来说就是没有名字的普通函数定义
在定义时调用匿名函数
匿名函数可以在声明后调用,即自执行函数。
package main
import "fmt"
func main() {
func(data int){
fmt.Printf("data = %v\n", data)// data = 1000
}(1000)
}
将匿名函数赋值给变量
package main
import "fmt"
func main() {
fn := func(data int){
fmt.Printf("data = %v\n", data)// data = 1000
}
fn(1000)
}
匿名函数本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。
匿名函数当作回调函数
例如:对切片遍历时访问每个元素
package main
import "fmt"
func loop(slice []int, fn func(int)){
for _,v := range slice{
fn(v)
}
}
func main() {
slice := []int{1, 2, 3, 4}
loop(slice, func(item int){
fmt.Printf("item = %v\n", item)
})
}
例如:对字符串左右两边的空格进行去除
使用匿名函数实现操作封装
例如:将匿名函数作为map
的键值,通过命令行参数动态调用匿名函数。
package main
import (
"flag"
"fmt"
)
//定义命令行参数type,在命令行输入--type可其后的字符串传入params指针变量中。
var params = flag.String("type", "", "type comment")
func main() {
//解析命令行参数,解析完毕后params指针变量将指向命令行传入的值。
flag.Parse()
//定义从字符串映射到函数的map,然后填充。
typeMap := map[string] func(){
//初始化map的键值对,值为匿名函数。
"fire":func(){
fmt.Printf("fire")
},
"run": func(){
fmt.Printf("run")
},
}
//typeMap是一个*string类型的指针变量
//使用*params获取命令行传入的值并在map中查找对应命令行参数指定的字符串函数
if fn,ok := typeMap[*params]; ok{
//若在map定义中存在参数则调用函数
fn()
}else{
fmt.Printf("not found")
}
}
运行测试
$ go run test.go --type fire
fire
使用函数实现接口
函数在数据类型中属于一等公民,其他类型能够实现接口,函数也可以。
例如:使用结构体实现接口
package main
import "fmt"
//定义调用器接口
type Invoker interface {
Call(interface{})//待实现调用方法
}
//定义结构体类型
type Struct struct {
}
//实现Invoker调用程序的Call调用方法,可传入任意类型interface{}的值。
func (this *Struct) Call(param interface{}){
fmt.Println(param)
}
func main() {
//声明接口变量
var invoker Invoker
//实例化结构体类型
s := new(Struct)
//将结构体实例赋值给接口
invoker = s
//使用接口变量调用结构体实例方法
invoker.Call("hello")
}
例如:使用函数实现接口
函数声明不能直接实现接口,需要将函数定义为类型后,使用类型实现结构体。当类型方法被调用时,还需调用函数本体。
package main
import "fmt"
//定义调用者接口
type Invoker interface {
Call(interface{})//待实现接口
}
//定义函数为类型
type Caller func(interface{})
//实现调用者方法
func (fn Caller) Call(param interface{}) {
fn(param)
}
func main() {
//声明接口变量
var invoker Invoker
//将匿名函数转换为Caller类型后再赋值给接口变量
invoker = Caller(func(v interface{}) {
fmt.Println(v)
})
//使用接口变量调用Caller类型的Call方法,内部会调用函数本体。
invoker.Call("hello")
}
闭包
闭包又称为词法闭包(lexical closure)或函数闭包(function closure),是函数式编程语言中用于实现词法范围的名称绑定技术。 闭包并非某种语言特有的机制,只是经常会出现在函数式编程语言中,因为函数式编程语言中函数是一等公民(first-class)。
从操作实现上来将,闭包是将函数及其运行环境(引用环境)打包存储的一条记录。函数的运行环境又称为执行上下文(execution context),包括函数运行时所处的内部环境和所依赖的外部环境。
Go语言中匿名函数可作为闭包,闭包和普通函数的区别在于,普通函数被调用者执行完毕后会丢弃环境,而闭包则依然会保留运行环境。
函数的运行环境只是一种映射,它会将函数的每个自由变量与创建闭包时名称绑定的值或引用相互关联。函数的自由变量特指在本地使用,但却在封闭的范围内定义的变量。
自由变量是相当于闭包或匿名函数而言的外部变量,由于该变量的定义不受自身控制,因此对闭包自身来说是自由的,因为不会受到闭包的约束。
与普通函数不同的是闭包允许函数通过闭包的值副本或引用来访问那些被捕获的变量,即使函数在其作用域之外被调用。也就是说闭包提供了一种可持续访问被捕获变量的能力,进而扩大了变量的作用域。
闭包提供了持续暴露变量的机制,使外界能够访问原本应该私有的变量,实现了全局变量的作用域效果。由此可见,一旦变量被闭包捕获后,外界使用者可以访问被捕获的变量值或引用,相当于访问了私有变量。
综上所述,闭包是函数式编程中实现名称绑定的技术,直观表现为函数嵌套以提升变量作用范围,使原本寿命短暂的局部变量获得了长生不老的能力。只要被捕获到的自由变量一直处于使用中,系统就不会回收其内存空间。
闭包(Closure)又称为Lambda表达式
闭包是由函数及其相关的引用环境共同组合而成的实体,即 “闭包 = 函数 + 运行(引用)环境”。因此有人又称:”对象是附有行为的数据,闭包是附有数据的行为“。
理解闭包首先需要了解函数的执行环境(execution context)又称为执行上下文。
- 函数是指执行的代码块,由于自由变量被包含在代码块中,因此这些自由变量以及它们引用的对象没有被释放。
- 运行环境是是指为自由变量提供绑定的计算环境,又称为作用域。
闭包包含了自由变量,自由变量是指没有绑定到特定对象的变量。自由变量不是在当前代码块内或任何全局上下文中定义的,而是在定义代码块的环境中定义的局部变量。
闭包的体现形式在函数体内返回另一个函数
闭包是指可以包含自由变量的代码块,自由变量特指没有绑定到特定对象的变量。
自由变量并不在代码块内或全局上下文中定义,而会在定义代码块的环境中定义。
当代码块所在的环境被外部调用时,代码块及其所引用的自由变量会构成闭包。
要执行的代码块会为自由变量提供绑定的计算环境(作用域),由于自由变量包含在代码块中,因此自由变量及其引用的对象不会被释放掉。
理解闭包最直观的方法就是将闭包函数当作一个类,一个闭包函数调用相当于实例化类,然后再从类的角度中区分那些是全局变量,那些是局部变量。实际上在Objective-C中闭包就是使用类来实现的。
闭包的价值在于可作为函数对象或匿名函数,对类型系统而言,意味着不仅要表示数据还要表示代码。
支持闭包的大多数语言都将函数作为第一公民(第一级对象),函数可以存储到变量中作为参数传递给其他函数,而且函数还可以被其他函数动态的创建和返回。
闭包一般是以匿名函数的形式出现,能够动态且灵活的创建和传递,由此体现出函数式编程的特点。
闭包的缺点在于函数中的变量会被保存在内存中,因此内存消耗很大,所以闭包不能滥用。
闭包是引用了自由变量的函数,被引用的自由变量和函数会一同存在,即使已经离开了自由变量的环境也不会被释放或删除,在闭包中可以继续使用这些自由变量。
同一个函数与不同引用环境组合后可形成不同的实例
函数类型和结构体一样可以被实例化,由于函数本身不能存储任何信息,只有与引用环境结合后形成的闭包才具有记忆性。
Go语言不能在函数内部声明函数,却可以在函数体内声明函数类型的变量。
与普通变量声明不同的是,Go语言不能在函数内声明另一个函数,Go语言不支持在函数内部显式地嵌套定义函数,但却可以定义匿名函数。因此,Go语言不支持函数嵌套,比如在Go源文件中,函数声明都是出现在最外层的。
Go语言支持匿名函数,匿名函数是没有指定函数名称的函数。匿名函数相当于一个内联语句或表达式,匿名函数的优越性在于可以直接使用函数内部的变量而无需事先声明。
匿名函数是指不需要定义函数名的一种函数的实现方式,匿名函数由一个不带函数名的函数声明和函数体构成。
func(x, y int) int {
return x + y
}
Go语言中函数也是一种数据类型,因此可以声明函数类型的变量,使用函数类型的变量来接收函数。
Go语言中所有的函数都是值类型的,既可以作为参数传递也可以作为返回值传递。
Go语言中可以将匿名函数赋值给变量
fn := func() int {
}
匿名函数可作为闭包
闭包是内层函数引用了外层函数中的变量,也就是所谓的引用自由变量的函数。
闭包的返回值也是一个函数
闭包只是在形式和表现上像函数,但实际并不是函数。因为函数是编译期静态的概念,闭包是运行期动态的概念。函数只是一段可执行的代码,这些代码在函数被定义后就确定了,也就是说编译后就固化了,因此不会在执行时发生变化。每个函数在内存中只会存在一份实例,所以说一个函数只是一个实例,只要得到函数的入口点即可执行函数。而闭包在运行时可以拥有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
所谓的引用环境是指在程序执行过程中的某个点上所有处于活跃状态的约束所组成的集合,这里的约束是指一个变量的名字和其代表的对象之间的联系。
函数式编程是一种编程模型,将计算机运算看作数学中函数的计算,以避免状态以及变量的概念。
函数式编程中函数是一等公民(First-Class Value,第一类对象),无需像命令式语言那样借助函数指针,委托操作函数。因此函数可以作为另一个函数的返回值或参数,也可以作为某个变量的值赋给某个变量。另外,函数可以嵌套定义,即在函数内部可以定义另一个函数。由于有了嵌套函数这种结构,也就产生了闭包问题。
函数式编程中可以将函数作为参数传递,因此又称之为高阶函数。在数学和计算机科学中,高阶函数至少需要满足下列两个条件中的之一:
- 接受一个或多个函数作为输入
- 输出一个函数
为什么闭包需要将引用环境和函数组合起来呢?因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。
闭包可以理解为定义在函数内部的函数,本质上闭包是将函数内部和外部连接的桥梁,是函数与其引用环境的组合。
下面以计算斐波拉契数列为例,来分析下闭包。
非巴拉契数列是一个递增的数列,形如1 1 2 3 5 8 13 21 34 55...,即从第三个数开始后一个数字是前两个数字之和。
使用函数实现斐波拉契数列生成器
使用闭包可实现拥有自身状态的函数
变量作用域
- 全局变量:在
main()
函数执行之前初始化,因此全局可见。 - 局部变量:在函数内或
if
、for
等语句块中有效,使用后外部不可见。 - 全局变量和局部变量同名时局部变量优先生效
变量可见性
- 包内任何变量或函数都能访问
- 包外首字母大写的可被访问,首字母小写的表示私有因此不能被外部调用。
变参函数
变参函数是指使用不同数量的参数调用的函数,变参函数允许用户在可变函数中传递0或多个参数。比如fmt.Print()
可接受任意数量的参数。
变参函数声明中,最后一个参数的类型前带有省略号...
以表明该函数可以调用任意数量此类型的参数。
func fname(args, ...type) type {}
...type
格式的类型只能作为函数的参数类型存在且必须是最后一个参数,它是一个语法糖(syntax sugar),即该语法对语言的功能并没有影响,只是为了更方便开发人员使用,使用语法糖能够增加代码的可读性而从减少程序出错的可能性。
从内部实现来将,类型...type
本质上是一个数组切片,也就是[]type
。
func fn(args ...int){
for k,v := range args {
fmt.Println(k, v)
}
}
fn(1, 2, 3)
func fn(args []int){
for k,v := range args {
fmt.Println(k, v)
}
}
fn([]int{1, 2, 3})
由于...type
可变参数本质上是一个数组切片,因此为变参函数传递切片。
func fn(args ...int){
for k,v := range args {
fmt.Println(k, v)
}
}
fn([]int{1, 2, 3}...)
slice := []int{1,2,3}
fn(slice...)
由于可变参数变量本身是一个包含函数参数的切片,若需要将含有可变参数的变量传递给下一个可变参数函数,可在传递时在可变参数后添加...
,这样即可将切片中的元素进行传递,而无需传递可变参数变量本身。
package main
import (
"bytes"
"fmt"
)
func concat(args ...string) string {
var buf bytes.Buffer//定义字节缓冲用于快速连接字符串
for _,v := range args {//遍历可变参数列表,类型为[]string
buf.WriteString(v)//将遍历出的字符串连续写入字节数组
}
return buf.String()//将连接好的字节数组转化为字符串
}
func print(args ...string){
str := concat(args...)
fmt.Println(str)
}
func main() {
print("hell", "o")
}
可变参数使用...
进行传递与切片间使用append
连接是同一种特性
可变参数不传入任何值的时候,函数内部会默认为nil
。
func fn(args ...int){
fmt.Printf("args = %v, type = %T\n", args, args)
}
fn()//args = [], type = []int
使用interface{}
空接口可指定任意类型的可变参数
func fn(args ...interface{}){
fmt.Printf("args = %v, type = %T\n", args, args)
}
fn()//args = [], type = []interface {}
可变参数列表数量是不固定的,传入的参数是一个切片,如果需要获得每个参数的具体值,可对可变参数变量进行遍历。
//快速拼接字符串
func concat(args ...string) string {
var buf bytes.Buffer//定义字节缓冲用于快速连接字符串
for _,v := range args {//遍历可变参数列表,类型为[]string
buf.WriteString(v)//将遍历出的字符串连续写入字节数组
}
return buf.String()//将连接好的字节数组转化为字符串
}
fmt.Printf("%s", concat("ham", "mer"))//hammer
延迟执行
Go语言中defer
语句会将其后面跟随的语句进行延迟处理
在defer
归属的函数即将返回时,将延迟处理的语句按defer
的逆序进行执行。先被defer
的语句最后被执行,最后被defer
的语句最先执行。
当多个defer
行为被注册时它们会议逆序执行(类似栈,先进后出,LIFO)
func main() {
fmt.Println("defer begin")
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("defer end")
}
defer begin
defer end
3
2
1
defer
关键字类似Java或C#中的finally
语句块,用于释放已被分配的资源,典型的例子是互斥解锁或关闭文件。
处理业务或逻辑中涉及到成对操作时,比如文件的打开和关闭,请求的接受与回复,加锁与解锁等,这些操作中,最容易忽视的在每个函数退出时需释放或关闭资源。使用defer
语句正好是在函数退出时执行的语句,可方便地解决资源释放等容易被忽视的问题。
使用defer
延迟并发解锁
当在函数中并发读写map
时为防止竞态问题,使用sync.Mutex
进行加锁。
package main
import "sync"
var (
dict = make(map[string]string)//map默认并非并发安全
dictGuard sync.Mutex //为保证使用映射时的并发安全,使用互斥锁。
)
//根据键读取值
func read(key string) string{
dictGuard.Lock()//对共享资源加锁,使用互斥量加锁。
defer dictGuard.Unlock()//对共享资源解锁,使用互斥量解锁。延迟到函数结束时调用
return dict[key]//获取共享资源
}
使用defer
延迟释放文件句柄
//获取文件大小
func filesize(filename string) int64 {
//打开文件返回文件句柄
fh, err := os.Open(filename)
if err != nil {
return 0
}
//延迟关闭文件
defer fh.Close()
//获取文件状态信息
fileinfo, err := fh.Stat()
if err != nil {
return 0
}
//获取文件大小
filesize := fileinfo.Size()
return filesize
}
递归函数
递归函数是指函数内部调用函数自身的函数,构成递归需要具备以下条件
- 一个文件可以被拆分为多个子问题
- 拆分前的原问题与拆分后的子问题除了数据规模不同,处理问题的思路是一样的。
- 不能无限的调用本身,子问题需要拥有退出递归状态的条件。
递归函数编写过程中,一定要有终止条件,否则函数会无限执行下去直到内存溢出。
例如:使用递归实现斐波那契数列
package main
func fib(n int) (result int) {
if n < 2 {
result = 1
}else{
result = fib(n - 1) + fib(n - 2)
}
return
}
func main() {
for i:=0; i<=10; i++{
println(fib(i))
}
}