Go-函数

函数定义

func funcName(形参列表) (返回值列表) {
    // 函数体
    return 
}

函数名称首字母大写时,该函数对其它包可见;小写时,只有包内可见。

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
  • 函数体:实现指定功能的代码块

函数特点

  • 函数可以没有输入参数,也可以没有返回值(默认返回0)

    func A() {
        // do something
    }
    
    func A() int{
        // do something 
        return 1
    }
    
  • 多个相邻的同类型参数可以简写

    func add(a, b int) int {
        return a + b
    }
    
  • 支持命名返回值变量,参数名就是函数体内最外层的局部变量。

    • 命名返回值变量初始化为类型的零值

    • 最后的return 语句可以省略参数名

      func add(a, b int) (sum int) {
          sum = a + b // 
          return      // 相当于 return sum
      }
      
      func add2(a, b int) (sum int){
          sum := a + b // 新声明一个sum 局部变量
          return sum // 必须显示调用返回变量
      }
      
  • 不支持默认值参数

  • 不支持函数重载

  • 不支持命名函数嵌套定义,支持匿名函数嵌套

    func add(a, b int) (sum int){
        anonymous := func(x,y int) int{
            return x + y
        }
        return anonymous(a, b)
    }
    

函数分类

  • 命名函数
  • 匿名函数

函数类型

一个函数的类型就是函数定义首行去掉函数名,参数名和{。可以使用fmt.Printf("%T")打印的类型

package main
import "fmt"
func add(a,b int) int{
    return a+b
}

func main() {
    fmt.Printf("%T\n", add) // func(int,int) int
}

判断2函数类型相同的条件

  • 相同的形参列表和返回值列表。即列表元素的次序,个数和数量都相同,形参名可以不同

    func add(a,b int) int { 
        return a + b 
    }
    
    func sub(x int, y int) (c int) {
        c = x - y
        return c
    }
    
    // 上面2个函数的函数类型相同 
    

使用 type 关键字定义函数类型

函数类型变量可以作为函数的参数或返回值

package main
import "fmt"

func add(a, b int) int {
    return a + b
}

func sub(a, b int) int {
    return a - b
}

type Op func(int,int) int // 定义一个函数类型,输入的是两个 int 类型,返回值是int

func do(f Op, a, b int) int { // 定义一个函数,第一个参数是函数类型 Op
    return f(a,b)  // 函数类型变量可以直接用来进行函数调用返回
}

func main() {
    a := do(add, 1, 2)
    fmt.Printf("a=%v", a)
    
    s := do(sub, 1, 2)
    fmt.Printf("b=%v", b)
}
  • 函数类型变量是一种引用类型, 未初始化的函数类型的变量默认值为 nil
  • 命名函数的函数名可以使用函数名直接调用,也可以赋值给函数类型变量

匿名函数

使用场景

匿名函数需要保存到某个变量或者作为立即执行函数。

使用示例

  • 匿名函数被直接赋值给函数变量

    • 匿名函数没有函数名,无法通过函数名调用,因此,匿名函数需要保存到某个变量或者作为立即执行函数
    func main() {
    	// 将匿名函数保存到变量
    	add := func(x, y int) {
    		fmt.Println(x + y)
    	}
    	add(10, 20) // 通过变量调用匿名函数
    
    	//自执行函数:匿名函数定义完加()直接执行
    	func(x, y int) {
    		fmt.Println(x + y)
    	}(10, 20)
    }
    
  • 匿名函数用作返回值

    func wrap(op string) func(int, int) int {
        switch op {
        case "add":
            return func(a, b int) int {
                return a + b
            }
        case "sub":
            return func(a, b int) int {
                return a - b
            }
        default:
            return nil
        }
    }
    
  • 匿名函数作为实参

    package main
    import "fmt"
    
    // 匿名函数被直接赋值函数变量
    var sum = func(a, b int) int {
        return a + b
    }
    
    // 匿名函数作为形参
    func doinput(f func(int, int) int, a, b int) int {
        return f(a, b)
    }
    
    // 匿名函数作为返回值
    func wrap(op string) func(int, int) int {
        switch op {
        case "add":
            return func(a, b int) int {
                return a + b
            }
        case "sub":
            return func(a, b int) int {
                return a - b
            }
        default:
            return nil
        }
    }
    
    func main() {
        // 直接调用匿名函数
        defer func() {
            if err := recover(); err != nil {
                fmt.Println(err)
            }
        }()
        
        sum(1,2)
        
        // 匿名函数作为实参
        doinput(func(x,y int) int {
            return x + y  
        }, 1, 2)   // return 1 + 2
        
        opFunc := wrap("add")
        re := opFunc(2,3)
        
        fmt.Printf("%d\n", re) // 5
    }
    

函数参数

传递方式

  • 值类型参数默认就是值传递

  • 引用类型参数默认就是引用传递

不管是值传递还是引用传递,传递给函数的都是变量的副本,值传递的是值的拷贝,引用传递的时地址的拷贝。通常地址拷贝方式效率高(数据量小),而值拷贝由变量的数据量大小决定其效率。

实参到形参的传递

函数实参到形参的传递永远是值拷贝

package main
import "fmt"

func chvalue(a int) int{
    a = a + 1
    return a
}

func chpointer(a *int) {
    *a = *a + 1
    return 
}

func main() {
    a := 10
    chvalue(a)     // 实参传递给形参是值拷贝
    fmt.Println(a) // 10 
    
    chpointer(&a)  // 实参传递给形参仍然是值拷贝,只不过复制的是 a 的地址值
    fmt.Println(a) // 11
}

函数作为参数

package main
import "fmt"

func add(x, y int) int {
	return x + y
}
func calc(x, y int, op func(int, int) int) int {
	return op(x, y)
}
func main() {
	ret2 := calc(10, 20, add)
	fmt.Println(ret2) //30
}

函数作为返回值

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
	}
}

