介绍
- Go语言里面拥三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者 闭包(lambda) 函数
- 方法:接收器中的函数
定义
Go语言是编译型语言,所以函数定义的顺序是无关紧要的。
func 函数名(形式参数列表)(返回值列表){函数体}
func f (int a) int {return a}
返回值
Go语言的函数支持多返回值。我们在开发时候会习惯返回运算结果与一个error对象。
同一类型返回值
- 如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。
- 使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致。
纯类型的返回值对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义。
func typedTwoValues() (int, int) {return 1, 2}func main() {a, b := typedTwoValues()fmt.Println(a, b) // 1,2}
带有变量名的返回值
Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。
- 命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。
```go func f2(a string, b string) (res string, err error) { return b,errors.New(“is err”) }
// 1.可以在函数体中直接对函数返回值进行赋值 // 2.在函数结束前需要显式地使用 return 语句进行返回 func f2(a string, b string) (res string, err error) { err = errors.New(“is err”) return }
---<a name="Zr4Pz"></a>### 函数调用1. 函数调用时,Go语言没有默认参数值。1. 不需要的返回值可使用匿名变量 `_` 来占位:1. 任何类型都可以赋值给它,但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。1. 匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。1. 在 Lua 等编程语言里,匿名变量也被叫做哑元变量。```go返回值变量列表 = 函数名(参数列表)// demoresult,_ := add(1,1)
参数传递
实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。
package mainimport "fmt"// 用于测试值传递效果的结构体type Data struct {complax []int // 测试切片在参数传递中的效果instance InnerData // 实例分配的innerDataptr *InnerData // 将ptr声明为InnerData的指针类型}// 代表各种结构体字段type InnerData struct {a int}// 值传递测试函数// 函数的参数和返回值都是 Data 类型。// 在调用中,Data 的内存会被复制后传入函数,// 当函数返回时,又会将返回值复制一次,func passByValue(inFunc Data) Data {// 输出参数的成员情况fmt.Printf("inFunc value: %+v\n", inFunc)// 打印inFunc的指针fmt.Printf("inFunc ptr: %p\n", &inFunc)return inFunc}func main() {// 准备传入函数的结构in := Data{complax: []int{1, 2, 3},instance: InnerData{5,},ptr: &InnerData{1},}// 输入结构的成员情况fmt.Printf("in value: %+v\n", in)// 输入结构的指针地址fmt.Printf("in ptr: %p\n", &in)// 传入结构体,返回同类型的结构体out := passByValue(in)// 输出结构的成员情况fmt.Printf("out value: %+v\n", out)// 输出结构的指针地址fmt.Printf("out ptr: %p\n", &out)}
运行代码,输出结果为:in value: {complax:[1 2 3] instance:{a:5} ptr:0xc042008100}in ptr: 0xc042066060inFunc value: {complax:[1 2 3] instance:{a:5} ptr:0xc042008100}inFunc ptr: 0xc0420660f0out value: {complax:[1 2 3] instance:{a:5} ptr:0xc042008100}out ptr: 0xc0420660c0
- 所有的 Data 结构的指针地址发生了变化,意味着所有的结构都是一块新的内存,无论是将 Data 结构传入函数内部,还是通过函数返回值传回 Data 都会发生复制行为。
- 所有的 Data 结构中的成员值都没有发生变化,原样传递,意味着所有参数都是值传递。
- Data 结构的 ptr 成员在传递过程中保持一致,表示指针在函数参数值传递中传递的只是指针值,不会复制指针指向的部分。
保存变量
在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中。
func fire() {fmt.Println("fire")}func main() {// demo1var f func()f = firef()// demo2var f = firef()}
可变参数
- 合适地使用可变参数,可以让代码简单易用,尤其是输入输出类函数,比如日志函数
fmt.Println() - 可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型。
- 形如
...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数,它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响。从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type
可变类型
1. 固定类型
func myfunc(args ...int) {for _, arg := range args {fmt.Println(arg)}}myfun(1,2,3)
2. 任意类型
如果你希望传任意类型,可以指定类型为 interface{}
func MyPrintf(args ...interface{}) {// 字节缓冲作为快速字符串连接var b bytes.Buffer// 遍历参数for _, s := range slist {// 将interface{}类型格式化为字符串str := fmt.Sprintf("%v", s)// 类型的字符串描述var typeString string// 对s进行类型断言switch s.(type) {case bool: // 当s为布尔类型时typeString = "bool"case string: // 当s为字符串类型时typeString = "string"case int: // 当s为整型类型时typeString = "int"}// 写字符串前缀b.WriteString("value: ")// 写入值b.WriteString(str)// 写类型前缀b.WriteString(" type: ")// 写类型字符串b.WriteString(typeString)// 写入换行符b.WriteString("\n")}return b.String()}func main() {var v1 int = 1var v2 string = "hello"var v3 bool = falseMyPrintf(v1, v2, v3)}
传递可变参数
- 可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加
...,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。// 实际打印的函数func rawPrint(rawList ...interface{}) {// 遍历可变参数切片for _, a := range rawList {// 打印参数fmt.Println(a)}}// 打印函数封装func print(slist ...interface{}) {// 将slist可变参数切片完整传递给下一个函数rawPrint(slist...)}func main() {print(1, 2, 3)}
匿名函数
介绍
- Go语言支持匿名函数,即在需要使用函数时再定义函数。
- 匿名函数没有函数名只有函数体,可以作为一种类型被赋值给函数类型的变量,匿名函数也可以变量方式传递。
- 匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。
定义
func(参数列表)(返回参数列表){函数体}
调用
1.在定义时调用匿名函数
func(data int) {fmt.Println("hello", data)}(100)
2.将匿名函数赋值给变量
// 将匿名函数体保存到f()中f := func(data int) {fmt.Println("hello", data)}// 使用f()调用f(100)
用作回调函数
func visit(list []int, f func(int)) {for _, v := range list {f(v)}}func main() {// 使用匿名函数打印切片内容visit([]int{1, 2, 3, 4}, func(v int) {fmt.Println(v)})}
closure 闭包
介绍
- Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:
函数 + 引用环境 = 闭包 - 一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。
其它编程语言中的闭包
- 闭包(Closure)在某些编程语言中也被称为 Lambda 表达式。
- 闭包对环境中变量的引用过程也可以被称为“捕获”,在 C++11 标准中,捕获有两种类型,分别是引用和复制,可以改变引用的原值叫做“引用捕获”,捕获的过程值被复制到闭包中使用叫做“复制捕获”。
- 在 Lua 语言中,将被捕获的变量起了一个名字叫做 Upvalue,因为捕获过程总是对闭包上方定义过的自由变量进行引用。
- 闭包在各种语言中的实现也是不尽相同的,在 Lua 语言中,无论闭包还是函数都属于 Prototype 概念,被捕获的变量以 Upvalue 的形式引用到闭包中。
- C++ 与 C# 中为闭包创建了一个类,而被捕获的变量在编译时放到类中的成员中,闭包在访问被捕获的变量时,实际上访问的是闭包隐藏类的成员。
闭包内部修改引用的变量
闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改
// 准备一个字符串str := "hello world"// 创建一个匿名函数foo := func() {// 匿名函数中访问strstr = "hello dude"}// 调用匿名函数foo()fmt.Println(str) // hello dude
记忆效应
被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。
package mainimport ("fmt")// 提供一个值, 每次调用函数会指定对值进行累加func Accumulate(value int) func() int {// 返回一个闭包return func() int {// 累加value++// 返回一个累加值return value}}func main() {// 创建一个累加器, 初始值为1accumulator := Accumulate(1)// 累加1并打印fmt.Println(accumulator())fmt.Println(accumulator())// 打印累加器的函数地址fmt.Printf("%p\n", &accumulator)// 创建一个累加器, 初始值为10accumulator2 := Accumulate(10)// 累加1并打印fmt.Println(accumulator2())// 打印累加器的函数地址fmt.Printf("%p\n", &accumulator2)}
- 对比输出的日志发现 accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例。
- 每调用一次 accumulator 都会自动对引用的变量进行累加。
工程模式
闭包的记忆效应被用于实现类似于设计模式中工厂模式的生成器,下面的例子展示了创建一个玩家生成器的过程。
package mainimport ("fmt")// 创建一个玩家生成器, 输入名称, 输出生成器func playerGen(name string) func() (string, int) {// 血量一直为150hp := 150// 返回创建的闭包return func() (string, int) {// 将变量引用到闭包中return name, hp}}func main() {// 创建一个玩家生成器generator := playerGen("high noon")// 返回玩家的名字和血量name, hp := generator()// 打印值fmt.Println(name, hp) // high noon 150}
闭包还具有一定的封装性,第 8 行的变量是 playerGen 的局部变量,playerGen 的外部无法直接访问及修改这个变量,这种特性也与面向对象中强调的封装性类似。
递归函数
介绍
- 所谓递归函数指的是在函数内部调用函数自身的函数。
- 从数学解题思路来说,递归就是把一个大问题拆分成多个小问题,再各个击破。实际开发过程中,递归函数可以解决许多数学问题,如计算给定数字阶乘、产生斐波系列等。
条件
构成递归需要具备以下条件:
- 一个问题可以被拆分成多个子问题;
- 拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的;
- 不能无限制的调用本身,子问题需要有退出递归状态的终止条件。否则就会无限调用下去,直到内存溢出。
例子
1. 斐波那契数列
以递归函数的经典示例 —— 斐波那契数列为例,数列的形式如下所示:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, …
实现:
func main() {result := 0start := time.Now()for i := 1; i <= 40; i++ {result = fibonacci(i)fmt.Printf("数列第 %d 位: %d\n", i, result)}end := time.Now()delta := end.Sub(start)fmt.Printf("程序的执行时间为: %s\n", delta)}func fibonacci(n int) (res int) {if n <= 2 {res = 1} else {res = fibonacci(n-1) + fibonacci(n-2)}return}
输出:
数列第 1 位: 1数列第 2 位: 1数列第 3 位: 2数列第 4 位: 3...数列第 39 位: 63245986数列第 40 位: 102334155程序的执行时间为: 2.2848865s
2. 数字阶乘
- 一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且 0 的阶乘为 1,自然数 n 的阶乘写作
n!,“基斯顿·卡曼”在 1808 年发明了n!这个运算符号。
例如,n!=1×2×3×…×n,阶乘亦可以递归方式定义:0!=1,n!=(n-1)!×n。
func Factorial(n uint64) (result uint64) {if n > 0 {result = n * Factorial(n-1)return result}return 1}func main() {var i int = 10fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))// 10 的阶乘是 3628800}
内存缓存
- 递归函数的缺点就是比较消耗内存,而且效率比较低,性能比较低。
- 当在进行大量计算的时候,提升性能最直接有效的一种方式是避免重复计算,通过在内存中缓存并重复利用缓存从而避免重复执行相同计算的方式称为内存缓存。
输出如下:const LIM = 41var fibs [LIM]uint64func main() {var result uint64 = 0start := time.Now()for i := 1; i < LIM; i++ {result = fibonacci(i)fmt.Printf("数列第 %d 位: %d\n", i, result)}end := time.Now()delta := end.Sub(start)fmt.Printf("程序的执行时间为: %s\n", delta)}func fibonacci(n int) (res uint64) {// 记忆化:检查数组中是否已知斐波那契(n)if fibs[n] != 0 {res = fibs[n]return}if n <= 2 {res = 1} else {res = fibonacci(n-1) + fibonacci(n-2)}fibs[n] = resreturn}
通过运行结果可以看出,同样获取数列第 40 位的数字,使用内存缓存后所用的时间为 0.0149603 秒,对比之前未使用内存缓存时的执行效率,可见内存缓存的优势还是相当明显的。数列第 1 位: 1数列第 2 位: 1数列第 3 位: 2数列第 4 位: 3...数列第 39 位: 63245986数列第 40 位: 102334155程序的执行时间为: 0.0149603s
defer 延迟执行
介绍
- Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数运行完返回语句时,将延迟处理的语句按后进先出执行。
- 关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。 ```go func main() { fmt.Println(“defer begin”) defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) fmt.Println(“defer end”) }
// log defer begin defer end 3 2 1
<a name="JxO4E"></a>#### 释放资源<a name="z9GVm"></a>##### 释放锁```govar (valueByKey = make(map[string]int)valueByKeyGuard sync.Mutex)func readValue(key string) int {valueByKeyGuard.Lock()defer valueByKeyGuard.Unlock()return valueByKey[key]}
释放文件句柄
func fileSize(filename string) int64 {f, err := os.Open(filename)if err != nil {return 0}// 延迟调用Close, 此时Close不会被调用defer f.Close()info, err := f.Stat()if err != nil {// defer机制触发, 调用Close关闭文件return 0}size := info.Size()// defer机制触发, 调用Close关闭文件return size}
