Golang -> 函数、包和错误处理

123 阅读5分钟

函数、包和错误处理

函数的基本概念

为完成某一功能的程序指令(语句)的集合,称为函数。 

在 Go 中,函数分为: 自定义函数、系统函数(查看 Go 编程手册)

函数的基本语法

快速入门案例

	//输入两个数,再输入一个运算符(+,-,*,/),得到结果.。
	//分析思路....
	var n1 float64 = 1.2
	var n2 float64 = 2.3
	var operator byte = '+'
	result := utils.Cal(n1, n2, operator)
	fmt.Println("result~=", result)
// Cal 将计算的功能,放到一个函数中,然后在需要使用,调用即可
// 为了让其它包的文件使用Cal函数,需要将C大小类似其它语言的public
func Cal(n1 float64, n2 float64, operator byte) float64 {

	var res float64
	switch operator {
	case '+':
		res = n1 + n2
	case '-':
		res = n1 - n2
	case '*':
		res = n1 * n2
	case '/':
		res = n1 / n2
	default:
		fmt.Println("操作符号错误...")
	}
	return res
}

函数作为值、类型

在 Go 中函数也是一种变量,我们可以通过 type 来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型

也就是说, 定义了这种函数类型后, 只要跟定义的函数的参数, 返回值一样的格式, 都属于这一种

type typeName func(input1 inputType1) (result1 resultType1)

函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子

type testInt func(int) bool // 声明了一个函数类型

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)
}

// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
	var result []int
	for _, value := range slice {
      // 直接调用这个函数
    		if f(value) {
    			result = append(result, value)
    		}
	}
	return result
}

// 下面这两个函数, 都是testInt定义的类型, 就在filter中可以使用
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
}

函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到 testInt 这个类型是一个函数类型,然后两个 filter 函数的参数和返回值与 testInt 类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。

defer

1.  Go 语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个 defer 语句。

2.  当函数执行到最后时,这些 defer 语句会按照逆序执行,最后该函数返回。

3.  特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。

func ReadWrite() bool {
    file.Open("file")
    defer file.Close()
    if failureX {
        return false
    }
    return true
}

错误处理

1.Go 没有像 Java 那样的异常机制,它不能抛出异常,而是使用了 panic 和 recover 机制。

2.一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有 panic 的东西。

3.这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢?

Panic

它就像Java的抛出异常

1.是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。

2.当函数 F 调用 panic,函数 F 的执行被中断,但是 F 中的延迟函数会正常执行,然后 F 返回到调用它的地方。

3.在调用的地方,F 的行为就像调用了 panic。

4.这一过程继续向上,直到发生 panic 的 goroutine 中所有调用的函数返回,此时程序退出。恐慌可以直接调用 panic 产生。

5.也可以由运行时错误产生,例如访问越界的数组。

下面这个函数演示了如何在过程中使用 panic

var user = os.Getenv("USER")

func main() {
	haha()
   fmt.Println("执行完成")
}

func haha() {
	if user == "" {
        // 抛出异常
        fmt.Println("执行haha")
        panic("没有找到环境变量 $USER")
	}
}

结果: 打印了haha 和 panic异常信息, 但是main方法也不能正常执行完成, 打印了错误

执行haha
panic: 没有找到环境变量 $USER              
                                           
goroutine 1 [running]:                     
main.haha()                                
        I:/go/day20230703/hello.go:19 +0x79
main.main()                                
        I:/go/day20230703/hello.go:11 +0x19

Recover

就像java的catch捕获异常, 但是这个cache只能在defer延函数中触发

1.是一个内建的函数,可以让进入令人恐慌的流程中的 goroutine 恢复过来。

2.recover 仅在延迟函数defer中有效。

3.在正常的执行过程中,调用 recover 会返回 nil,并且没有其它任何效果。

4.如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

func main() {
	throwsPanic()
   fmt.Println("执行完成")
}

func haha() {
	if user == "" {
		//
		fmt.Println("执行haha")
		panic("没有找到环境变量 $USER")
	}
}

func throwsPanic() (b bool) {
	defer func() {
    // 在抛出异常时, 函数最后会走defer函数, 如果出现了recover, 就不会继续像上抛出
      fmt.Println("recover")
        if x := recover(); x != nil {
                b = true
        }
	}()
	haha() // 执行函数,出现了panic,那么就可以恢复回来
	return
}
执行haha
recover 
执行完成

