Go函数篇

本文参考:https://www.liwenzhou.com/posts/Go/09_function/

函数

函数概述

函数是一段能够重复使用的代码的封装。函数参数定义了外界给函数输入的数据。返回值定义了函数给外界输出的数据。Go语言函数支持不定长参数和多个返回值。

函数定义

Go语言中定义函数使用func关键字,具体格式如下:

func 函数名(参数)(返回值){
	函数体
}

函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。函数名也不能重复命名。

参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分割

返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分割

函数体:实现指定功能的代码块

// 实现求两个数之和的函数
func intSum(x int, y int) int{
	return x + y
}

当然,函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数

func sayHello(){
	fmt.Println("fuck off")
}

函数的调用

定义了函数之后,可以通过函数名()的方式调用函数。注意:调用有返回值的函数时,可以不接收其返回值。

func main(){
	sayHello()
	ret := intSum(1,2)
	fmt.Println(ret)
}

参数

类型简写

函数的参数中如果相邻的变量的类型相同,则可以省略类型。

// 函数接收两个参数,这两个参数的类型均为int,因此可以省略x的类型,y后面有类型说明
func intSum(x,y int)int{
	return x + y
}

可变参数(不定长参数)

可变参数是指函数短的参数数量不固定,Go语言中的可变参数通过在参数名后加...来标识

注意:可变参数通常要座位函数的最后一个参数

func intSum(x ...int)int{
	fmt.Println(x)  // x是一个切片
	sum := 0
	for _,v := range x{
		sum += v
	}
	return sum
}

func main(){
    fmt.Println(intSum())  // 0
    fmt.Println(intSum(10))  // 10
    fmt.Println(intSum(10,20)) // 30
    fmt.Println(intSum(10,20,30)) // 60
}

如果固定参数和可变参数混合使用,可变参数必须是在固定参数的后面

func intSum(x int, y ...int) int {
    fmt.Println(x, y)
    sum := x
    for _,v := range y{
        sum += v
    }
    return sum
}

func main(){
    fmt.Println(intSum(10))  // 10
    fmt.Println(intSum(10,20))  // 30
    fmt.Println(intSum(10,20,30)) // 60
}

本质上,函数的可变参数是通过切片来实现的。

返回值

通过return 关键字向外输出返回值。

多返回值

函数如果有多个返回值时必须用()将所有返回值包裹起来。

func calc(x,y int)(int, int){
	sum := x + y
	sub := x - y
    return sum,sub
}

返回值命名

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。

func calc(x,y int)(sum,sub int){
	sum = x + y
	sum = x - y
	return 
}

返回值补充

当一个函数返回值类型是slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。

func sliceFunc(x string) []int {
	if x == ""{
		return nil   // 没必要返回[]int{}
	}
}

函数进阶

变量作用域

全局变量

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。在函数中可以访问到全局变量。

package main

import "fmt"

// 定义全局变量
var num int64 = 10   // 不能使用 num := 10

func globalVar(){
    fmt.Println(num)  // 函数中可以访问到全局变量num
}

func main(){
    globalVar()   // num=10
}

局部变量

局部变量又分为以下几种:

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

    func localVar(){
    	// 定义一个函数局部变量x,仅在该函数内生效
        x := 100
        fmt.Println(x)
    }
    
    func main(){
        localVar()
        fmt.Println(x)   // 此时无法使用变量x
    }
    
  • 如果局部变量和全局变量重名,优先访问局部变量

    package main
    
    import "fmt"
    
    // 定义全局变量num 
    var num int64 = 100
    
    func testNum(){
        num := 10
        fmt.Println(num)   // 10 函数优先使用局部变量
    }
    
    fumc main(){
        testNum()  // 10
        fmt.Println(num) // 100
    }
    
  • iffor,switch等语句块中定义的变量,也只能在语句块中访问

    func localVar(x, y int){
    	fmt.Println(x,y) // 函数的参数也只能在本函数中生效
        if x > 0{
            z := 100 // 变量z只能在if语句块生效
            fmt.Println(z)
        }
        // fmt.Println(z)   // 此处无法使用变量z
    }
    
  • for循环语句中定义的变量,也是只能在for语句中生效

    func localVar(){
        for i := 0; i < 10; i++ {
            fmt.Println(i)  // 变量i只能在当前for语句块中生效
        }
        // fmt.Println(i)  // 此处无法使用变量i
    }
    

