go语言函数的学习笔记
函数
1.1 函数的概念
函数是执行特定任务的代码块,也是最基本的代码块。可以被多次调用。
Go是编译型语言,所以编写顺序无所谓;但最好把 main
函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。而且,同一个任务(函数)可以被调用多次,有助于代码重用。
当函数执行到代码块最后一行(}
之前)或者 return
语句的时候会退出,其中 return
语句可以带有零个或多个参数;这些参数将作为返回值供调用者使用。简单的 return
语句也可以用来结束 for 死循环,或者结束一个协程(goroutine)。
除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。
不正确的函数代码:
func fun1()
{
}
它必须是
func fun1(){
}
1.2 函数参数与返回值
语法格式:
func funcName(arg1 type1, arg2 type2...)( output1 type1, output2 type2){
// 逻辑代码
//返回多个值
return value1, value2
}
-
func: 函数有func开始声明
-
funcName: 函数名称
-
arg1 type1: 参数列表
-
output1 type1: 返回类型以及返回值
一个简单的例子:
func main() {
println("before call func")
sayHello()
println("after call func")
}
func sayHello() {
println("hello, world !")
}
输出如下:
before call func
hello, world !
after call func
返回两个值的函数例子:
package main
import (
"fmt"
"strings"
)
func main() {
a, b := myFunc1("ABC", "bnm")
fmt.Printf("a: %s, b:%s", a, b)
}
func myFunc1(arg1 string, arg2 string) (string, string) {
fmt.Printf("a: %s, b:%s\n", arg1, arg2)
//将第一个传入值转小写,第二个转换大写
return strings.ToLower(arg1), strings.ToUpper(arg2)
}
输出:
a: ABC, b:bnm
a: abc, b:BNM
函数重载:指可以编写多个同名函数,只要它们拥有不同的参数或者不同的返回值。但是在 Go 里面函数重载是不被允许的。
例子:
package main
import (
"fmt"
"strings"
)
func main() {
a, b := myFunc1("ABC", "bnm")
fmt.Printf("a: %s, b:%s", a, b)
}
func myFunc1(arg int) (int, int) {
return 0, 0
}
func myFunc1(arg1 string, arg2 string) (string, string) {
fmt.Printf("a: %s, b:%s\n", arg1, arg2)
return strings.ToLower(arg1), strings.ToUpper(arg2)
}
myFunc1 redeclared in this block
previous declaration at ./function_demo.go:13:29
exit status 2
Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以你需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名
如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:
func flushICache(begin, end uintptr) // implemented externally
函数也可以以申明的方式被使用,作为一个函数类型,就像:
type binOp func(int, int) int
函数值(functions value)之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数来破除这个限制。
目前 Go 没有泛型(generic)的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口(interface),特别是空接口与类型选择(type switch)与/或者通过使用反射(reflection)来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。
可变参数
传入函数的参数不确定,参考以下例子:
func main() {
fmt.Println(getSum(1, 2, 3, 4))
}
func getSum(number ...int) int {
sum := 0
for i := 0; i < len(number); i++ {
sum += number[i]
}
return sum
}
输入如下:
10
注意事项:
1.如果一个函数的参数是可变参数并且还有其他参数,那么可变参数需要放到函数参数最后。
2.一个函数参数列表,最多只有一个可变参数。
空白标识符
专门用于舍弃数据:_
假如一个函数具有多返回值,但是我们只需要其中一个,那么空白标识符就派上用场了。
以下面代码为例,假如一个函数可以返回int和string 两种类型的数据,我们只需要用到返回的字符串数据,那这时_
就会用来舍弃返回的int数据。
func main() {
_, str := getInfo()
fmt.Println(str)
}
func getInfo() (int, string) {
return 123, "hello"
}
输出:
hello
那这时候可能有人会有疑问,我既然只想用其中一个返回值,那我们在程序设计的时候,只返回一个值不就可以了吗? 确实是可以这样,但是如果我们需要调用第三方的方法时,这可就由不得我们了,难不成让我把刀架在开发者脖子上,让他重构代码吗?很显然,多返回值加空白标识符的组合可以让我们节省很多时间,尽最大可能去节省代码空间,提高代码复用率。但如果多返回值在函数设计用不好的话,反而会引起逻辑混乱,所以也是一柄双刃剑,但我相信大多数人还是能很轻松地掌握这点。
1.3 按值传递和引用传递
go默认使用按值传递,传递的是值的副本,也就是说传入的值如果发生改变,不会影响原来的变量。
如果我们希望可以更改参数的值,并且将传入的值本身也进行改变,那么我们需要将参数的地址传递给函数,也就是说这就是引用传递。
几乎在任何情况下,传递指针的消耗要比传递副本少很多。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
有些函数没有返回值,因为我们不需要用到它的返回值,只是用来做某些操作而已,比如说发邮件,记录log。但即使函数没有定义返回值,我们依然可以使用return来停止函数逻辑,退出函数。
如果一个函数需要返回很多值,可以传递一个切片给函数(返回值具有相同类型),或者传递一个结构体(如果返回都是不同类型)。
关于按值传递和引用传递可以参考以下代码:
func main() {
//此处传的是切片,传递的是引用
arr := []int{1, 2, 3, 4}
fmt.Println("引用传递:->函数前,切片数据:", arr)
testFunc1(arr)
fmt.Println("引用传递:->函数后,切片数据:", arr)
//此处传的是数组,传递的是数组的副本
arr2 := [4]int{3, 4, 5, 6}
fmt.Println("值传递:->函数前,切片数据:", arr2)
testFunc2(arr2)
fmt.Println("值传递:->函数后,切片数据:", arr2)
}
func testFunc1(arr []int) {
fmt.Println("引用传递:->函数中,切片数据:", arr)
arr[0] = 100
fmt.Println("引用传递:->函数中,切片数据更改后:", arr)
}
func testFunc2(arg [4]int) {
fmt.Println("值传递:->函数中,切片数据:", arg)
arg[0] = 100
fmt.Println("值传递:->函数中,切片数据更改后:", arg)
}
输出如下:
引用传递:->函数前,切片数据: [1 2 3 4]
引用传递:->函数中,切片数据: [1 2 3 4]
引用传递:->函数中,切片数据更改后: [100 2 3 4]
引用传递:->函数后,切片数据: [100 2 3 4]
值传递:->函数前,切片数据: [3 4 5 6]
值传递:->函数中,切片数据: [3 4 5 6]
值传递:->函数中,切片数据更改后: [100 4 5 6]
值传递:->函数后,切片数据: [3 4 5 6]
1.4 defer
defer定义
defer语句,也就是延迟语句。顾名思义,延迟语句用于执行函数调用时,用来延迟函数或者方法的执行。
defer用途,比如说一些数据库连接的关闭,删除临时文件,解锁资源等等。
用法类似于面向对象编程语言 Java 和 C# 的 finally
语句块,它一般用于释放某些已分配的资源。
说到defer,go语言关于异常的处理,使用panic() 和 recover(),panic()用于引发恐慌,导致程序中断。
recover函数用于恢复函数执行,而且语法上要求必须在defer中执行。
先看个小例子:
func main() {
defer fmt.Println("defer run...")
fmt.Println("main function start.")
_, str := getInfo()
fmt.Println(str)
fmt.Println("main function end.")
}
输出如下:
main function start.
hello
main function end.
defer run...
我们可以看到,虽然defer写在前面,但是仍然是在最后面执行。
defer执行顺序
如果一段代码有多个defer语句,那么他们的执行顺序上什么样的呢?
答案是先进后出,相当于把这些语句存进栈中,然后从上向下执行,后面进来的先执行。
下面来看一段代码:
func main() {
defer fmt.Println("defer run1...")
fmt.Println("main function start.")
_, str := getInfo()
fmt.Println(str)
defer fmt.Println("defer run2...")
defer fmt.Println("defer run3...")
fmt.Println("main function end.")
}
输出如下:
main function start.
hello
main function end.
defer run3...
defer run2...
defer run1...
defer函数
defer函数调用时,就已经传递参数数据来,只是暂时不执行函数中的代码而已。
所以不管defer语句后面执行什么样的逻辑,传入defer函数里的值是不变的。
func main() {
a := 2
fmt.Println("此时a的值是:", a)
defer deferFunc(a) //此时a的值已经传入函数
a++
fmt.Println("此时a的值是:", a)
}
func deferFunc(a int) {
fmt.Println("deferFunc a的值为:", a)
}
输出如下:
此时a的值是: 2
此时a的值是: 3
deferFunc a的值为: 2
1.5 内置函数
此处参考https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/06.5.md,直接搬运了。
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。
以下是一个简单的列表,我们会在后面的章节中对它们进行逐个深入的讲解。
名称 | 说明 |
---|---|
close | 用于管道通信 |
len、cap | len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map) |
new、make | new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针(详见第 10.1 节)。它也可以被用于基本类型:v := new(int) 。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作(详见第 7.2.3/4 节、第 8.1.1 节和第 14.2.1 节)new() 是一个函数,不要忘记它的括号 |
copy、append | 用于复制和连接切片 |
panic、recover | 两者均用于错误处理机制 |
print、println | 底层打印函数,在部署环境中建议使用 fmt 包 |
complex、real imag | 用于创建和操作复数 |
1.6 函数的本质究竟是什么?
函数可以作为其他函数的参数进行传递,在其他函数中调用执行,这种行为我们称之为回调。
函数作为一种复合数据类型,可以认为是一种特殊变量。
函数名(): 将函数进行调用,函数中代码会全部被执行,然后将return结果返回调用者。
函数名: 指向函数体的内存地址。
func main() {
var c func(int) int
fmt.Printf("%T\n", c) //返回c的type
fmt.Println(c) //只声明了函数,但没有具体实现,所以说nil
fmt.Println(funcTest) //指向内存地址
}
func funcTest(a int) {
fmt.Println(a)
}
输出如下:
func(int) int
<nil>
0x10c1bf0
1.6.1 将函数作为参数
func main() {
callback(1, Add)
}
func Add(a, b int) {
fmt.Println("调用Add函数")
fmt.Println("return : a + b = ", a+b)
}
func callback(y int, f func(int, int)) {
fmt.Printf("第一个参数type: %T, 第二个参数type:%T\n", y, f)
fmt.Println("此时调用传入的函数f : ", f)
f(y, 2)
}
第一个参数type: int, 第二个参数type:func(int, int)
此时调用传入的函数f : 0x10c1a70
调用Add函数
return : a + b =
1.7 匿名函数
顾名思义,匿名函数就说没有名字的函数
定义一个匿名函数,直接进行调用,通常只使用一次。
func main() {
func() {
fmt.Println("匿名函数调用")
}()
}
匿名函数调用
定义带返回值的匿名函数:
func main() {
sum := func(a, b int) int {
return a + b
}(1, 2)
fmt.Println(sum) // 3
}
1.8 闭包
go语言支持函数式编程,也就是说支持将一个函数作为另一个函数的参数。
同时也支持将一个函数作为另一个函数的返回值。
闭包是什么呢?一个外层函数里,有内层函数,这个内层的函数会操作外层函数的局部变量,并且这个外层函数的返回值就是这个内层函数。
局部变量的生命周期会改变,正常的局部变量会随着函数的调用而创建,随着函数调用结束而销毁。
举个例子:
func func1(a, b int){
c:= 1
fmt.Println(a+b+c)
}
在这个函数func1里,存在一个局部变量c,在调用这个函数时,c被初始化为1,然后输出a+b+1, 在函数调用结束后,c的值会被销毁回收。
正常的逻辑就是这样的是吧!没毛病~
但是!在闭包结构里,外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还需要继续使用。
举个例子:
func main() {
funcRef := closurefunc()
fmt.Println(funcRef()) //第一次调用,i=0, i++后,返回1
fmt.Println(funcRef()) //第二次调用,此时i=1,i++后,返回2
fmt.Println(funcRef()) //第三次调用,此时i=2,i++后,返回3
}
func closurefunc() func() int {
i := 0 //局部变量,初始化为0
b := func() int {
i++ //自增加1
return i
}
return b
}
输出结果:
1
2
3
第二个例子,如果我们再定义一个变量也让它等于closurefunc()呢,是接着返回4还是返回1?
func main() {
funcRef := closurefunc()
fmt.Println(funcRef())
fmt.Println(funcRef())
fmt.Println(funcRef())
funcRef2 := closurefunc()
fmt.Println(funcRef2()) //1
fmt.Println(funcRef2()) //2
fmt.Println(funcRef2()) //3
}
func closurefunc() func() int {
i := 0
b := func() int {
i++
return i
}
return b
}
输出结果:
1
2
3
1
2
3
由上面程序结果可知,我们每次定义一个新的变量等于这个闭包时,就会新开辟一块内存指向这个新的变量。
每当新声明一个closurefunc()函数时,就会产生一个新的i,开辟一块内存来存储这个i变量。
下面我们做个小小的demo:
func main() {
funcRef1 := closurefunc()
// fmt.Println(funcRef1())
// fmt.Println(funcRef1())
// fmt.Println(funcRef1())
fmt.Printf("funcRef1的内存地址是: %v\n", &funcRef1)
funcRef2 := closurefunc()
// fmt.Println(funcRef2())
// fmt.Println(funcRef2())
// fmt.Println(funcRef2())
fmt.Printf("funcRef2的内存地址是: %v\n", &funcRef2)
funcRef3 := closurefunc()
fmt.Printf("funcRef3的内存地址是: %v\n", &funcRef3)
}
func closurefunc() func() int {
i := 0
fmt.Printf("函数局部变量的内存地址是: %v\n", &i)
b := func() int {
i++
return i
}
return b
}
输出如下:这时我们可以看到,三个变量funcRef1、funcRef2、funcRef3的地址均不一样,并且closurefunc的变量i
在每次被新声明的时候,也是指向不同的内存地址。
函数局部变量的内存地址是: 0xc0000a4008
funcRef1的内存地址是: 0xc00009e018
函数局部变量的内存地址是: 0xc0000a4010
funcRef2的内存地址是: 0xc00009e028
函数局部变量的内存地址是: 0xc0000a4018
funcRef3的内存地址是: 0xc00009e030
闭包的调试
我们都知道,随着需求的增加,程序或多或少会增加复杂度,那么我们在调试程序的时候,能够掌握和监控这些代码的运行,那无疑帮助将会是巨大的。
runtime和log包中的特殊函数将会为我们提供这些功能。
计算函数执行时间
在程序开始的时候设置一个起点,程序结束时设置一个终点,最后取他们之间的时间差,就能获取到这个程序的运行时间了。我们可以使用time
包中的Now()
和Sub()
函数
func main() {
start := time.Now()
func1()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("function took this amount of time: %s\n", delta)
}
func func1() {
for i := 0; i < 3; i++ {
time.Sleep(time.Second * 2)
}
}
输出如下:
function took this amount of time: 6.004907268s