作者简介:
高科,先后在 IBM PlatformComputing从事网格计算,淘米网,网易从事游戏服务器开发,拥有丰富的C++,go等语言开发经验,mysql,mongo,redis等数据库,设计模式和网络库开发经验,对战棋类,回合制,moba类页游,手游有丰富的架构设计和开发经验。 (谢谢你的关注)
--------------------------------------------------------------------------------------------------------------------------------
函数定义
Go语言函数基本组成:关键字func、函数名、参数列表、返回值、函数体和返回语句。语法如下:
func 函数名(参数列表) (返回值列表) {
// 函数体
return
}
函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。
有点简单,很多做C,C++开发的都知道声明函数原型是一件有点冗余的事情,而转到go之后,你会发现go函数用起来便轻松简洁很多,至少不需要你声明函数原型。那么我们接下来一起先看看go函数都有哪些特点:
• 无需声明原型。
• 支持不定 变参。
• 支持多返回值。
• 支持命名返回参数。
• 支持匿名函数和闭包。
• 函数也是一种类型,一个函数可以赋值给变量。• 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
• 不支持 重载 (overload)
• 不支持 默认参数 (default parameter)。
"func" 为定义函数的关键字,函数这一行的最末尾需要一个左大括号,注意左大括号依旧不能另起一行。
这里我写一个简单的两整数相加的函数
package main
import (
"fmt"
)
func Sum(a int, b int) int {
return a + b
}
func main() {
fmt.Println("sum(1,2):", Sum(1, 2))
}
运行下结果
sum(1,2): 3
这里实际上如果类型相同的相邻参数,我们可以吧参数类型合并一下,所以我们可以这样写Sum函数:
func Sum(a, b int) int {
return a + b
}
所以接下来我们接下来要说下函数的参数
函数参数
函数定义时有参数,该参数变量可称为函数的形参。形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参。
值传递和引用传递
函数可以通过两种方式来传递参数:
值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
func swap(x, y int) int {
... ...
}
引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
package main
import (
"fmt"
)
/* 定义相互交换值的函数 */
func swap(x, y *int) {
var temp int
temp = *x /* 保存 x 的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y*/
}
func main() {
var a, b int = 1, 2
/*
调用 swap() 函数
&a 指向 a 指针,a 变量的地址
&b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Println(a, b)
}
输出结果:
2 1
再来一个map的传参例子:
package main
import (
"fmt"
)
func Mod(a []int) {
for i, v := range a {
a[i] = 100 + v
}
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Mod(numbers)
fmt.Println(numbers)
}
输出结果:
[101 102 103 104 105 106 107 108 109 110]
可见,虽然我们的Mod函数传参过来并非指针,表面上好像是值传递,从结果上来看似乎又间接的修改了slice的值,因此slice 实际上是以引用的方式传递。
总结
在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
注意2:map、slice、chan、指针、interface默认以引用的方式传递。
不定参数传值
就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。
在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。
func myfunc(args ...int) { //0个或多个参数
}
func add(a int, args…int) int { //1个或多个参数
}
func add(a int, b int, args…int) int { //2个或多个参数
}
注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.
其实说到这里你可能会想到我们最开始写helloworld的时候,用到的fmt.Println函数。接下来我用一个简单的例子来调用不定参数的函数:
package main
import (
"fmt"
)
func Sum(a int, args ...int) (sum int) {
sum = a
for _, v := range args {
sum += v
}
return sum
}
func main() {
fmt.Println("sum(1-4):", Sum(1, 2, 3, 4))
}
输出:sum(1-4):
由于这种不定参的函数可以接受某种类型的切片 slice 为参数,因此我们的代码还可以这样调用起来:
package main
import (
"fmt"
)
func Sum(args ...int) (sum int) {
sum = 0
for _, v := range args {
sum += v
}
return sum
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("sum(1-4):", Sum(numbers ...)) // slice时使用
}
在调用的时候我们需要给slice参数后边加上...即可。
任意类型的不定参数
就是函数的参数和每个参数的类型都不是固定的。
用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。
func myfunc(args ...interface{}) {
}
函数重载
函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参或者不同的返回值,在 Go 语言里面函数重载是不被允许的。
当你这样写同名函数的时候如下:
func Mod(a []int) {
for i, v := range a {
a[i] = 100 + v
}
}
func Mod(a int) {
a = a+1
}
编译报错,信息如下:
Mod redeclared in this block
other declaration of Mod
函数签名
函数也可以作为函数类型被使用。函数类型也就是函数签名,函数类型表示具有相同参数和结果类型的所有函数的集合。函数类型的未初始化变量的值为nil。就像下面:
type funcType func (int, int) int
上面通过type关键字,定义了一个新类型,函数类型 funcType 。
函数签名由函数参数、返回值以及它们的类型组成,如果两个函数的参数列表和返回值列表的变量类型能一一对应,那么这两个函数就有相同的签名,下面testa与testb具有相同的函数签名。
func testa (a, b int, z float32) bool
func testb (a, b int, z float32) (bool)
函数调用传入的参数必须按照参数声明的顺序。而且Go语言没有默认参数值的说法。
那么我们如何来显示的调用对应的函数呢?我们可以这样来:
package main
import (
"fmt"
)
func testa(a int, b int, z float32) bool {
fmt.Println("testa")
return a+b > int(z)
}
func testb(a, b int, z float32) bool {
fmt.Println("testb")
return a+b > int(z)
}
type FuncType func(int, int, float32) bool
func main() {
var z float32
z = 10.345
FuncType(testa)(1, 2, z)
FuncType(testb)(1, 2, z)
}
函数也可以在表达式中赋值给变量,这样作为表达式中右值出现,我们称之为函数值字面量(function literal),函数值字面量是一种表达式,它的值被称为匿名函数,就像下面一样:
f := func() int { return 7 }
下面代码对以上2种情况都做了定义和调用:
package main
import (
"fmt"
"time"
)
type funcType func(time.Time) // 定义函数类型funcType
func main() {
f := func(t time.Time) time.Time { return t } // 方式一:直接赋值给变量
fmt.Println(f(time.Now()))
var timer funcType = CurrentTime // 方式二:定义函数类型funcType变量timer
timer(time.Now())
funcType(CurrentTime)(time.Now()) // 先把CurrentTime函数转为funcType类型,然后传入参数调用
// 这种处理方式在Go 中比较常见
}
func CurrentTime(start time.Time) {
fmt.Println(start)
}
函数返回值
"_"
标识符,用来忽略函数的某个返回值
Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。
返回值的名称应当具有一定的意义,可以作为文档使用。
没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。
直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。
package main
import (
"fmt"
)
func add(a, b int) (c int) {
c = a + b
return
}
func calc(a, b int) (sum int, avg int) {
sum = a + b
avg = (a + b) / 2
return
}
func main() {
var a, b int = 1, 2
c := add(a, b)
sum, avg := calc(a, b)
fmt.Println(a, b, c, sum, avg)
}
输出结果:
1 2 3 3 1
Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_"
忽略。
package main
func test() (int, int) {
return 1, 2
}
func main() {
// s := make([]int, 2)
// s = test() // Error: multiple-value test() in single-value context
x, _ := test()
println(x)
}
输出结果:
1
多返回值可直接作为其他函数调用实参。
package main
func test() (int, int) {
return 1, 2
}
func add(x, y int) int {
return x + y
}
func sum(n ...int) int {
var x int
for _, i := range n {
x += i
}
return x
}
func main() {
println(add(test()))
println(sum(test()))
}
输出结果:
3
3
命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
package main
func add(x, y int) (z int) {
z = x + y
return
}
func main() {
println(add(1, 2))
}
输出结果: 3
命名返回参数可被同名局部变量遮蔽,此时需要显式返回。
func add(x, y int) (z int) {
{ // 不能在一个级别,引发 "z redeclared in this block" 错误。
var z = x + y
// return // Error: z is shadowed during return
return z // 必须显式返回。
}
}
命名返回参数允许 defer 延迟调用通过闭包读取和修改。
package main
func add(x, y int) (z int) {
defer func() {
z += 100
}()
z = x + y
return
}
func main() {
println(add(1, 2))
}
输出结果: 103
显式 return 返回前,会先修改命名返回参数。
package main
func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()
z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}
func main() {
println(add(1, 2)) // 输出: 203
}
输出结果:
203
203
函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。
package main
import "fmt"
func test(fn func() int) int {
return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)
println(s1, s2)
}
输出结果: 100 10, 20
有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
panic和recover函数简要说明
使用场景
Go 语言拥有一些内置函数,内置函数是预先声明的,它们像任何其他函数一样被调用,内置函数没有标准的类型,因此它们只能出现在调用表达式中,它们不能用作函数值。这里我们简要的说明下常用的panic和recover函数(细节我们将会在其他文章里专门作为一个专题来分析):
内置函数 | 说明 |
---|---|
panic | 用来表示非常严重的不可恢复的异常错误 |
recover | 用于从 panic 或 错误场景中恢复 |
func panic(interface{})
func recover() interface{}
panic和recover两个内置函数,协助报告和处理运行时异常和程序定义的错误。
在执行函数时,显式调用panic或者运行时发生panic都会终止函数的执行。然后,由函数延迟(defer)的任何函数都照常执行。 依此类推,直到执行goroutine中的顶级函数延迟。 此时,程序终止并报告错误条件,包括panic参数的值。
panic(42)
panic("unreachable")
panic(Error("cannot parse"))
因此当我们需要终止一个异常,我们就可以直接使用panic,比如下面的代码:
if false == LoadCfg() {
panic("加载配置文件失败")
}
当进入到if条件判断里边之后,panic会将此时的堆栈信息打印输出:
这段代码里我们会看到panic的原因有输出,堆栈信息终止在InitModels函数体内,GameConfig.go的第63行,这样你能很方便的定位到问题。
recover函数用于在发生panic异常时恢复程序的控制流。它只能在defer函数中调用,并且可以捕获并处理发生的panic异常。
当程序发生panic异常时,它会中断当前的控制流程,并且开始执行所有在当前函数中定义的defer函数。在defer函数中调用recover函数,可以捕获到panic异常,并且程序可以继续正常执行。
recover函数的签名如下:
func recover() interface{}
它没有任何参数,返回一个interface{}类型的值。
当调用recover函数时,它会返回panic函数传递过来的值,如果没有发生panic异常或者recover函数不是在defer函数中调用,那么它会返回nil。
通常情况下,我们会在defer函数中使用recover函数来捕获和处理panic异常。示例代码如下:
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("Something went wrong!")
}
在上面的示例中,我们使用defer函数在程序发生panic异常时调用recover函数。在defer函数中,我们通过检查recover函数的返回值是否为nil来判断是否发生了panic异常,并进行相应的处理。
需要注意的是,recover函数只能在defer函数中调用,否则它不会起作用。另外,一旦recover函数被调用并且成功恢复了程序的控制流,后续的defer函数中的代码将不会执行。
和try-catch的区别
实际上有很多人在练习刚才的例子的时候很容易和try catch联系起来(如果你有其他语言开发基础),作为初学者的时候,我也会这样联想。的确,它与其他语言中的 try-catch 机制相似,recover
充当了异常处理器的角色,可以在出现异常的函数中使用 defer
语句来捕获和处理 panic 异常。
然而,需要注意的是,在 Go 语言中的 panic-recover 机制与传统的 try-catch 不完全一样。在 Go 中,panic 是一种特殊的异常情况,它表示发生了不可恢复的错误。当 panic 异常发生时,程序会终止当前的执行流程,并从调用栈中逐层寻找 defer
函数,直到找到 recover
函数为止。
与 try-catch 不同的是,Go 中的 recover
函数只能在 defer
函数中使用,并且只能在同一个 goroutine 中的调用中生效。这意味着 recover
并不能用来处理跨协程的异常。另外,由于 recover
只能在 defer
中使用,它也不能用来捕获和处理其他非 panic 的异常。
总结来说,Go 语言中的 recover
机制可以起到类似于 try-catch 的异常处理作用,但有一些细节上的差异。在使用 recover
时需要注意其在 defer
函数中的限制和局限性。