函数类型与变量

定义函数类型

我们可以通过type关键字来定义一个函数类型

type calc func(int, int) int

上面的语句定义了一个calc函数类型,这种函数类型接收两个int类型的参数并且返回一个int类型的返回值。

简单说,凡是满足这个条件的函数都是calc类型的函数。例如下面的sum和sub都是calc类型

func sum(x, y int)int{
	return x + y 
}

func sum(x, y int)int{
    return x - y
}

sum和sub都能赋值给calc类型的变量

var c calc
c = add

函数类型变量

我们可以声明函数类型的变量并未该变量赋值

func main(){
	var c calc  //声明一个calc类型的变量c
    c = sum    // 把sum赋值给c
    fmt.Printf("type of c:%T
",c)  //type of c:main.calc
    fmt.Println(c(1,2))  // 像调用sum一样调用c
    
    d := sub  // 把函数sub赋值给变量d
    fmt.Printf("type if d:%T
", d)  // type of d:func(int,int)
    fmt.Println(d(20,10))  // 像调用sub一样调用d
}

高阶函数

函数作为参数

func sum(x,y int)int{
    return x + y
}

func calc(x,y int, op func(int,int)int)int{
    return op(x,y)
}

func main(){
    ret := calc(10,20,sum)
    fmt.Println(ret) // 30
}

函数作为返回值

func do(s string)(func(int,int)int,error){
    switch s {
        case "+":
        	return sum,nil
        case "-":
        	return sub,nil
        default:
        	err := errors.New("无法识别的操作符")
        	return nil,err
    }
}

匿名函数

匿名函数主要作用是封装一段一次性执行的代码,它无所谓复用,所以无需起名,之所以进行封装的意义在于使一段代码成为 一个整体。

func(参数)(返回值){
	函数体
}

匿名函数因为没有函数名,所以没有办法像普通函数那样调用,所以匿名函数需要保存到某个变量或作为立即执行的函数。

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 adder()func(int)int{
	var x int   // 初始为0
	return func(y int)int{
		x += y
		return x
	}
}

func main(){
    var a = adder()     // x = 0
    fmt.Println(a(10))  // adder()(10)   x=0, y=10  x=10
    fmt.Println(a(20))  // adder()(20)   x=10,y=20  x=30
    fmt.Println(a(30))  // adder()(30)   x=30,y=30  x=60
    
    b := adder()        // x = 0
    fmt.Println(b(40))  // adder()(40)   x=0,y=40  x=40
    fmt.Println(b(50))  // adder()(50)   x=40,y=50 x=90
}

变量ab是一个函数并且它们引用了其外部作用域中x变量。此时ab就是闭包。在ab的生命周期内,变量x一直有效。

闭包进阶示例1:

func adder(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}

func main(){
    var a = adder(10)   // x=10
    fmt.Println(a(10))  // adder(10)(10)  x=10,y=10,x=20
    fmt.Println(a(20))  // adder(20)(20)  x=20,y=20,x=40
    fmt.Println(a(30))  // adder(40)(30)  x=40,y=30,x=70
    
    b := adder(20)  // x=20
    fmt.Println(b(40))  // adder(20)(40)   x=20,y=40,x=60
    fmt.Println(b(50))  // adder(60)(50)   x=60,y=50,x=110
}

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix){
            return name + suffix
        }
        return name
    }
}

