文章目录
- 一、Go 语言函数
- 二、函数的声明
- 三、函数的调用
- 四、函数参数
- 1. 值传递和引用传递
- (1)值传递
- (2)引用传递
- 2. 不定参数传值
- 五、函数返回值
- 理解 Golang 的延迟调用(defer)
- 六、匿名函数
- 七、函数用法
- 1. 函数作为实参
- 2. 闭包
- 3. 方法
- 八、递归函数
- 九、内置函数
- 十、变量的作用域
- 1. 局部变量
- 2. 全局变量
- 3. 形式参数
- 4. 两个重要说明
- 十一、异常处理
- 参考链接
一、Go 语言函数
函数是基本的代码块,用于执行一个任务。
你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。
Go 语言最少有个 main() 函数。
golang函数特点:
支持:
- 无需声明原型。
- 支持不定 变参。
- 支持多返回值。
- 支持命名返回参数。
- 支持匿名函数和闭包。
- 函数也是一种类型,一个函数可以赋值给变量。
不支持:
- 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
- 不支持 重载 (overload)
- 不支持 默认参数 (default parameter)。
二、函数的声明
函数声明告诉了编译器函数的名称,参数,和返回类型。
函数定义格式如下:
func name( [parameter list] ) [return_types] {
函数体
}
解析:
- 函数声明包含一个函数名,参数列表, 返回值列表和函数体。
- func:函数由关键字 func 声明。左大括号依旧不能另起一行。
- name:函数名称。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。函数可以没有参数或接受多个参数。注意参数类型在变量名之后 。当两个或多个连续的参数是同一类型,则除了最后一个类型之外,其他都可以省略。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型,如(string, string)返回两个字符串。有些功能不需要返回值,如果函数没有返回值,则返回列表可以省略。也就是说,函数可以返回任意数量的返回值。有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
函数可以返回多个值,多返回值必须用括号。如:
func test(x, y int, s string) (int, string) {
// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
n := x + y
return n, fmt.Sprintf(s, n)
}
- 函数体:代码集合(一般实现一个功能)。函数从第一条语句开始执行,直到执行 return 语句或者执行函数的最后一条语句。
实例:max() 函数,传入两个整型参数 num1 和 num2,返回这两个参数的最大值。
/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int
if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}
三、函数的调用
函数的声明定义了函数的功能和使用方式,想要使用函数真正执行任务需要调用该函数。
调用函数,向函数传递参数,并返回值,例如:
package main
import "fmt"
func main() {
var a int = 100
var b int = 200
var ret int
/* 调用函数 */
ret = max(a, b)
fmt.Printf( "最大值是 : %d\n", ret )
}
func max(num1, num2 int) int {
var result int
if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}
四、函数参数
1. 值传递和引用传递
函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
(1)值传递
传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
以下定义了 swap() 函数:
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int
temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/
return temp;
}
接下来,让我们使用值传递来调用 swap() 函数:
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
fmt.Printf("交换前 a 的值为 : %d\n", a)
fmt.Printf("交换前 b 的值为 : %d\n", b)
/* 通过调用函数来交换值 */
swap(a, b)
fmt.Printf("交换后 a 的值 : %d\n", a)
fmt.Printf("交换后 b 的值 : %d\n", b)
}
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int
temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/
return temp
}
输出结果:
交换前 a 的值为 : 100
交换前 b 的值为 : 200
交换后 a 的值 : 100
交换后 b 的值 : 200
可见交换前后a,b的值没变。
所以值传递不会改变所传入实参的值。只是复制一份值用于函数体执行而已。
(2)引用传递
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
引用传递将指针参数传递到函数内,以下是交换函数 swap() 使用了引用传递:
/* 定义交换值函数*/
func swap(x *int, y *int) {
var temp int
temp = *x /* 保持 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
以下我们通过使用引用传递来调用 swap() 函数:
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
fmt.Printf("交换前,a 的值 : %d\n", a)
fmt.Printf("交换前,b 的值 : %d\n", b)
/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Printf("交换后,a 的值 : %d\n", a)
fmt.Printf("交换后,b 的值 : %d\n", b)
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
输出结果:
交换前,a 的值 : 100
交换前,b 的值 : 200
交换后,a 的值 : 200
交换后,b 的值 : 100
注意:
- 无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
- map、slice、chan、指针、interface默认以引用的方式传递。
2. 不定参数传值
不定参数传值就是函数的参数数量不固定,后面的类型是固定的。(可变参数)
Golang 可变参数本质上是 slice。该 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) 来判断传递参数的个数。
实例 1:逐个赋值
package main
import (
"fmt"
)
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s, x)
}
func main() {
println(test("sum: %d", 1, 2, 3))
}
输出结果:
sum: 6
实例 2:使用切片赋值
在参数赋值时可以不用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…
”即可。
使用 slice 对象做变参时,必须展开。(slice…)
package main
import (
"fmt"
)
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s, x)
}
func main() {
s := []int{1, 2, 3}
res := test("sum: %d", s...) // slice... 展开slice
println(res)
}
输出结果:
sum: 6
多一嘴:
任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。
用 interface{} 传递任意类型数据是Go语言的惯例用法,而且 interface{} 是类型安全的。
func myfunc(args ...interface{}) {
}
五、函数返回值
- 返回值的忽略
_
标识符,用来忽略函数的某个返回值。
Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 “_” 忽略。 - 多返回值可直接作为其他函数调用实参。
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
- 命名返回值
Go 函数的返回值可以被命名,就像在函数体开头声明变量。
返回值的名称应当具有一定的意义,可以作为文档使用。
命名返回参数可看做与形参类似的局部变量,最后由 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 // 必须显式返回。
}
}
- 没有参数的 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
- 命名返回参数允许 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
理解 Golang 的延迟调用(defer)
defer特性:
- 关键字 defer 用于注册延迟调用。
- 这些调用直到 return 跳转前才被执。因此,可以用来做资源清理。
- 多个defer语句,按先进后出的方式执行。
- defer语句中的变量,在defer声明时就决定了。
defer用途:
- 关闭文件句柄
- 锁资源释放
- 数据库连接释放
Go 语言 中的 defer 语句用于延迟函数的调用,每次 defer 都会把一个函数压入 栈 中,函数返回前再把延迟的函数取出并执行。Golang 中的 defer 可以帮助我们处理容易忽略的问题,如资源释放、连接关闭等。
go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。
Golang 官方博客里总结了 defer 的行为规则,只有三条,分别为:
- 延迟函数的参数在 defer 语句出现时就已经确定下来了。
实例:
package main
import "fmt"
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
func main() {
a()
}
输出结果:
0
defer 语句中的 fmt.Println() 参数 i 值在 defer 出现时就已经确定下来,实际上是拷贝了一份。后面对变量 i 的修改不会影响 fmt.Println() 函数的执行,仍然打印 “0”。
注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer 后面的语句对变量的修改可能会影响延迟函数。
- 延迟函数执行按 后进先出 顺序执行,即先出现的 defer 最后执行。
这个规则很好理解,定义 defer 类似于入栈操作,执行 defer 类似于出栈操作。
设计 defer 的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请 A 资源,再跟据 A 资源申请 B 资源,根据 B 资源申请 C 资源,即申请顺序是:A–>B–>C,释放时往往又要反向进行。这就是把 defer 设计成 FIFO 的原因。
每申请到一个用完需要释放的资源时,立即定义一个 defer 来释放资源是个很好的习惯。
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。 - 延迟函数可能操作主函数的具名返回值(命名返回值)
定义 defer 的函数,即主函数可能有返回值,返回值有没有名字没有关系,defer 所作用的函数,即延迟函数可能会影响到返回值。若要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回的就足够了。
3.1 函数返回过程(命名返回值的情况)
有一个事实必须要了解,关键字 return 不是一个原子操作,实际上 return 只代理汇编指令 ret,即将跳转程序执行。比如语句 return i,实际上分两步进行,即将 i 值存入栈中作为返回值,然后执行跳转,而 defer 的执行时机正是跳转前,所以说 defer 执行时还是有机会操作返回值的。
举个实际的例子进行说明这个过程:
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
该函数的 return 语句可以拆分成下面两行:
result = i
return
而延迟函数的执行正是在 return 之前,即加入 defer 后的执行过程如下:
result = i
result++
return
所以上面函数实际返回 i++ 值。
3.2 主函数拥有匿名返回值,返回字面值
一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回 “1”、“2”、“Hello” 这样的值,这种情况下 defer 语句是无法操作返回值的。一个返回字面值的函数,如下所示:
func foo() int {
var i int
defer func() {
i++
}()
return 1
}
上面的 return 语句,直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。
3.3 主函数拥有匿名返回值,返回变量
一个主函数拥有一个匿名返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。一个返回本地变量的函数,如下所示:
func foo() int {
var i int
defer func() {
i++
}()
return i
}
上面的函数,返回一个局部变量,同时 defer 函数也会操作这个局部变量。对于匿名返回值来说,可以假定系统给分配了一个命名变量来存储返回值,假定返回值变量为 “anony”,上面的返回语句可以拆分成以下过程:
anony = i
i++
return
由于 i 是整型,会将值拷贝给 anony,所以 defer 语句中修改 i 值,对函数返回值不造成影响。
3.4 主函数拥有具名返回值
主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,可能会改变返回结果。一个影响函返回值的例子:
func foo() (ret int) {
defer func() {
ret++
}()
return 0
}
上面的函数拆解出来,如下所示:
ret = 0
ret++
return
函数真正返回前,在 defer 中对返回值做了 +1 操作,所以函数最终返回 1。
六、匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式。
在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
package main
import (
"fmt"
"math"
)
func main() {
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
}
输出结果:
2
上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作。
Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。
package main
func main() {
// --- function variable ---
fn := func() { println("Hello, World!") }
fn()
// --- function collection ---
fns := [](func(x int) int){
func(x int) int { return x + 1 },
func(x int) int { return x + 2 },
}
println(fns[0](100))
// --- function as field ---
d := struct {
fn func() string
}{
fn: func() string { return "Hello, World!" },
}
println(d.fn())
// --- channel of function ---
fc := make(chan func() string, 2)
fc <- func() string { return "Hello, World!" }
println((<-fc)())
}
输出结果:
Hello, World!
101
Hello, World!
Hello, World!
七、函数用法
1. 函数作为实参
Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。
函数是第一类对象,可作为参数传递。
以下实例中我们在定义的函数中初始化一个变量,该函数仅仅是为了使用内置函数 math.sqrt(),实例为:
package main
import (
"fmt"
"math"
)
func main(){
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
}
输出结果:
3
另一个例子:
package main
import "fmt"
// 声明一个函数类型
type cb func(int) int
func main() {
testCallBack(1, callBack)
testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}
func testCallBack(x int, f cb) {
f(x)
}
func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}
输出结果:
我是回调,x:1
我是回调,x:2
建议将复杂签名定义为函数类型,以便于阅读。
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
2. 闭包
理解闭包
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
以下实例中,我们创建了函数 getSequence() ,它没有参数,返回值是一个匿名函数。该函数的目的是在闭包中递增 i 变量,代码如下:
package main
import "fmt"
//getSequence()是函数名,没有参数。函数func() int是返回值。
//func()是一个匿名函数,它也没有参数,它返回一个int值
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}
输出结果:
1
2
3
1
2
函数 func() int 嵌套在函数 getSequence()内部,函数 getSequence() 返回函数 func() int。这样在执行完nextNumber := getSequence()
后,变量 nextNumber 实际上是指向了函数 func() int ,再执行函数 nextNumber() 后就会实现i
的自增,第一次为1,第二次为2,第三次为3,以此类推。 其实,这段代码就创建了一个闭包。因为函数 getSequence() 外的变量 nextNumber 引用了函数 getSequence() 内的函数 func() int ,就是说:当函数a()的内部函数b()被函数a()外的一个变量引用的时候,就创建了一个闭包。
在上面的例子中,由于闭包的存在使得函数 getSequence() 返回后, getSequence() 中的i
始终存在,这样每次执行 nextNumber ,i
都是自加1后的值。 从上面可以看出闭包的作用就是在 getSequence() 执行完并返回后,闭包使得垃圾回收机制GC不会收回 getSequence() 所占用的资源,因为 getSequence() 的内部函数 func() int 的执行需要依赖 getSequence() 中的变量i
。
在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。
下面来说闭包的另一要素:引用环境。nextNumber 跟 nextNumber1 引用的是不同的环境,在调用i += 1
时修改的不是同一个i,因此两次都从1开始输出。函数 getSequence() 每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。
下面来想象另一种情况,如果内嵌函数b()没有被外部变量引用,情况就完全不同了。因为a()执行完后,b()没有被返回给a()的外界,只是被a()所引用,而此时a()也只会被b()引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用a()页面并没有信息输出:
package main
import (
"fmt"
)
func a() func() int {
i := 0
b := func() int {
i++
fmt.Println(i)
return i
}
return b
}
func main() {
c := a()
c()
c()
c()
a() //不会输出i
}
输出结果:
1
2
3
闭包:引用传递
闭包复制的是原对象指针,这就很容易解释延迟引用现象。
package main
import "fmt"
func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)
return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}
func main() {
f := test()
f()
}
输出结果:
x (0xc42007c008) = 100
x (0xc42007c008) = 100
在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调匿名函数时,只需以某个寄存器传递该对象即可。
FuncVal { func_address, closure_var_pointer ... }
外部引用函数的参数
package main
import "fmt"
// 外部引用函数参数局部变量
func add(base int) func(int) int {
return func(i int) int {
base += i
return base
}
}
func main() {
tmp1 := add(10)
fmt.Println(tmp1(1), tmp1(2))
// 此时tmp1和tmp2不是一个实体了
tmp2 := add(100)
fmt.Println(tmp2(1), tmp2(2))
}
输出结果:
11 13
101 103
返回2个闭包
package main
import "fmt"
// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {
// 定义2个函数,并返回
// 相加
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 := test01(10)
// base一直是没有消
fmt.Println(f1(1), f2(2))
// 此时base是9
fmt.Println(f1(3), f2(4))
}
输出结果:
11 9
12 8
3. 方法
Go 语言中同时有函数和方法。
一个方法就是一个包含了接受者的函数,接受者可以是任何命名类型(接口类型除外)或者结构体类型的一个值或者是一个指针。
给定类型的所有方法属于该类型的方法集。
方法的声明语法:
//方法function_name()在(variable_name variable_data_type)这个变量上做工作
//(variable_name variable_data_type)是接受者
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
实例 1
下面定义一个结构体类型和该类型的一个方法:
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
输出结果:
圆的面积 = 314
关于值和指针,如果想在方法中改变结构体类型的属性,需要对方法传递指针,体会如下对结构体类型改变的方法 changRadis() 和普通的函数 change() 中的指针操作:
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c Circle
fmt.Println(c.radius)
c.radius = 10.00
fmt.Println(c.getArea())
c.changeRadius(20)
fmt.Println(c.radius)
change(&c, 30)
fmt.Println(c.radius)
}
func (c Circle) getArea() float64 {
return c.radius * c.radius
}
// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64) {
c.radius = radius
}
// 以下操作将不生效
//func (c Circle) changeRadius(radius float64) {
// c.radius = radius
//}
// 引用类型要想改变值需要传指针
func change(c *Circle, radius float64) {
c.radius = radius
}
输出结果:
0
100
20
30
说明:
getArea() 和 changeRadius() 是方法,因为它们是定义在某个接收对象上的。
调用方法的语法是c.方法()
,针对某个对象 c 调用定义在其上的方法。
注意和函数 change() 的直接调用方法不同哦。
实例2
实际上,除了结构体类型之外,可以为任意类型(接口类型除外)添加方法。
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法(接口类型除外)。
举个例子,我们基于内置的 int 类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
package main
import (
"fmt"
)
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
输出结果:
Hello, 我是一个int。
100 main.MyInt
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
关于Go语言方法的深入理解请参考我的另一篇文章:【Go】Go语言中的方法
八、递归函数
递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。
构成递归需具备的条件:
1.子问题须与原始问题为同样的事,且更为简单。
2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
语法格式如下:
func recursion() {
recursion() /* 函数调用自身 */
}
func main() {
recursion()
}
重点注意!!!!!!无限循环警告!!!!!!:
Go 语言支持递归。但我们在使用递归时,开发者需要设置 退出条件 ,否则递归将陷入无限循环中。
递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。
实例1:数字阶乘
一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!
。
package main
import "fmt"
func factorial(i int) int {
if i <= 1 {
return 1
}
return i * factorial(i-1)
}
func main() {
var i int = 7
fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}
输出结果:
Factorial of 7 is 5040
实例2:斐波那契数列(Fibonacci)
这个数列从第3项开始,每一项都等于前两项之和。
package main
import "fmt"
func fibonaci(i int) int {
if i == 0 {
return 0
}
if i == 1 {
return 1
}
return fibonaci(i-1) + fibonaci(i-2)
}
func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d\n", fibonaci(i))
}
}
输出结果:
0
1
1
2
3
5
8
13
21
34
九、内置函数
Go 语言标准库提供了多种可动用的内置函数。
例如,len() 函数可以接受不同类型参数并返回其长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。
十、变量的作用域
变量的作用域由 变量声明的地方 和 函数 的 相对位置 决定。
作用域为已声明的标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。
Go 语言中变量可以在三个地方声明:
(1)函数内定义的变量称为局部变量
(2)函数外定义的变量称为全局变量
(3)函数定义中的变量称为形式参数
1. 局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
以下实例中 main() 函数使用了局部变量 a, b, c:
package main
import "fmt"
func main() {
/* 声明局部变量 */
var a, b, c int
/* 初始化参数 */
a = 10
b = 20
c = a + b
fmt.Printf ("结果: a = %d, b = %d and c = %d\n", a, b, c)
}
输出结果:
结果: a = 10, b = 20 and c = 30
2. 全局变量
在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
全局变量可以在任何函数中使用,以下实例演示了如何使用全局变量:
package main
import "fmt"
/* 声明全局变量 */
var g int
func main() {
/* 声明局部变量 */
var a, b int
/* 初始化参数 */
a = 10
b = 20
g = a + b
fmt.Printf("结果: a = %d, b = %d and g = %d\n", a, b, g)
}
输出结果:
结果: a = 10, b = 20 and g = 30
一个说明:
Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。实例如下:
package main
import "fmt"
/* 声明全局变量 */
var g int = 20
func main() {
/* 声明局部变量 */
var g int = 10
fmt.Printf ("结果: g = %d\n", g)
}
输出结果:
结果: g = 10
3. 形式参数
形式参数会作为函数的局部变量来使用。实例如下:
package main
import "fmt"
/* 声明全局变量 */
var a int = 20
func main() {
/* main 函数中声明局部变量 */
var a int = 10
var b int = 20
var c int = 0
fmt.Printf("main()函数中 a = %d\n", a)
c = sum(a, b)
fmt.Printf("main()函数中 c = %d\n", c)
}
/* 函数定义-两数相加 */
func sum(a, b int) int {
fmt.Printf("sum() 函数中 a = %d\n", a)
fmt.Printf("sum() 函数中 b = %d\n", b)
return a + b
}
输出结果:
main()函数中 a = 10
sum() 函数中 a = 10
sum() 函数中 b = 20
main()函数中 c = 30
4. 两个重要说明
(1)总结
变量可见性:
1)声明在函数内部,是函数的本地值,类似 private
2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似 protect
3)声明在函数外部且首字母大写是所有包可见的全局值,类似 public
(2)默认初始化值
不同类型的局部和全局变量初始化 默认值(就是不初始化时,系统自动给的值)为:
十一、异常处理
Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
(结构化异常指的是C/C++程序语言中,程序控制结构try-except
与try-finally
语句用于处理异常事件。)
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic 介绍:
- 内置函数
- 假如函数 F 中书写了 panic 语句,会终止其后要执行的代码,在 panic 所在函数 F 内如果存在要执行的 defer 函数列表,按照 defer 的逆序执行
- 返回函数 F 的调用者 G ,在 G 中,调用函数 F 语句之后的代码不会执行,假如函数 G 中存在要执行的 defer 函数列表,按照 defer 的逆序执行
- 直到 goroutine 整个退出,并报告错误
recover 介绍:
- 内置函数
- 用来控制一个 goroutine 的 panicking 行为,捕获 panic ,从而影响应用的行为
- 一般的调用建议
a. 在 defer 函数中,通过 recever 来终止一个 goroutine 的 panicking 过程,从而恢复正常代码的执行
b. 可以获取通过 panic 传递的 error
注意:
- 利用 recover 处理 panic 指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当 panic 时,recover无法捕获到 panic ,无法防止 panic 扩散。
- recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
- 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
实例1:panic 和 recover 函数的配合使用
package main
func main() {
test()
}
func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()
panic("panic error!")
}
输出结果:
panic error!
说明:
由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。
func panic(v interface{})
func recover() interface{}
实例2:向已关闭的通道发送数据会引发 panic
package main
import (
"fmt"
)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var ch chan int = make(chan int, 10)
close(ch)
ch <- 1
}
输出结果:
send on closed channel
实例3:延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
package main
import "fmt"
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
输出结果:
defer panic
实例4:捕获函数 recover 只有在 defer 延迟调用内 直接调用 才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
package main
import "fmt"
func test() {
defer func() {
fmt.Println(recover()) //有效
}()
defer recover() //无效!
defer fmt.Println(recover()) //无效!
defer func() {
func() {
println("defer inner")
recover() //没有在defer函数内直接调用,无效!
}()
}()
panic("test panic")
}
func main() {
test()
}
输出结果:
defer inner
<nil>
test panic
实例5:使用延迟匿名函数或下面这样都是有效的。
package main
import (
"fmt"
)
func except() {
fmt.Println(recover())
}
func test() {
defer except()
panic("test panic")
}
func main() {
test()
}
输出结果:
test panic
实例6:如果需要保护代码段,可将代码块重构成匿名函数,如此可确保后续代码可以被执。
package main
import "fmt"
func test(x, y int) {
var z int
func() {
defer func() {
if recover() != nil {
z = 0
}
}()
panic("test panic")
z = x / y
return
}()
fmt.Printf("x / y = %d\n", z) //panic + recover结束了匿名函数内部的执行,跳出了匿名函数。但这行代码仍然可以被执行。
}
func main() {
test(2, 1)
}
输出结果:
x / y = 0
另外:
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态:
type error interface {
Error() string
}
如何区别使用 panic 和 error 两种方式 ? 惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
参考链接