1. 基本格式
函数是golang中的核心设计,它通过关键字func声明,其基本格式如下:
Go
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
return value1, value2
}
上面的伪代码可以看出函数的以下特点
-
关键字func声明了一个函数的函数名
-
函数可以接收一个或者多个参数,每个参数后面带了类型,通过逗号分隔
-
函数可以返回一个或者多个类型,每个返回值后面也带了类型,通过逗号分隔
-
上面返回值中声明了两个变量output1和output2,如果不想声明也行,直接两个类型即可
-
如果只有一个返回值,则可以省略返回值,返回值类型及括号
-
如果没有返回值,直接省略最后的返回信息
-
如果有返回值,则必须加上return语句
一个简单的例子如下:
Go
package main
import "fmt"
func sum(x, y int) int {
return x + y
}
func main() {
fmt.Println(sum(2, 3))
}
这里一个非常简单的函数sum, 这个函数中传入两个同类型的参数,那么第一个变量的类型可以省略。
2. 多个返回值
golang函数支持多个返回值,直接上例子
Go
package main
import "fmt"
func sumAndProduct(x, y int) (int, int) {
return x + y, x * y
}
func main() {
sumx, sumy := sumAndProduct(2, 3)
fmt.Println(sumx)
fmt.Println(sumy)
}
上面的例子可以看出我们直接返回了两个参数,这里为了简写,省略了返回的参数名,但是建议在实际项目中,还是写上项目名,这样规范一些
3. 变参
golang支持函数传不定数量的参数,例子如下:
Go
package main
import "fmt"
func printargs(args ...int) {
for i, n := range args {
fmt.Printf("args %d is %d | ", i, n)
}
}
func main() {
printargs(1, 2, 3, 4, 5)
fmt.Println()
printargs(3, 5)
}
从代码中可以看出,传递数目不定的同类型参数使用...int表示参数类型,且在函数体中,变量args是一个int的切片,输出结果如下
4. 传值和传指针
和C、C++类似,go函数传参也有传值和传指针的区别。传值的时候只是传递这个值的拷贝到函数中,传递指针则是将值的地址传给函数,因此二者的区别就在于在函数中修改传递的参数值会不会影响到原值。例子如下:
Go
package main
import "fmt"
// 简单的一个函数,实现了参数+1的操作
func addValue(a int) int {
a = a + 1 // 我们改变了a的值
return a //返回一个新值
}
func addPointer(a *int) int {
*a = *a + 1 // 我们改变了a的值
return *a //返回一个新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := addValue(x) //调用add1(x)
fmt.Println("x1 = ", x1) // 应该输出"x1 = 4"
fmt.Println("x = ", x) // 应该输出"x = 3"
x2 := addPointer(&x) //调用add1(x)
fmt.Println("x2 = ", x2) // 应该输出"x2 = 4"
fmt.Println("x = ", x) // 应该输出"x = 4"
}
运行代码,输出结果如下:
从结果来看,我们传递地址给函数,函数中对地址指向的值进行了修改,导致了原值的同步修改。
指针传递往往有以下优点:
-
便于多个函数操作同一个对象
-
针对大的结构体,指针传递只需要8个字节,传值则需要花费更多的时间和CPU去进行拷贝
-
golang中channal,slice, map这三种类型的实现机制类似指针,可以直接传递(如果需要改变slice的长度,则需要传递指针)
5. defer
defer是golang里面的一个独有的设计,表示函数在退出的时候需要执行的语句。比如我们在函数中打开一个文件,但是什么时候关闭呢?可能有很多场景,比如打开文件失败,读取内容失败,以及正常退出,每个场景都需要去考虑是否需要关闭文件。defer就可以解决这一问题,我们无需考虑什么时候关闭文件,只需要在函数开头加上
defer file.close()
这样,函数在结束的时候就会自动执行file.close()这个语句了。
6. 函数作为值,类型
golang中函数也是一种变量,可以通过type来定义,它的类型就是所有拥有相同的参数,相同的返回值的一种类型
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (output1 outputType1 [, ...])
将函数作为类型的好处就是可以将这个类型的函数当作值来传递,例子如下:
Go
package main
import "fmt"
type functype func(int) bool // 声明了一个函数类型
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f functype) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main() {
slice := []int{1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd)
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven)
fmt.Println("Even elements of slice are: ", even)
}
以上代码中定义了一个函数类型,然后将这个类型的函数作为参数传递给另一个函数,这样的做法可以使得我们的代码变得更加灵活
7. Panic和Recover
go里面的异常和恢复机制就是通过panic和recover实现的。
panic函数用于触发一个运行时错误,并开始回溯函数调用栈,直至发生panic
的goroutine
中所有调用的函数返回,此时程序退出,或者遇到recover语句将panic
recover函数仅在defer语句中有效,在正常的执行过程中,调用recover
会返回nil
,并且没有其它任何效果。如果当前的goroutine
陷入panic
状态,调用recover
可以捕获到panic
的输入值,并且恢复正常的执行。
例子如下:
Go
package main
import "fmt"
func test() {
panic("some panic happend")
}
func throwsPanic() (b bool) {
defer func() {
if x := recover(); x != nil {
fmt.Println(x)
fmt.Println("recover from panic")
b = true
}
}()
test()
return
}
func main() {
b := throwsPanic()
if b {
fmt.Println("panic and recover")
}
}
运行结果为:
可以看到,我们触发了panic后,被defer中的recover捕获到了,进行了处理,最后恢复了正常的执行。
8. main和init函数
golang里面有两个保留的函数,main函数和init函数,作为内置的函数,这两个函数在定义的时候不能有任何的参数和返回值。
init函数表示包的初始化函数,在包加载的时候调用,用于包的初始化操作,为了便于维护,一般建议一个包最多定义一个init函数即可。
main函数表示程序的入口,只能在main包里面调用。
程序运行时一般先调用init函数,然后再调用main函数,因此我们可以在init函数中做一些初始化的操作。
Go
package main
import "fmt"
func init() {
fmt.Println("init function")
}
func main() {
fmt.Println("main function")
}
运行后我们会发现先执行的是init函数中的代码