func main(){
    jpgFunc := makeSuffixFunc(".jpg")
    txtFunc := makeSuffixFunc(".txt")
    fmt.Println(jpgFunc("test"))  // test.jpg
    fmt.Println(txtFunc("test"))  // test.txt
}

闭包进阶示例3:

func calc(base int) (func(int) int, func(int) int){
    add := func(i int) int{
        base += i
        return base
    }
    
    sub := func(i int) int{
        base -= i
        return base
    }
    return add,sub
}

func main(){
    a,b := calc(10)   // base = 10
    fmt.Println(a(1),b(2))  // a(1)->add(10)(1)->base=10,i=1,base=11;
    						// b(2)->sub(11)(2)->base=11,i=2,base=9
    fmt.Println(a(3),b(4))  // a(3)->add(9)(3)->base=12, b(4)->8
    
}

闭包的好处:内层函数的状态被保存在闭包中,不使用闭包就要开辟多个全局变量来保存函数以外的数据。如果这个函数被多方调用,大家都需要各自保存各自的数据 ,此时就需要开辟多个全局变量,具体使用哪个全局变量,还要在函数内做判断,增大了重复代码。令代码开起来比较垃圾。

使用多个全局变量保存多套副本的索引
// 全局变量
var heros = [...]string{"关胜","林冲","秦明","呼延灼","武松","鲁达"}
// 宋江的索引
var index1 = 0
// 吴用的索引
var index2 = 0
// 脑补卢员外的索引,柴进的索引...

func useNormal(){
    for i:=0; i<10;i++{
        fmt.Println(giveMeOne("宋江"))
    }
    for i:=0;i<10;i++{
        fmt.Println(giveMeOne("吴用"))
    }
}

func giveMeOne(who string) string{
    var theOne = ""
    // 差不多的东西写两遍,很垃圾
    // 万一卢俊义也来带队,那就需要三个全局变量,三个if分支。。。
    if who == "宋江"{
        theOne = heros[index1]
        index1++
        if index1 > len(heros) -1{
            index1 = 0
        }
    } else {
        theOne = heros[index2]
        index2 ++
        if index2 >len(heros)-1{
            index2 = 0
        }
    }
    return theOne
}
使用闭包函数
// 全局变量
var heros = [...]string{"关胜","林冲","秦明","呼延灼","武松","鲁达"}
// 使用函数闭包的案例
func useClosure(){
    // 得到返回的闭包内函数
    songjiang := giveHimOne(0)
    wuyong := giveHimOne(4)
    for i:=0;i<10;i++{
        fmt.Println("宋江线", songjiang("黑子"))
    }
    for i:=0;i<10;i++{
        fmt.Println("吴用线", wuyong("大坏比"))
    }
}

// 闭包函数:返回函数的函数
func giveHimOne(start int)func(name string)string{
    // 保存闭包系统内的状态
    var index int = start
    // 内层函数
    return func(name string) string{
        theOne := heros[index]
        // 状态被保存在外层的闭包中
        index++
        if index > len(heros)-1{
            index = 0
        }
        return name + ":" + theOne        
    }
}

defer语句

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

示例:

func main(){
    fmt.Println("start")
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println("end")
}

输出:

start
end
3
2
1

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题,比如:资源清理,文件关闭,解锁及记录时间等。

defer执行时机

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

func f1() int {
    x := 5
    defer func(){
        x++    // 改变的是x,不是返回值
    }()
    return x
}

func f2() (x int){
    defer func(){
        x++
    }()
    return 5    // 最终返回x,先给x赋值,x=5,执行defer,x++,此时x=6, 最后返回x
}

func f3() (y int){
    x := 5
    defer func(){
        x++    // 改变的是y
    }()
    return x   // 返回x   5
}

func f4() (x int){
    defer func(x int){
        x++     // 改变x的副本
    }(x)
    return 5    // 5
}