main 函数和 init 函数

Go 里面有两个保留的函数:

  1. init 函数(能够应用于所有的 package )

  2. main 函数(只能应用于 package main)。

这两个函数在定义时不能有任何的参数和返回值。

虽然一个 package 里面可以写任意多个 init 函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个 package 中每个文件只写一个 init 函数。

Go 程序会自动调用 init() 和 main(),所以你不需要在任何地方调用这两个函数。

每个 package 中的 init 函数都是可选的,但 package main 就必须包含一个 main 函数。

  1. 程序的初始化和执行都起始于 main 包。如果 main 包还导入了其它的包,那么就会在编译时将它们依次导入。

  2. 有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt 包,但它只会被导入一次,因为没有必要导入多次)。

  3. 当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化

  4. 接着执行 init 函数, 就是导入的包的init函数(如果有的话)

  5. 等所有被导入的包都加载完毕了

  6. 就会开始对 main 包中的包级常量和变量进行初始化

  7. 然后执行 main 包中的 init 函数(如果存在的话)

  8. 最后执行 main 函数。

下图详细地解释了整个执行过程:

image.png

包的引出

1.  在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如 main.go 中,去使用 utils.go 文件中的函数,如何实现?

2.  现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang 也想定义函数也叫 Cal。两个程序员为此还吵了起来,怎么办?

包的原理图

包的本质实际上就是创建不同的文件夹,来存放程序文件。

image.png

包的基本概念

go 的每一个文件都是属于一个包的,也就是说 go 是以包的形式来管理文件和项目目录结构的

包的三大作用

1.  区分相同名字的函数、变量等标识符 

2.  当程序文件很多时,可以很好的管理项目 

3.  控制函数、变量等访问范围,即作用域

包的相关说明

打包基本语法: package 包名 

引入包的基本语法: import "包的路径"

package utils

import (
	"fmt"
)

包使用的快速入门

包快速入门-Go 相互调用函数,我们将 func Cal 定义到文件 utils.go , 将 utils.go 放到一个包中,当 其它文件需要使用到 utils.go 的方法时,可以 import 该包,就可以使用了

utils.go

package utils

import (
	"fmt"
)

var Num1 int = 300

// Cal 将计算的功能,放到一个函数中,然后在需要使用,调用即可
// 为了让其它包的文件使用Cal函数,需要将C大小类似其它语言的public
func Cal(n1 float64, n2 float64, operator byte) float64 {

	var res float64
	switch operator {
	case '+':
		res = n1 + n2
	case '-':
		res = n1 - n2
	case '*':
		res = n1 * n2
	case '/':
		res = n1 / n2
	default:
		fmt.Println("操作符号错误...")
	}
	return res
}

main.go

package main

import (
	"../utils"
	"fmt"
)

func main() {
	//输入两个数,再输入一个运算符(+,-,*,/),得到结果.。
	//分析思路....
	var n1 float64 = 1.2
	var n2 float64 = 2.3
	var operator byte = '+'
	result := utils.Cal(n1, n2, operator)
	fmt.Println("result~=", result)
}

包使用的注意事项和细节讨论

1.  在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils, 文件的包名通常和文件所在的文件夹名一致,一般为小写字母。 

2.  当一个文件要使用其它包函数或变量时,需要先引入对应的包

1.  引入方式 1:import "包名"

2.  引入方式 2:import ( "包名" "包名" )

3.  package 指令在文件第一行,然后是 import 指令

4.  在 import 包时,路径从 $GOPATH 的 src 下开始,不用带 src, 编译器会自动从 src 下开始引入

3.  为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言 的 public ,这样才能跨包访问。比如 utils.go 的

func Cal(n1 float64, n2 float64, operator byte) float64

4.  在访问其它包函数,变量时,其语法是 包名.函数名, 比如这里的 main.go 文件中

utils.Cal(90, 80, "+")

5.  如果包名较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了

import (
	util "../utils"
	"fmt"
)
func main() {
    util.Cal(90, 80, "+")
}

6.  在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义

7.  如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就 是一个语法规范,如果你是写一个库 ,包名可以自定义 8.  点操作

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的 fmt.Println ("hello world") 可以省略的写成 Println ("hello world")

import (
    . "fmt"
)

func main() {
    Println ("hello world")
}