Go语言基础之函数

1 函数概述

函数可以将一个语句序列打包为一个单元,然后可以从程序中其他地方很多次调用。函数的机制可以将一个大的工作分解为小的任务,这样的小任务可以让不同程序员在不同时间、不同地方独立完成。函数减少了代码的重复编写,增加了代码的利用率。

Golang中的函数不支持嵌套、重载和默认参数。

  • 无需声明原型

  • 支持不定长变参

  • 支持多返回值

  • 支持命名返回参数

  • 支持匿名函数和闭包

2 函数基本语法

2.1 函数定义

func 函数名 (形参列表) (返回值列表) {
    执行语句...
    return 返回值列表
}
  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内函数名不能重复;

  • 形参列表:表示函数的输入;

  • 函数中的语句:表示为了实现某一功能代码块;

  • 函数可以有返回值,也可以没有;

下面定义一个求直角三角形的边长的函数:

func hypot(x, y float64) float64 {	//类型相同的相邻参数可合并
    return math.Sqrt(x*x + y*y)
}

2.2 函数参数

函数中的参数称为形参,用来接收实参的传递。

2.2.1 可变参数

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

可变参数通常要作为函数的最后一个参数。

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

func main() {
	res1 := intSum(10)
	res2 := intSum(10, 3)
	res3 := intSum(10, 20, 3)
	res4 := intSum(10, 20, 3, 10)
	fmt.Println(res1, res2, res3, res4)  //10 13 33 43
}

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

2.2.2 参数传递方式

实参向形参传递参数时,传递给函数的都是变量的副本。不同的是值传递的是值的拷贝,引用传递的是地址的拷贝。一般来说,地址拷贝效率高,因为数据量小,而值拷贝取决于拷贝的数据大小,数据越大,效率越低。

值类型和引用类型

  • 值类型:int系列,float系列,bool,string,数组和结构体
  • 引用类型:指针,slice切片,map,管道chan,interface等都是引用类型

值传递和引用传递使用特点

值类型默认是值传递:变量直接存储值,内存通常在栈中的分配如下示意图:

引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常分配在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就是一个垃圾,有GC来回收。引用类型的内存分配之意图:

如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。

func test(n1 *int) {
	*n1 = *n1 + 10
	fmt.Println("test() n1=", *n1)
}

func main() {
	num := 20
	test(&num)
	fmt.Println("main() num=", num)
}

2.3 函数返回值

Go语言中通过return关键字向外输出返回值

2.3.1 多返回值

Go语言中的函数支持多返回值,函数如果有多个返回值必须用() 将所有返回值包裹起来,示例如下:

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

2.3.2 返回值命名

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

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

3 函数进阶

3.1 函数的调用

3.1.1 函数调用过程

在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他的栈的空间区分开来

在每个函数对应的栈中,数据空间是独立的,不会混淆

当一个函数调用完毕(执行完毕)后,程序会销毁这个函数对应的栈空间

3.1.2 函数递归调用

一个例子:整个数组中找最大值,代码如下:

func getMax(nums []int, L, R int) int {
	if L == R {
		return nums[L]
	}
	mid := (L + R) / 2
	maxLeft := getMax(nums, L, mid)
	maxRight := getMax(nums, mid+1, R)
	return int(math.Max(float64(maxRight), float64((maxLeft))))
}

func main() {
	var nums = []int{4, 3, 2, 1}
	maxNum := getMax(nums, 0, len(nums)-1)
	fmt.Println(maxNum)
}

无论在哪种语言中,函数的调用的过程都是压栈弹栈的过程。函数递归过程是一个函数在调用子过程之前,会把自己所有的过程(程序执行到哪一行、所有的参数、变量)压栈,信息完全保存。子过程返回后,会利用这些信息彻底还原现场,继续进行,最终串起来所有子过程与父过程的通信。下面具体来看getMax()函数执行的过程:

递归调用总结:

  • 执行一个函数是,就创建一个新的受保护的独立空间(新函数栈);
  • 函数的局部变量是独立的,不会相互影响;
  • 递归必须向退出递归的条件逼近,否则就是无限递归;
  • 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁。

3.2 变量作用域

在函数中变量分为全局变量和局部变量。

函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部

函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效

如果变量是在一个代码块,比如for/if中,那么这个变量的作用域就在该代码块

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

3.3 函数类型与变量

函数是第一类对象,可以将复杂签名定义为函数类型,以便阅读。

3.3.1 函数类型的定义

使用type关键字来定义一个函数类型,如下示例:

type FormatFunc func(s string, x, y int) string //定义函数类型

上面定义了一个FormatFunc的函数类型,其可以接收一个string类型和两个int类型的参数,并且返回一个string类型的返回值。