func f5()(x int){
    defer func(x int) int {
	x ++
	return x   // 改变x的副本
    }(x)
    return 5     // 5
}

// 传一个x的指针
func f6()(x int){
    defer func(x *int){
	*x ++
    }(&x)
    return 5  // 1.返回值=x=5; 2.defer x=6, 3.返回x
}

func main(){
    fmt.Println(f1())    // 5
    fmt.Println(f2())    // 6
    fmt.Println(f3())    // 5
    fmt.Println(f4())    // 5
    fmt.Println(f5())    // 5
    fmt.Println(f6())    // 6
}

defer面试题

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: a = 1, b = 2
//2: calc("A",1,2)->3
//3: defer calc("AA",x,3)->defer calc("AA",1,3)
//4: x = 10
//5: calc("B",10,2)->12
//6: defer calc("BB",10,12)
//7: y = 20
//8: 执行6 
//9:执行3

递归函数

递归:函数自己调用自己

// 计算n的阶乘
func f(n uint64)uint64{
    if n<= 1{
	return 1
    }
    return n * f(n-1)
}

// 上台阶面试题
// n个台阶,一次走一步,也可以走两步,有多少种走法
func taijie(n uint64) uint64{
    if n == 1{
	return 1
    }
    if n == 2{
	return 2
    }
    return taijie(n-1) + taijie(n-2)
}

内置函数介绍

内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

panic/recover

使用panic/recover模式来处理错误。panic可以再任何地方引发,但recover只有在defer调用的函数中有效。

func f1(){
	fmt.Println("func f1")
}

func f2(){
	panic("panic in f2")
}

func f3(){
	fmt.Println("func f3")
}

func main(){
	f1()
	f2()
	f3()
}

输出:

func f1
panic:panic in f2

goroutine 1 [running]:
main.f2(...)
        .../code/func/main.go:12
main.main()
        .../code/func/main.go:20 +0x98

程序运行期间f2中引发panic导致程序崩溃,异常退出了,这时候我们就可以通过recover将程序恢复回来,继续往后执行。

func funcA(){
    fmt.Println("func A")
}

func funcB(){
    defer func(){
        err := recover()
        // 如果程序出现了panic错误,可以通过recover恢复过来
        if err != nil{
            fmt.Println("recover in B")
        }
    }()
    panic("panic in B")
}

func funcC(){
    fmt.Println("func C")
}

func main(){
    funcA()
    funcB()
    funcC()
}

注意:

recover()必须搭配defer使用

defer一定要在可能引发panic的语句之前定义

练习题

你有50枚金币,需要分配给一下几个人:Mathew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth.
分配规则如下:
a.名字中每包含1个'e'或'E'分1枚金币
b.名字中没包含1个'i'或'I'分2枚金币
c.名字中没包含1个'o'或'I'分O枚金币
b.名字中没包含1个'u'或'I'分U枚金币

写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
程序结构如下,请实现'dispatchCoin'函数

var (
    coins = 50
    users = [] string{"Matthew","Sarah","Augustus","Heidi","Emilie","Peter","Giana","Adriano","Aaron","Elizabeth",}
    distribution = make(map[string]int, len(users))
)

func main() {
    left := dispatchCoin()
    fmt.Println("剩下:",left)
    fmt.Println(distribution)
}

func dispatchCoin()(left int){
    for _,name:=range users{
	for _,c:=range name{
	    switch  c {
	    case 'e','E':
	    // 满足这个条件,分一枚金币
	        distribution[name] ++
		coins --
	    case 'i','I':
		distribution[name] += 2
		coins -= 2
	    case 'o','O':
		distribution[name] +=3
		coins -= 3
	    case 'u', 'U':
		distribution[name] += 4
		coins -= 4
	    }
        }
    }
    return coins
}

本文参考:https://www.liwenzhou.com/posts/Go/09_function/

原文地址:https://www.cnblogs.com/huiyichanmian/p/12768750.html