error作为返回值类型时,必须作为最后一个

不定参数

函数支持不定数目的形式参数,不定参数声明使用 param ...type 的语法格式

package main

import "fmt"

func add(args ...int) (sum int) {
	for _, v := range args {
		sum += v
	}
	return sum
}

func main() {
	fmt.Printf("sum=%v", add(10, 1, 2))
}

  • 不定参数类型必须相同

  • 不定参数必须是函数最后一个参数

  • 不定参数名在函数体内是相当于切片

  • 切片作为参数传递给不定参数,切片名后要加上...

    package main
    
    import "fmt"
    
    func add(args ...int) (sum int) {
    	for _, v := range args {
    		sum += v
    	}
    	return sum
    }
    
    func main() {
        s1 := []int{1,2,3,4,5}
        add(s1...)
    }
    
    

变量作用域

全局变量

定义在函数外部的变量,它在整个运行周期内都有效。

package main
import "fmt"

// 全局变量
var num int = 1

func f1() {
    fmt.Printf("num=%d\n", num) // 函数中可以访问全局变量num
}

func main() {
	f1() //num=1
}

局部变量

函数内定义的变量无法在该函数外使用

如果局部变量和全局变量重名,优先访问局部变量。

package main

import "fmt"

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

func testNum() {
	num := 100
	fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量
}
func main() {
	testNum() // num=100
}

语句块内定义的变量

if条件判断、for循环、switch语句上定义的变量

for i := 0; i < 10; i++ {
		fmt.Println(i) //变量i只在当前for语句块中生效
}

闭包

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境

示例

package main
import "fmt"

func addUpper() func(int) int{
    var n int = 10
    return func(x int) int{
        n = n + x
        return n
    }
}