如果一个函数的签名(考虑因素:参数和返回值的个数、类型、顺序)和这个函数相同,那么这个函数就属于FormatFunc类型,例如下面的add和sub函数都是FormatFunc类型:

func add(s string, x, y int) string {
    return fmt.Sprintln(s, x+y)
}

func sub(s string, x, y int) string {
    return fmt.Sprintln(s, x-y)
}

3.3.2 函数类型变量

如果声明了一个函数类型,那么就可以使用该函数类型声明一个变量,并且可以为该变量赋值:

type FormatFunc func(s string, x, y int) string  //定义函数类型

func add(s string, x, y int) string {
	return fmt.Sprintln(s, x+y)
}

func main() {
	var f FormatFunc  //声明一个FormatFunc类型的变量f
	f = add			//把add赋值给f
	fmt.Printf("type of f:%T
", f)  //type of f:main.FormatFunc
	fmt.Println(f("10 + 2 =", 10,2))  //10 + 2 = 12

	v := add
	fmt.Printf("type of v:%T
", v)  //type of v:func(string, int, int) string
	fmt.Println(f("5 + 7 =", 5,7))  //5 + 7 = 12
}

3.4 init函数

3.4.1 init函数介绍

每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被go运行框架调用,也就是说init会在main函数前被调用。

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

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

运行结果:

init()...
main()...

3.4.2 init函数注意事项和细节

如果一个文件同时包含全局变量定义,init函数和main函数,则执行流程全局变量定义->init函数->main函数

var age = test()

func test() int {
	fmt.Println("test()")
	return 24
}

// 通常可以在init函数中完成初始化工作
func init() {
	fmt.Println("init()")
}

func main() {
	fmt.Println("main()...age=", age)
}

运行结果:

test()
init()
main()...age= 24

init函数最主要的作用就是完成一些初始化的工作,比如下面的案例:

//utils包中的utils.go
package utils

import "fmt"

var Age int
var Name string

//Age和Name是全局变量,需要在main.go中使用,所以要对其初始化

func init() {
	fmt.Println("utils 包的 init()...")
	Age = 19
	Name = "tom"
}

//main.go中导入utils包
var age = test()

//为了看到全局变量是先被初始化,先写函数
func test() int {
	fmt.Println("test()")
	return 23
}

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

func main() {
	fmt.Println("main()...age=", age)
	fmt.Println("Age=", utils.Age, "Name=", utils.Name)
}

执行结果:

utils 包的 init()...
test()
init()
main()...age= 23
Age= 19 Name= tom

细节说明,面试题:如上案例,如果main.goutils.go都含有变量定义,init函数时执行流程是什么?

  • 最先执行utils.go中的变量定义。然后是init函数
  • 之后执行main.go中的变量定义,然后是init函数,最后是main函数

3.5 高阶函数

函数在是一等公民,函数也是对象,是可调用的对象,函数可以作为普通变量、参数、返回值等等。那么什么是高阶函数?在数学的概念中是将一个函数作为一个函数的参数,形如y=g(f(x))。所以在数学和计算机科学中,高阶函数应当是至少满足下面一个条件的函数:

  • 接受一个或多个函数作为参数
  • 输出一个函数

3.5.1 函数作为参数

函数作为参数示例:

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() {
    ret := calc(10, 20, add)
    fmt.Println(ret)	//30
}

3.5.2 函数作为返回值

函数作为返回值示例:

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

3.6 匿名函数

Go支持匿名函数,匿名函数就是没有名字的函数,如果某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。

3.6.1 匿名函数使用方式

匿名函数使用方式1

在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。

func main() {
    //使用匿名函数求两数之和
    res := func (n1, n2 int) int {
        return n1 + n2
    }(10, 20)
    
    fmt.Println("res=", res)
}

匿名函数使用方式2

将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数。

func main() {
    //使用匿名函数求两数之差
    a := func (n1, n2 int) int {
        return n1 - n2
    }
    res := a(10, 20)
    fmt.Println("res=", res)
}

3.6.2 全局匿名函数

如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,在整个程序中有效

var (
    //fun是一个全局匿名函数
    fun = func(n1, n2 int) int {
        return n1 * n2
    }
)

func main() {
    res := fun(2, 3)
    fmt.Println("res=", res)
}

3.7 闭包

3.7.1 闭包介绍

闭包就是一个函数和与其相关的引用环境组和的一个整体,怎么理解这句话呢?看下面例子:

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))
	fmt.Println(f(2))
	fmt.Println(f(3))
	fmt.Println(f(4))
}
  • AddUpper是一个函数,返回的数据类型是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包
  • 当反复的调用f函数时,因为n是初始化一次,因此每调用一次进行累加
  • 闭包只会产生在高阶函数中,要搞清楚闭包的关键,就是要分析出返回的函数或调用的函数它使用到哪些变量,因为函数和它引用到的变量共同构成闭包
  • 对上面代码的一个修改,加深对闭包的理解
