【Go】Go 语言函数
文章目录
- 一、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 intif (num1 > num2) {result = num1} else {result = num2}return result
}
三、函数的调用
函数的声明定义了函数的功能和使用方式,想要使用函数真正执行任务需要调用该函数。
调用函数,向函数传递参数,并返回值,例如:
package mainimport "fmt"func main() {var a int = 100var b int = 200var ret int/* 调用函数 */ret = max(a, b)fmt.Printf( "最大值是 : %d\n", ret )
}func max(num1, num2 int) int {var result intif (num1 > num2) {result = num1} else {result = num2}return result
}
四、函数参数
1. 值传递和引用传递
函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
(1)值传递
传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
以下定义了 swap() 函数:
/* 定义相互交换值的函数 */
func swap(x, y int) int {var temp inttemp = x /* 保存 x 的值 */x = y /* 将 y 值赋给 x */y = temp /* 将 temp 值赋给 y*/return temp;
}
接下来,让我们使用值传递来调用 swap() 函数:
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 100var b int = 200fmt.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 inttemp = 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 inttemp = *x /* 保持 x 地址上的值 */*x = *y /* 将 y 值赋给 x */*y = temp /* 将 temp 值赋给 y */
}
以下我们通过使用引用传递来调用 swap() 函数:
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 100var b int = 200fmt.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 inttemp = *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 mainimport ("fmt"
)func test(s string, n ...int) string {var x intfor _, 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 mainimport ("fmt"
)func test(s string, n ...int) string {var x intfor _, i := range n {x += i}return fmt.Sprintf(s, x)
}func main() {s := []int{1, 2, 3}res := test("sum: %d", s...) // slice... 展开sliceprintln(res)
}
输出结果:
sum: 6
多一嘴:
任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。
用 interface{} 传递任意类型数据是Go语言的惯例用法,而且 interface{} 是类型安全的。
func myfunc(args ...interface{}) {}
五、函数返回值
返回值的忽略
_
标识符,用来忽略函数的某个返回值。
Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 “_” 忽略。多返回值可直接作为其他函数调用实参。
package mainfunc test() (int, int) {return 1, 2 }func add(x, y int) int {return x + y }func sum(n ...int) int {var x intfor _, i := range n {x += i}return x }func main() {println(add(test()))println(sum(test())) }
输出结果:
3 3
命名返回值
Go 函数的返回值可以被命名,就像在函数体开头声明变量。
返回值的名称应当具有一定的意义,可以作为文档使用。
命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。package mainfunc add(x, y int) (z int) {z = x + yreturn }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 returnreturn z // 必须显式返回。} }
没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。
直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。package mainimport ("fmt" )func add(a, b int) (c int) {c = a + breturn }func calc(a, b int) (sum int, avg int) {sum = a + bavg = (a + b) / 2return }func main() {var a, b int = 1, 2c := add(a, b)sum, avg := calc(a, b)fmt.Println(a, b, c, sum, avg) }
输出结果:
1 2 3 3 1
命名返回参数允许 defer 延迟调用通过闭包读取和修改。
package mainfunc add(x, y int) (z int) {defer func() {z += 100}()z = x + yreturn }func main() {println(add(1, 2)) }
输出结果:
103
显式 return 返回前,会先修改命名返回参数。
package mainfunc add(x, y int) (z int) {defer func() {println(z) // 输出: 203}()z = x + yreturn 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 mainimport "fmt"func a() {i := 0defer 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 := 1defer 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 intdefer func() {i++}() return 1 }
上面的 return 语句,直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。
3.3 主函数拥有匿名返回值,返回变量
一个主函数拥有一个匿名返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。一个返回本地变量的函数,如下所示:
func foo() int { var i intdefer 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 mainimport ("fmt""math"
)func main() {getSqrt := func(a float64) float64 {return math.Sqrt(a)}fmt.Println(getSqrt(4))
}
输出结果:
2
上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作。
Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。
package mainfunc 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 mainimport ("fmt""math"
)func main(){/* 声明函数变量 */getSquareRoot := func(x float64) float64 {return math.Sqrt(x)}/* 使用函数 */fmt.Println(getSquareRoot(9))}
输出结果:
3
另一个例子:
package mainimport "fmt"// 声明一个函数类型
type cb func(int) intfunc 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 mainimport "fmt"func test(fn func() int) int {return fn()
}// 定义函数类型。
type FormatFunc func(s string, x, y int) stringfunc 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 mainimport "fmt"//getSequence()是函数名,没有参数。函数func() int是返回值。
//func()是一个匿名函数,它也没有参数,它返回一个int值
func getSequence() func() int {i := 0return func() int {i += 1return 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 mainimport ("fmt"
)func a() func() int {i := 0b := func() int {i++fmt.Println(i)return i}return b
}func main() {c := a()c()c()c()a() //不会输出i
}
输出结果:
1
2
3
闭包:引用传递
闭包复制的是原对象指针,这就很容易解释延迟引用现象。
package mainimport "fmt"func test() func() {x := 100fmt.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 mainimport "fmt"// 外部引用函数参数局部变量
func add(base int) func(int) int {return func(i int) int {base += ireturn 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 mainimport "fmt"// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {// 定义2个函数,并返回// 相加add := func(i int) int {base += ireturn base}// 相减sub := func(i int) int {base -= ireturn base}// 返回return add, sub
}func main() {f1, f2 := test01(10)// base一直是没有消fmt.Println(f1(1), f2(2))// 此时base是9fmt.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 mainimport ("fmt"
)/* 定义结构体 */
type Circle struct {radius float64
}func main() {var c1 Circlec1.radius = 10.00fmt.Println("圆的面积 = ", c1.getArea())
}//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {//c.radius 即为 Circle 类型对象中的属性return 3.14 * c.radius * c.radius
}
输出结果:
圆的面积 = 314
关于值和指针,如果想在方法中改变结构体类型的属性,需要对方法传递指针,体会如下对结构体类型改变的方法 changRadis() 和普通的函数 change() 中的指针操作:
package mainimport ("fmt"
)/* 定义结构体 */
type Circle struct {radius float64
}func main() { var c Circlefmt.Println(c.radius)c.radius = 10.00fmt.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 mainimport ("fmt"
)//MyInt 将int定义为自定义MyInt类型
type MyInt int//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {fmt.Println("Hello, 我是一个int。")
}
func main() {var m1 MyIntm1.SayHello() //Hello, 我是一个int。m1 = 100fmt.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 mainimport "fmt"func factorial(i int) int {if i <= 1 {return 1}return i * factorial(i-1)
}func main() {var i int = 7fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}
输出结果:
Factorial of 7 is 5040
实例2:斐波那契数列(Fibonacci)
这个数列从第3项开始,每一项都等于前两项之和。
package mainimport "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 intfor 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 mainimport "fmt"func main() {/* 声明局部变量 */var a, b, c int/* 初始化参数 */a = 10b = 20c = a + bfmt.Printf ("结果: a = %d, b = %d and c = %d\n", a, b, c)
}
输出结果:
结果: a = 10, b = 20 and c = 30
2. 全局变量
在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
全局变量可以在任何函数中使用,以下实例演示了如何使用全局变量:
package mainimport "fmt"/* 声明全局变量 */
var g intfunc main() {/* 声明局部变量 */var a, b int/* 初始化参数 */a = 10b = 20g = a + bfmt.Printf("结果: a = %d, b = %d and g = %d\n", a, b, g)
}
输出结果:
结果: a = 10, b = 20 and g = 30
一个说明:
Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。实例如下:
package mainimport "fmt"/* 声明全局变量 */
var g int = 20func main() {/* 声明局部变量 */var g int = 10fmt.Printf ("结果: g = %d\n", g)
}
输出结果:
结果: g = 10
3. 形式参数
形式参数会作为函数的局部变量来使用。实例如下:
package mainimport "fmt"/* 声明全局变量 */
var a int = 20func main() {/* main 函数中声明局部变量 */var a int = 10var b int = 20var c int = 0fmt.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 mainfunc 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 mainimport ("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 mainimport "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 mainimport "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 mainimport ("fmt"
)func except() {fmt.Println(recover())
}func test() {defer except()panic("test panic")
}func main() {test()
}
输出结果:
test panic
实例6:如果需要保护代码段,可将代码块重构成匿名函数,如此可确保后续代码可以被执。
package mainimport "fmt"func test(x, y int) {var z intfunc() {defer func() {if recover() != nil {z = 0}}()panic("test panic")z = x / yreturn}()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。
参考链接
- Go 语言函数
- Go 语言变量作用域
- 函数
- Golang defer详解
【Go】Go 语言函数相关推荐
- c语言exit在哪个头文件_C语言函数执行成功时,返回1和返回0,究竟哪个好?
基本上,没有人会将大段的C语言代码全部塞入 main() 函数,更好的做法是按照复用率高,耦合性低的原则,尽可能的将代码拆分不同的功能模块,并封装成函数.C语言代码的组合千变万化,因此函数的功能可能会 ...
- c语言s开头的函数以及作用,C语言函数大全-s开头-完整版.doc
C语言函数大全-s开头-完整版 C语言函数大全(s开头) 函数名: sbrk 功能: 改变数据段空间位置 用法: char *sbrk(int incr); 程序例: #include#include ...
- R语言函数:length计算长度、seq生成数据序列、rep将数据对象重复N遍复制、cut将连续变量分割为多水平的因子变量、pretty将连续变量x分成n个区间创建合适的断点、cat数据对象拼接
R语言函数:length函数计算数据对象的长度.seq函数生成数据序列(sequenceÿ
- go语言----函数 结构体 接口 多态
函数 Go语言 函数是反过来声明 变量类型和 函数返回值 一.一个返回值 package main import "fmt"func max(a int,b int) int { ...
- head在c语言中的作用,阅读以下说明和C语言函数,将应填入(n)处的字句写在对应栏内。【说明】 函数sort (NODE *head)的功能 - 赏学吧...
阅读以下说明和C语言函数,将应填入(n)处的字句写在对应栏内. [说明] 函数sort (NODE *head)的功能是:用冒泡排序法对单链表中的元素进行非递减排序.对于两个相邻结点中的元素,若较小的 ...
- C语言函数知识体系大学霸IT达人
C语言函数知识体系大学霸IT达人 C语言中的函数会集成一条或多条命令(语句)用于实现指定的一个或多个功能.简单的可以将函数理解为一个工具,例如,锤子.锤子的功能是砸东西,木柄和锤头两部分就是函数中包含 ...
- swift1.2语言函数和闭包函数介绍
swift1.2语言函数和闭包函数介绍 在编程中,随着处理问题的越来越复杂,代码量飞速增加.其中,大量的代码往往相互重复或者近似重复.如果不采有效方式加以解决,代码将很难维护. swift1.2语言函 ...
- Swift 1.1语言函数参数的特殊情况本地参数名外部参数名
Swift 1.1语言函数参数的特殊情况本地参数名外部参数名 7.4 函数参数的特殊情况 声明定义有参函数时,为函数的每一个参数都定义了参数名称.根据参数名定义的形式不同,函数参数包括本地参数和外部 ...
- C语言函数指针 和 OC-Block
C语言函数指针 和 OC-Block 一. C语言函数指针 关于函数指针的知识详细可参考: http://www.cnblogs.com/mjios/archive/2013/03/19/296703 ...
- 【C 语言】C 语言 函数 详解 ( 函数本质 | 顺序点 | 可变参数 | 函数调用 | 函数活动记录 | 函数设计 ) [ C语言核心概念 ]
相关文章链接 : 1.[嵌入式开发]C语言 指针数组 多维数组 2.[嵌入式开发]C语言 命令行参数 函数指针 gdb调试 3.[嵌入式开发]C语言 结构体相关 的 函数 指针 数组 4.[嵌入式开发 ...
最新文章
- PL/SQL -- 动态SQL调用包中函数或过程
- Nginx配置中一个不起眼字符/的巨大作用,失之毫厘谬以千里
- 单模光电转换器怎么接_行业观察 | 硅基光电子与微电子单片集成研究进展
- ionic3 动态设置tabs页面底部导航栏隐藏,并显示输入框添加评论
- argparse.ArgumentParser()的用法
- 如何在 ASP.NET MVC 中集成 AngularJS(3)
- Spring Framework--SpringMVC(1)--DispatcherServlet
- 实战解读丨Linux下实现高并发socket最大连接数的配置方法
- jQuery幸运大转盘_jQuery+PHP抽奖程序
- Java日志框架(二)
- Android简单实现百度地图显示及定位
- 研究生期间论文发表经验总结
- mac的 tr命令_tr命令 - Holy_Shit - 博客园
- 【问题解决】xlwings处理excel复制粘贴时数字自动变成科学计数法
- audio音频不能自动播放的解决方法
- Android调整Bitmap图片大小
- java毕业设计拾忆鲜花销售系统mybatis+源码+调试部署+系统+数据库+lw
- 知道创宇研发技能列表v3.0
- Matlab 求全要素生产率,关于使用DEAP2.1计算全要素生产率的问题
- oracle oca课程,Oracle OCA教材.pdf