【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] {
   函数体
}

解析:

  1. 函数声明包含一个函数名,参数列表, 返回值列表和函数体。

  2. func:函数由关键字 func 声明。左大括号依旧不能另起一行。

  3. name:函数名称。

  4. parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。函数可以没有参数或接受多个参数。注意参数类型在变量名之后 。当两个或多个连续的参数是同一类型,则除了最后一个类型之外,其他都可以省略。

  5. return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型,如(string, string)返回两个字符串。有些功能不需要返回值,如果函数没有返回值,则返回列表可以省略。也就是说,函数可以返回任意数量的返回值。有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
    函数可以返回多个值,多返回值必须用括号。如:

    func test(x, y int, s string) (int, string) {
        // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
        n := x + y          
        return n, fmt.Sprintf(s, n)
    }
    
  6. 函数体:代码集合(一般实现一个功能)。函数从第一条语句开始执行,直到执行 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. 值传递和引用传递

函数如果使用参数,该变量可称为函数的形参
形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
【Go】Go 语言函数_第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

注意:

  1. 无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
  2. 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{}) {
}

五、函数返回值

  1. 返回值的忽略
    _标识符,用来忽略函数的某个返回值。
    Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 “_” 忽略。

  2. 多返回值可直接作为其他函数调用实参。

    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
    
  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 // 必须显式返回。
        }
    }
    
  4. 没有参数的 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
    
  5. 命名返回参数允许 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特性:

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 跳转前才被执。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

defer用途:

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放

Go 语言 中的 defer 语句用于延迟函数的调用,每次 defer 都会把一个函数压入 中,函数返回前再把延迟的函数取出并执行。Golang 中的 defer 可以帮助我们处理容易忽略的问题,如资源释放、连接关闭等。

go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。

Golang 官方博客里总结了 defer 的行为规则,只有三条,分别为:

  1. 延迟函数的参数在 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 后面的语句对变量的修改可能会影响延迟函数。

  2. 延迟函数执行按 后进先出 顺序执行,即先出现的 defer 最后执行。

    这个规则很好理解,定义 defer 类似于入栈操作,执行 defer 类似于出栈操作。

    设计 defer 的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请 A 资源,再跟据 A 资源申请 B 资源,根据 B 资源申请 C 资源,即申请顺序是:A–>B–>C,释放时往往又要反向进行。这就是把 defer 设计成 FIFO 的原因。

    每申请到一个用完需要释放的资源时,立即定义一个 defer 来释放资源是个很好的习惯。

    多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

  3. 延迟函数可能操作主函数的具名返回值(命名返回值)

    定义 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!

七、函数用法

【Go】Go 语言函数_第2张图片

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, 我是一个int100  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)默认初始化值

不同类型的局部和全局变量初始化 默认值(就是不初始化时,系统自动给的值)为:
【Go】Go 语言函数_第3张图片


十一、异常处理

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。

(结构化异常指的是C/C++程序语言中,程序控制结构try-excepttry-finally语句用于处理异常事件。)

异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

panic 介绍:

  1. 内置函数
  2. 假如函数 F 中书写了 panic 语句,会终止其后要执行的代码,在 panic 所在函数 F 内如果存在要执行的 defer 函数列表,按照 defer 的逆序执行
  3. 返回函数 F 的调用者 G ,在 G 中,调用函数 F 语句之后的代码不会执行,假如函数 G 中存在要执行的 defer 函数列表,按照 defer 的逆序执行
  4. 直到 goroutine 整个退出,并报告错误

recover 介绍:

  1. 内置函数
  2. 用来控制一个 goroutine 的 panicking 行为,捕获 panic ,从而影响应用的行为
  3. 一般的调用建议
    a. 在 defer 函数中,通过 recever 来终止一个 goroutine 的 panicking 过程,从而恢复正常代码的执行
    b. 可以获取通过 panic 传递的 error

注意:

  1. 利用 recover 处理 panic 指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当 panic 时,recover无法捕获到 panic ,无法防止 panic 扩散。
  2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  3. 多个 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。


参考链接

  1. Go 语言函数
  2. Go 语言变量作用域
  3. 函数
  4. Golang defer详解

你可能感兴趣的:(Go,golang,开发语言)