func AddUpper() func (int) int {
	var n int = 10
    var str = "hello"
	return func (x int) int {
		n = n + x
        str += string(36)  // 36 -> '$'
        fmt.Println("str=", str)
		return n
	}
}

func main() {
	f := AddUpper()
	fmt.Println(f(1))
	fmt.Println(f(2))
	fmt.Println(f(3))
	fmt.Println(f(4))
}

3.7.2 闭包的最佳实践

编写一个程序,具体要求如下:

  • 编写一个函数makeSuffix(suffic string)可以接收一个文件后缀名(比如.mp4),并返回一个闭包
  • 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.mp4),则返回文件名.mp4,如果已经有.mp4后缀,则返回原文件名
  • 要求使用闭包的方式完成
  • string.HasSuffix该函数可以判断某个字符串是否有指定的后缀
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
}

上面代码的总结和说明:

  • 返回的匿名函数和makeSuffix(suffix string)suffix变量组和成一个闭包,因为返回函数引用到suffix这个变量
  • 如果使用传统的方法,需要每次都传入文件名.suffix,而闭包因为可以保留上次引用的某个值,所以传入一次就可以反复使用

闭包进阶示例:

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() {
	f1, f2 := calc(10)
	fmt.Println(f1(1), f2(2)) //11 9
	fmt.Println(f1(3), f2(4)) //12 8
	fmt.Println(f1(5), f2(6)) //13 7
}

闭包其实并不复杂,只要牢记闭包=函数+引用环境

3.8 函数的defer

在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放中资源,Go的设计者提供defer(延时机制)。

3.8.1 defer执行时机

在Go语言的函数中return语句在底层并不是原子操作,它分别给返回值赋值和RET指令两步。而defer语句执行的时机就是在返回值赋值操作后,RET指令执行前。具体如下图所示:

3.8.2 defer的使用

func sum(n1 int, n2 int) int {
    //当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈,为了方便理解)
    //当函数执行完毕后,再从defer栈,按照先进后厨的方式出栈执行
    defer fmt.Println("ok1 n1=", n1)
    defer fmt.Println("ok2 n2=", n2)
    
    res := n1 + n2
    fmt.Println("ok3 res=", res)
    return res
}

func main() {
    res := sum(10, 20)
    fmt.Println("res=", res)
}

输出结果:

ok3 res= 30
ok2 n2= 20
ok1 n1=10
res=30

defer将语句放入到栈时,也会将相关的值拷贝同时入栈。请看下面代码:

func sum(n1 int, n2 int) int {
    //当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈,为了方便理解)
    //当函数执行完毕后,再从defer栈,按照先进后厨的方式出栈执行
    defer fmt.Println("ok1 n1=", n1)
    defer fmt.Println("ok2 n2=", n2)
    
    n1++
    n2++
    res := n1 + n2
    fmt.Println("ok3 res=", res)
    return res
}

func main() {
    res := sum(10, 20)
    fmt.Println("res=", res)
}

输出结果:

ok3 res= 32
ok2 n2= 20
ok1 n1= 10
res= 32

3.8.3 defer经典案例

思考下面的代码,写出执行的结果:

func f1() int {
	x := 5
	defer func() {
		x++
	}()
	return x
}

func f2() (x int) {
	defer func() {
		x++
	}()
	return 5
}

func f3() (y int) {
	x := 5
	defer func() {
		x++
	}()
	return x
}
func f4() (x int) {
	defer func(x int) {
		x++
	}(x)
	return 5
}
func main() {
	fmt.Println(f1())
	fmt.Println(f2())
	fmt.Println(f3())
	fmt.Println(f4())
}

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

上面代码的输出结果是?(提示:defer注册要延迟执行的函数时,该函数所有的参数都需要确定其值)

3.9 panic/recover

Go语言中目前没有异常机制,但是使用panci/recover模式来处理错误。panic可以在任何地方引发,但recover只有在defer调用的函数中有效。如下示例:

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

func funcB() {
	panic("panic in B")
}

func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}

输出结果:

func A
panic: panic in B

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

程序运行期间funcB中引发了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()
}

注意:

  • revocer()必须搭配defer使用
  • defer一定要在可能引发panic的语句之前定义
原文地址:https://www.cnblogs.com/dabric/p/12342888.html