壹 前言
在学习函数之前,需要明白为什么要学习函数?
首先有这么一段代码:
代码1:
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
代码2:
#这段代码的意思是将6句Hello!World!放进fun()自定义函数中,并调用fun()函数
func fun(){
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fun()
也许现在会很疑惑,这不就是简单的fmt.Println函数打印6遍而已吗?
是的,他们看上去差不多,甚至有人会觉得代码2比代码1复杂许多,但是你想想如果这6句Hello!World!不是单单在一处地方打印时,而是上百处上千处时,那要把代码1打印多久?而代码2只要把定义写完,想在那打印就调用一下即可。函数在刚学习的时候不容易发现他的长处,甚至觉得比较麻烦,但是到了以后做大项目的时候,会发现编程因为有了函数而变得有趣和简洁。
贰 什么是函数
函数是基本的代码块,用于执行一个任务,Go 语言最少有个 main()
函数,你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务,函数声明告诉了编译器函数的名称,返回类型,和参数。
Go 语言标准库提供了多种可动用的内置的函数。例如,len()
函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。
叁 创建函数
格式
func 函数名( [参数] ) [返回值] {
函数体
[return]
}
- func:函数由
func
开始声明 - 函数名:函数名称,参数列表和返回值类型构成了函数签名。
- 参数:参数列表,参数由参数变量和参数变量的类型组成,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数是可选的,也就是说函数也可以不包含参数。多个参数之间使用
,
分隔。 - 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分隔。有些功能不需要返回值,这种情况下返回值不是必须的,就不写了。 - 函数体:实现指定功能的代码块。
- return:要返回的值,根据返回值确定需不需要,需要就返回值,不需要就不写。
代码
func funtext( x int ) int {
y := x + 1
return y
}
肆 函数的调用以及返回值
4.1 多返回值
当创建函数时,你定义了函数需要做什么,通过调用该函数来执行指定任务。定义好后的函数,我们直接根据函数加对应参数,即可调用,同时go可以根据函数的[返回值]
个数返回对应多个值。如果有返回类型,但是不返回return的话,会报错,所以有几个[返回值],就要有就几个返回值。
格式:
func 函数名( [参数] ) [返回值类型] {
函数体
return 返回值1,返回值2,...
}
// 调用通过 函数名([参数])的方式调用函数,这里的参数根据定义函数时有无参数决定的,没有不用写
函数名([参数])
例子:
package main
import "fmt"
func main() {
sum1, sum2 := 10, 20
fmt.Println(fun(sum1, sum2))
}
//返回多少个值,[返回值类型]内就需要多少个类型,当然如果没有的话就不用写返回类型和return
func fun(a int, b int) (int, int) {
//这里如果没有下面这行返回对应数量返回值的话会报错
return a + b, b
}
4.2 返回值命名
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return
关键字返回。
格式:
func 函数名( [参数] ) (返回值1 返回值类型1, 返回值2 返回值类型2,...) {
函数体
return
}
例子:
package main
import "fmt"
func main() {
ret1, ret2 := calc(100, 200)
fmt.Println(ret1, ret2)
}
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
4.3 返回值补充
- 当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}
- 在编程中我们需要报错来判断该方法是否执行成功,但是出现报错会将程序中断,这时候我们可以只写
return
即可!func portScanner(Host, Port string) { // 这个方法是go的TCP连接方法 conn, porterr := net.Dial("tcp", Host+":"+Port) if porterr != nil { // 可以看到我们需要这个返回值,防止程序终端 return } conn.Close() fmt.Println(Port, "端口打开") }
伍 参数传递以及函数参数
函数如果使用参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。调用函数,可以通过两种方式来传递参数:值传递、引用传递。
传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
5.1 值传递
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
默认情况下,Go 语言调用函数使用的是值传递,即在调用过程中不会影响到实际参数。
package main
import "fmt"
func main() {
/* 定义局部变量 */
a, b := 100, 200
fmt.Printf("交换前 a 的值为 : %d\n交换前 b 的值为 : %d\n", a, b)
/* 通过调用函数来交换值 */
swap(a, b)
fmt.Printf("交换后 a 的值 : %d\n交换后 b 的值 : %d\n", a, b)
}
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int
temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/
return temp
}
函数中使用的是值传递,所以两个值并没有实现交互。
5.2 引用传递(需要后面的指针知识)
这个知识我们了解就行,后面学习了指针再回来学习。
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
引用传递指针参数传递到函数内,以下是交换函数 swap() 使用了引用传递:
package main
import "fmt"
func main() {
/* 定义局部变量 */
a, b := 100, 200
fmt.Printf("交换前 a 的值为 : %d\n交换前 b 的值为 : %d\n", a, b)
/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Printf("交换后 a 的值 : %d\n交换后 b 的值 : %d\n", a, b)
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
5.3 普通参数
普通参数定义,例如:
// 通常情况下每个参数都包含参数名和类型
func intSum1(x int, y int) int {
return x + y
}
// 而如果相邻变量的类型相同,则可以省略类型
func intSum2(x, y int) int {
return x + y
}
//intSum2函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型
5.4 可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...
来标识。注意:可变参数通常作为函数的最后一个参数。
例子:
package main
import "fmt"
func main() {
ret5 := intSum2(100)
ret6 := intSum2(100, 10)
ret7 := intSum2(100, 10, 20)
ret8 := intSum2(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8)
}
func intSum2(x int, y ...int) int {
fmt.Println(x, y)
sum := x
for _, v := range y {
sum = sum + v
}
return sum
}
本质上,函数的可变参数是通过切片来实现的。
陆 函数数组传参
如果你想向函数传递数组参数,你需要在函数定义时,声明形参为数组,我们可以通过以下方式来声明:
- 形参设定数组大小:
例子:void myFunction(param [10]int){ ... }
package main import "fmt" func main() { /* 数组长度为 5 */ var balance = [5]int {1000, 2, 3, 17, 50} var avg float32 /* 数组作为参数传递给函数 */ avg = getAverage( balance, 5 ) ; /* 输出返回的平均值 */ fmt.Printf( "平均值为: %f ", avg ); } func getAverage(arr [5]int, size int) float32 { var i,sum int var avg float32 for i = 0; i < size;i++ { sum += arr[i] } avg = float32(sum) / float32(size) return avg; }
柒 函数类型与变量
7.1 定义函数类型
假设我们要将函数定义成一个可以在不同场景下使用,但是功能一样的函数时,我们可以使用type
关键字来定义一个函数类型,具体格式如下:
type calculation func(int, int) int
//用于定义函数类型
//calculation为函数类型名字
上面语句定义了一个叫calculation
类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
add和sub都能赋值给calculation类型的变量。
var c calculation
c = add
7.2 函数类型变量
我们可以声明函数类型的变量并且为该变量赋值:
func main() {
var c calculation // 声明一个calculation类型的变量c
c = add // 把add赋值给c,这里的add函数是上面定义过的,这里只是引用
fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
fmt.Println(c(1, 2)) // 像调用add一样调用c
f := add // 将函数add赋值给变量f1
fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
fmt.Println(f(10, 20)) // 像调用add一样调用f
}
需要明白一点,在函数赋值给其他变量时,如果带括号返回的是return的值,不是本身,而如果不带括号,则是函数本身赋值给其他变量。
捌 高阶函数
高阶函数分为函数作为参数和函数作为返回值两部分。
8.1 函数作为参数
函数可以作为参数:
func add(x, y int) int {
return x + y
}
func calc(x, y int, funtext func(int, int) int) int {
return funtext(x, y)
}
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}
8.2 函数作为返回值
函数也可以作为返回值:
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
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
}
}
玖 闭包以及匿名函数
9.1 匿名函数
在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数,匿名函数就是没有函数名的函数,匿名函数多用于实现回调函数和闭包。
9.1.1 格式
匿名函数的定义格式如下:
//可以看到没有函数名,而是将其赋值给一个变量,不赋值到变量里,该匿名函数就没有意义,或者直接在后面加参数执行,具体实现下面会讲
valfun := func(参数)(返回值){
函数体
}
9.1.2 例子
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
package main
import "fmt"
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
9.2 闭包
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
。 首先我们来看一个例子:
// func(int)是adder()函数返回值,而int是func(int)返回值
func adder() func(int) int {
var x int
// fmt.Println(x)
// x不进行赋值默认是0
return func(y int) int {
x += y
return x
}
}
func main() {
// 这就就是将func(y int) int {
// x += y
// return x
// }
// 这个匿名函数传给f,这时候x已经进行了实例化,即为0,这样就直接保留了x这个变量的值
var f = adder()
// 接着我们将10赋值到f里,就是将10赋值给y,然后进行x=x+y运算
fmt.Println(f(10)) //10
// 接着我们将20赋值到f里,就是将20赋值给y,然后进行x=x+y运算,由于前面x的已经被f(10)改变为10,所以这里x=x+y=10+20=30
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60
// 由于这里是新的变量赋值,所以重新实例化一个adder函数,x也被重新实例化,但是不影响之前的x
f1 := adder()
fmt.Println(f1(40)) //40
fmt.Println(f1(50)) //90
}
变量f
是一个函数并且它引用了其外部作用域中的x
变量,此时f
就是一个闭包。 在f
的生命周期内,变量x
也一直有效。可以看到闭包就是将adder这个函数的返回值(一个匿名函数)和adder函数内的作用域环境给f
。
闭包进阶示例1:
func adder2(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder2(10)
fmt.Println(f(10)) //20
fmt.Println(f(20)) //40
fmt.Println(f(30)) //70
f1 := adder2(20)
fmt.Println(f1(40)) //60
fmt.Println(f1(50)) //110
}
闭包进阶示例2:
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return "1"
}
}
func main() {
jpgFunc := makeSuffixFunc(".jpg")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc(".jpg")) //1
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() {
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
}
闭包其实并不复杂,只要牢记闭包=函数+引用环境
。
拾 递归函数
10.1 递归介绍
递归,就是在运行的过程中调用自己。语法格式如下:
func recursion() {
recursion() /* 函数调用自身 */
}
func main() {
recursion()
}
Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中。递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。
10.2 阶乘
以下实例通过 Go 语言的递归函数实例阶乘:
package main
import "fmt"
func Factorial(n uint64)(result uint64) {
if (n > 0) {
result = n * Factorial(n-1)
return result
}
return 1
}
func main() {
var i int = 15
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}
10.3 斐波那契数列
以下实例通过 Go 语言的递归函数实现斐波那契数列:
package main
import "fmt"
func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-2) + fibonacci(n-1)
}
func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d\t", fibonacci(i))
}
}
拾壹 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")
}
由于defer
语句延迟调用的特性,所以defer
语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
defer执行时机:
在Go语言的函数中return
语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。
如下示例
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
以下是defer三条使用规则:
当defer被声明时,其参数就会被实时解析
即defer的参数的变量值,在代码中defer使用后的位置改变并不会对改变defer用到的值defer执行顺序为先进后出
在函数中,先定义的defer将会在最后执行defer可以读取函数内变量
defer代码块的作用域仍然在函数之内,因此defer仍然可以读取函数内的变量defer执行在panic语句之前
当出现panic语句的时候,会先按照defer的后进先出的顺序执行,最后才会执行panic