func main() {
    f := addUpper()
    fmt.Println(f(1)) // 11 ,n 初始化为10
    fmt.Println(f(2)) // 13 ,此时n为11
    fmt.Println(f(3)) // 16 ,此时n为13
}
  • addUpper 是一个函数,返回的数据类型 func(int) int

  • 闭包

    image-20210307090350261

    • 返回是一个匿名函数,它引用外部变量n,因此,这个匿名函数和变量n就形成一个整体,构成闭包
  • 反复调用f 函数时,变量 n 初始化一次,后面每次调用进行累积

实践

编程题目要求

  1. 编写一个函数 makeSuffix(suffix string) ,可以接收一个文件后缀名(如 .jpg),并返回一个闭包
  2. 调用闭包,可以传入一个文件名,若该文件名没有指定的后缀(如 .jpg),则返回 "文件名.jpg",如果已经有后缀.jpg,则返回原文件名
  3. 使用闭包方式完成
  4. 提示:strings.HasSuffix 函数用于判断某个字符串是否有指定后缀

代码

package main

import (
	"fmt"
    "strings"
)

func makeSuffix(suffix string) func(string) string {
    
    return func(name string) string {
        // 判断后缀
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

func main() {
    f := makeSuffix(".jpg")
    fmt.Println("文件名称: ", f("biao")) // 
    fmt.Println("文件名称: ", f("write.jpg")) //
}
  • 返回的匿名函数 和 makeSuffix(suffix string) 的变量suffix 构成一个闭包。返回的函数引用外部变量 suffix

  • 该案例中,闭包引用的变量可以反复使用

  • 使用传统函数方法实现,每次都需要传入后缀名

    package main
    
    import (
    	"fmt"
        "strings"
    )
    
    func makeSuffix2(name, suffix string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
    
    func main() {
        fmt.Println("文件名称: ", makeSuffix2("biao", ".jpg")) // 
        fmt.Println("文件名称: ", makeSuffix2("write.jpg", ".jpg")) //
    }
    

defer (延迟调用)

defer 关键字用于注册延迟调用,它们以FILO(先进后出)的顺序在函数返回前被执行

defer 常用于确保资源(如:文件句柄、数据库连接、锁 ...)最终能被释放和回收。

  • defer 后面必须是函数或方法的调用,不能是语句

  • defer 函数的实参在注册时通过值拷贝传递进去

    package main
    import "fmt"
    
    func f() int {
        a := 0
        // go 执行到 defer 时,不会立即执行defer后的语句,而是将defer后面的语句压入到一个独立的栈,然后执行函数的下一个语句
        defer func(i int) {
            fmt.Println("defer i= ", i)  // defer 将语句入栈的同时,也会将相关的值拷贝入栈
        }(a)
        
        a++
        // 当函数执行完毕时,再从栈顶中依次语句执行
        return a
    }
    
    func main() {
        f() // defer i=  0
    }
    
  • 若defer 位于 return 之后,则不会被执行

  • 主动调用 os.Exit(int) 退出进程时,defer 无论如何都不会被执行

  • defer 注册要延迟执行的函数时,该函数的所有参数都需要确定其值

    package main
    import "fmt"
    
    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. calc("A", x, y)   // A 1 2 3
    2. calc("B", x, y)   // B 10 2 12
    3. calc("BB", x, 12) // BB 10 12 22
    4. calc("AA", x, 3)  // AA 1 3 4
    */
    

    执行结果:

    image-20210305170300242

defer执行时机

函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。

defer执行时机

注意事项

  • defer 语句的位置不当,有可能导致 panic ,一般 defer 语句放在错误检查语句之后。
  • defer 也有明显的副作用:defer 会推迟资源的释放,defer 尽量不要放到循环语句里面,将大函数内部的 defer 语句单独拆分成一个小函数是一种很好的实践方式。
  • defer 相对于普通的函数调用需要间接的数据结构的支持,相对于普通函数调用有一定的性能损耗 。
  • defer 中最好不要对有名返回值参数进行操作 ,否则会引发匪夷所思的结果
posted @ 2021-03-07 09:48  KuBee  阅读(63)  评论(0编辑  收藏  举报