函数、包、错误处理

函数

为什么需要函数?

以下面这个例子来说明:
需求:
实现简易计算器:输入两个数、一个运算符,得到运算结果。
实现:

  1. // 通过传统方式实现
  2. // 缺点:
  3. // 1、如果需要实现相同功能,代码需要重复编写,无法直接复用(如果有多组数据需要计算,如何优雅的实现呢?)
  4. // 2、代码冗余、可维护性、可读性差
  5. package main
  6. import "fmt"
  7. func main() {
  8. var (
  9. a, b, result float64
  10. expr string
  11. )
  12. fmt.Println("请输入数值a")
  13. fmt.Scan(&a)
  14. fmt.Println("请输入数值b")
  15. fmt.Scan(&b)
  16. fmt.Println("请输入计算符号")
  17. fmt.Scan(&expr)
  18. switch expr {
  19. case "+":
  20. result = a + b
  21. case "-":
  22. result = a - b
  23. case "*":
  24. result = a * b
  25. case "/":
  26. result = a / b
  27. default:
  28. fmt.Println("输入的计算符号不支持")
  29. }
  30. fmt.Println("result=", result)
  31. }
  1. // 通过函数的方式实现
  2. // 优点:
  3. // 1、可重复调用
  4. package main
  5. import "fmt"
  6. func cal(a float64, b float64, expr string) float64 {
  7. var result float64
  8. switch expr {
  9. case "+":
  10. result = a + b
  11. case "-":
  12. result = a - b
  13. case "*":
  14. result = a * b
  15. case "/":
  16. result = a / b
  17. default:
  18. fmt.Println("输入的计算符号不支持")
  19. }
  20. return result
  21. }
  22. func main() {
  23. var (
  24. a, b, result float64
  25. expr string
  26. )
  27. fmt.Println("请输入数值a")
  28. fmt.Scan(&a)
  29. fmt.Println("请输入数值b")
  30. fmt.Scan(&b)
  31. fmt.Println("请输入计算符号")
  32. fmt.Scan(&expr)
  33. result = cal(a, b, expr) //此处通过调用cal函数进行数据运算
  34. fmt.Println("result=", result)
  35. }

基本介绍

为了完成某一个功能的代码集合,称为函数。
在Go中,函数可以分为:自定义函数、系统函数
函数、包、错误处理 - 图1

基本语法

func <函数名> (形参列表) (返回值列表) {
<执行语句>
return <返回值列表>
}

  • 函数名:编程人员自定义的函数名称,符合Go标识符命名规范即可
  • 形参列表:定义调用该函数需要传入的参数
  • 返回值列表:定义调用该函数后,会返回的值列表;如果没有返回值,也可以不定义,留空
  • 执行语句:函数具体执行的代码语句

入门案例

这里通过函数实现8.1.1章节中的数据运算需求

  1. // 通过函数的方式实现数据运算
  2. // 优点:
  3. // 1、可重复调用、维护性高、可读性好
  4. package main
  5. import "fmt"
  6. func cal(a float64, b float64, expr string) float64 {
  7. var result float64
  8. switch expr {
  9. case "+":
  10. result = a + b
  11. case "-":
  12. result = a - b
  13. case "*":
  14. result = a * b
  15. case "/":
  16. result = a / b
  17. default:
  18. fmt.Println("输入的计算符号不支持")
  19. }
  20. return result
  21. }
  22. func main() {
  23. var (
  24. a, b, result float64
  25. expr string
  26. )
  27. fmt.Println("请输入数值a")
  28. fmt.Scan(&a)
  29. fmt.Println("请输入数值b")
  30. fmt.Scan(&b)
  31. fmt.Println("请输入计算符号")
  32. fmt.Scan(&expr)
  33. result = cal(a, b, expr) //此处通过调用cal函数进行数据运算
  34. fmt.Println("result=", result)
  35. }

为什么需要包?

在实际的开发中,我们往往需要在不同的文件中,去调用其他文件中定义的函数,比如在main.go中调用utils.go文件中定义的函数,那么如何实现这些操作呢?就是通过包(package)来实现的。

包的原理图

包的本质实际上就是创建不同的文件夹来存放程序文件
image.png
如上图

  • cmd目录用于存放需要编译成二进制可执行文件的main包:main.go
  • db目录用于存放数据库相关操作的包文件db.go
  • utils目录用于存放实用程序的包文件utils.go

基本介绍

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

  • 不同包内可以使用相同名称的函数、变量等标识符
  • 当程序文件很多时,包可以很好的进行项目管理
  • 可以控制函数、变量等标识符的作用范围,即作用域

基本语法

  • 创建包:package <包名称>


  • 引用包:import <包路径+包名称>

包路径需要从go module的位置开始写

入门案例

将前面8.1.4章节中的cal函数定义在包utils中,并将其首字母大写,变成可导出的函数Cal,当其他包需要使用到Cal函数时,可以import utils包
image.png

  1. // utils.go
  2. package utils
  3. import "fmt"
  4. // 这里定义Cal函数时,首字母须大写,使其称为可导出的函数,才能被其他包引用
  5. func Cal(a float64, b float64, expr string) float64 {
  6. var result float64
  7. switch expr {
  8. case "+":
  9. result = a + b
  10. case "-":
  11. result = a - b
  12. case "*":
  13. result = a * b
  14. case "/":
  15. result = a / b
  16. default:
  17. fmt.Println("输入的计算符号不支持")
  18. }
  19. return result
  20. }
  1. // main.go
  2. package main
  3. import (
  4. "fmt"
  5. "learning/app/utils" // 路径从go module的位置开始写
  6. )
  7. func main() {
  8. var (
  9. a, b, result float64
  10. expr string
  11. )
  12. fmt.Println("请输入数值a")
  13. fmt.Scan(&a)
  14. fmt.Println("请输入数值b")
  15. fmt.Scan(&b)
  16. fmt.Println("请输入计算符号")
  17. fmt.Scan(&expr)
  18. result = utils.Cal(a, b, expr) //此处通过调用utils包中的Cal函数进行数据运算
  19. fmt.Println("result=", result)
  20. }

注意事项

  • 在给一个文件打包时,该包对应一个文件夹,比如这里的utils文件夹对应的包名就是utils,包名称和文件夹名称保持一致,一般为小写字母
  • 当一个文件要使用其他包函数或变量时,需要先引入对应的包
  • 为了让其他包可以访问包中的函数,则该函数名称的首字母需要大写。即函数属性为可导出,这样该函数才能被跨包访问
  • 在访问其他包中的函数、变量时,语法为:包名称.函数名
  • 同一个包内不能有同名的函数、变量等标识符,但是不同的包内可以存在同名的函数、变量等标识符
  • 如果要编译成一个二进制可执行文件,需要将这个包声明为main包,即package main;如果是写一个库程序,包名可以自定义
  • 如果包名称很长,Go支持在import阶段给包取别名,注:取了别名后,原包名就不能使用了 ```go // main.go package main

import ( “fmt” ut “learning/app/utils” // 此处使用ut作为utils的别名,调用时只能通过别名ut进行调用 )

func main() { var ( a, b, result float64 expr string ) fmt.Println(“请输入数值a”) fmt.Scan(&a) fmt.Println(“请输入数值b”) fmt.Scan(&b) fmt.Println(“请输入计算符号”) fmt.Scan(&expr) result = ut.Cal(a, b, expr) //此处通过调用ut包中的Cal函数进行数据运算 fmt.Println(“result=”, result) }

  1. <a name="X8YyH"></a>
  2. ## 函数调用
  3. <a name="bHCTN"></a>
  4. ### 调用过程
  5. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1065799/1642937510209-4adfbade-113d-4ed6-abb6-224c7a329da5.png#clientId=ue1edb7cc-26cd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=367&id=u5144039a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=734&originWidth=1672&originalType=binary&ratio=1&rotation=0&showTitle=false&size=523532&status=done&style=none&taskId=ucb1777a1-a689-41b9-af53-5c13faab9ac&title=&width=836)
  6. - 在调用一个函数时,会给这个函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他栈空间区分开
  7. - 在每个函数对应的栈中,数据空间时独立的,不会混淆
  8. - 当一个函数调用完毕,程序会销毁这个函数对应的栈空间
  9. <a name="wxW9E"></a>
  10. ### return语句
  11. <a name="z4Kn6"></a>
  12. #### 基本语法
  13. Go函数支持返回多个值,这一点是其他编程语言没有的<br />**func <函数名> (形参列表) (返回值类型列表) {<br /> <代码块>**<br />**return 返回值列表**<br />**}**
  14. - 如果返回多个值时,在接收时,希望忽略某个返回值,可以使用 **_** 进行占位忽略
  15. - 如果返回值只有一个,**(返回值类型列表) **可以不写**( )**
  16. <a name="AhYhc"></a>
  17. #### 案例演示
  18. 编写一个函数计算两个数的和与差,并返回结果
  19. ```go
  20. package main
  21. import "fmt"
  22. func cal(a, b float64) (sum, sub float64) {
  23. sum = a + b
  24. sub = a - b
  25. return sum, sub
  26. }
  27. func main() {
  28. var a, b float64
  29. fmt.Println("请输入变量a的值")
  30. fmt.Scan(&a)
  31. fmt.Println("请输入变量b的值")
  32. fmt.Scan(&b)
  33. sum, sub := cal(a, b)
  34. fmt.Printf("sum=%v, sub=%v",sum, sub)
  35. }

希望忽略返回值中两个数的差

  1. package main
  2. import "fmt"
  3. func cal(a, b float64) (sum, sub float64) {
  4. sum = a + b
  5. sub = a - b
  6. return sum, sub
  7. }
  8. func main() {
  9. var a, b float64
  10. fmt.Println("请输入变量a的值")
  11. fmt.Scan(&a)
  12. fmt.Println("请输入变量b的值")
  13. fmt.Scan(&b)
  14. sum, _ := cal(a, b)
  15. //fmt.Printf("sum=%v, sub=%v",sum, sub)
  16. fmt.Printf("sum=%v",sum)
  17. }

函数递归调用

基本介绍

一个函数在函数体内又调用的自己,称之为函数递归调用

入门案例

  1. package main
  2. import "fmt"
  3. func test(n int) {
  4. if n > 2 {
  5. n--
  6. test(n)
  7. }
  8. fmt.Println("n=", n)
  9. }
  10. func main() {
  11. test(4)
  12. }
  13. /*
  14. test(4)
  15. 4 > 2
  16. n=3
  17. test(3)
  18. 3 > 2
  19. n = 2
  20. test(2)
  21. 2 !> 2 此时if条件不满足,执行test(2)中的print
  22. n = 2
  23. Print n=2
  24. Print n=2 test(3) if语句执行完毕后,开始执行test(3)的print
  25. Print n=3 test(4) if语句执行完毕后,开始执行test(4)的print
  26. 因此程序运行结果是
  27. n= 2
  28. n= 2
  29. n= 3
  30. */
  1. package main
  2. import "fmt"
  3. func test2(n int) {
  4. if n > 2 {
  5. n--
  6. test2(n)
  7. } else {
  8. fmt.Println("n=", n)
  9. }
  10. }
  11. func main() {
  12. test2(4)
  13. }
  14. /*
  15. test2(4)
  16. 4 > 2
  17. n = 3
  18. test2(3)
  19. 3 > 2
  20. n = 2
  21. test2(2)
  22. 2 !> 2
  23. n = 2
  24. print n=2
  25. test2(3) 阶段符合了if条件,不会执行else后的print语句
  26. test2(4) 阶段符合了if条件,不会执行else后的print语句
  27. 因此程序运行结果是
  28. n= 2
  29. */

注意事项

函数递归需要遵守的重要原则:

  • 当执行一个函数时,会创建一个新的保护的独立空间(新函数栈)
  • 函数的局部变量时独立的,不会相互影响
  • 每次递归必须趋向退出递归的条件,否则就是无限递归了
  • 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁
  • 函数执行完毕/返回时,该函数本身栈空间也会被系统销毁

函数调用注意事项

  • 函数的形参列表可以是多个,返回值列表也可以是多个
  • 形参列表和返回值列表的数据类型可以是 值类型,也可以是引用类型
  • 函数命名须符合Go标识符命名规范,可以由字母、数字、下划线组成,不能以数字开头;函数名首字母大写表示该函数可导出的,可被其他包访问
  • 函数内部定义的变量是局部变量,在函数外不生效
  • 基本数据类型、数组默认都是值传递,即进行值拷贝。在函数内修改,不会影响到原来的值
  • 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,使得函数内以指针的方式操作变量
  • Go中函数不支持重载,会报错重复定义
  • 在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量,通过该变量可以对函数进行调用
  • 函数既然是一种数据类型,因此在Go中,函数可以作为其他函数的形参,进行函数调用
  • 为了简化数据类型定义,Go支持自定义数据类型
  • 支持对函数返回值进行命名
  • 使用 下划线_ 可以对返回值进行占位忽略
  • Go支持可变参数

init函数

基本介绍

每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用
init函数一般用于在main函数之前执行一些初始化的工作

入门案例

  1. // 演示Go语言 init函数
  2. package main
  3. import "fmt"
  4. func init() {
  5. fmt.Println("exec init func")
  6. }
  7. func main() {
  8. fmt.Println("exec main func")
  9. }
  10. /*
  11. 从函数运行结果可以看出,init函数是先于main函数执行的
  12. 函数运行结果展示:
  13. exec init func
  14. exec main func
  15. */

注意事项

  • 如果一个文件同时包含全局变量定义、init函数、main函数,则执行的流程是:

函数、包、错误处理 - 图4

  • init函数最主要的作用,就是完成一些初始化的工作
  • 细节说明:如果main.go、utils.go都含有变量定义,那么init函数执行流程是怎么的呢?

函数、包、错误处理 - 图5

匿名函数

基本介绍

Go支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。

基本语法

  • 方式一:在定义匿名函数时,直接调用,这种方式下匿名函数只能被调用一次 ```go // 演示匿名函数使用方式一 package main

import “fmt”

func main() { result := func(n1 int, n2 int) int { return n1 + n2 }(10, 20) fmt.Println(“result=”, result) }

  1. - 方式二:将匿名函数赋予给一个变量(函数变量),再通过该变量来调用匿名函数
  2. ```go
  3. // 演示匿名函数使用方式二
  4. package main
  5. import "fmt"
  6. func main() {
  7. a := func(n1 int, n2 int) int {
  8. return n1 - n2
  9. }
  10. result01 := a(10, 20)
  11. result02 := a(100, 200)
  12. fmt.Printf("result01=%v\nresult02=%v", result01, result02)
  13. }
  • 方式三:全局匿名函数

如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在整个包/程序有效(如果变量首字母大写,则整个程序都可以调用)

  1. // 演示将匿名函数赋予给全量变量
  2. package main
  3. import (
  4. "fmt"
  5. )
  6. var (
  7. Fvar = func(n1 int, n2 int) int {
  8. return n1 * n2
  9. }
  10. )
  11. func main() {
  12. result := Fvar(10, 20)
  13. fmt.Println("result=", result)
  14. }

闭包

基本介绍

闭包就是一个函数 和 与其相关的引用环境 组合的一个整体

案例演示

  1. // 演示Go中闭包的使用
  2. package main
  3. import "fmt"
  4. func AddUpper() func(int) int {
  5. var n int = 10
  6. return func(x int) int {
  7. n = n + x
  8. return n
  9. }
  10. }
  11. func main() {
  12. f := AddUpper()
  13. fmt.Println(f(1))
  14. fmt.Println(f(2))
  15. fmt.Println(f(3))
  16. }
  17. /*
  18. AddUpper是一个函数,返回的数据类型是func (int) int
  19. 运行结果展示:
  20. 11
  21. 13
  22. 16
  23. */

闭包的说明

  • 返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包
  • 当我们反复的调用函数f时,因为n仅初始化一次,因此每调用一次就进行累计
  • 要搞清楚闭包,需要分析出返回的函数使用了哪些变量,因为函数和它所引用的变量共同构成了闭包

defer

为什么需要defer?

在函数中,程序员经常需要创建资源(例如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go语言提供了defer(延时机制)

入门案例

  1. // 演示defer的用法
  2. package main
  3. import "fmt"
  4. func sum(n1, n2 int) int {
  5. defer fmt.Println("ok01 n1=", n1)
  6. defer fmt.Println("ok02 n2=", n2)
  7. res := n1 + n2
  8. fmt.Println("ok03 res=", res)
  9. return res
  10. }
  11. func main() {
  12. res := sum(10, 20)
  13. fmt.Println("res=", res)
  14. }

注意事项

  • 当Go执行到一个defer时,不会立即执行defer后的语句,而是将defer语句及其当时环境变量的值一起暂存起来
  • 当函数执行完毕后,依据defer先入后出的原则,执行defer语句

函数参数传递

基本介绍

函数参数传递方式有两种:值传递、引用传递
值类型参数默认就是值传递,引用类型参数默认就是引用传递

分类

  • 值类型:基本数据类型(int系列、float系列、bool、string)、数组、结构体
  • 引用类型:指针、切片、map、channel、interface等是引用类型

使用特点

  • 值类型默认就是值传递:变量直接存储值,内存通常在栈中分配
  • 引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才是真正存储数据的空间,内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
  • 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用。

常用内置函数

字符串处理

字符串长度

  • len(str)

仅可用于统计不含中文的字符串长度

  1. package main
  2. import "fmt"
  3. func main() {
  4. strA := "helloWorld!"
  5. fmt.Println("strA len =", len(strA))
  6. strB := "hello world!"
  7. fmt.Println("strB len =", len(strB))
  8. strC := "你好 hello world!"
  9. fmt.Println("strC len =", len(strC))
  10. strD := "你好~ hello world!"
  11. fmt.Println("strD len =", len(strD))
  12. }
  13. /*
  14. 如上程序运行结果是:
  15. strA len = 11
  16. strB len = 12
  17. strC len = 19
  18. strD len = 20
  19. 可以看到,当字符串中含有中文时,通过len(str) 函数得到的结果与期望不一致
  20. 这是因为len(str) 函数统计字符串长度是按照字节来统计的,而一个中文字符通常会占有3个字节
  21. golang的编码使用的是UTF-8,ascii码(字母和数字)的字符占用一个字节,汉子占用3个字节
  22. */
  • len([]rune(str))

先将字符串转为rune类型的切片,再使用len(str)函数,可用于统计含中文的字符串长度

  1. package main
  2. import "fmt"
  3. func main() {
  4. strC := "你好 hello world!"
  5. fmt.Println("strC len =", len([]rune(strC)))
  6. }
  7. /*
  8. 如上程序运行结果是:
  9. strC len = 15
  10. */

点击查看更多方法

字符串遍历

  • for range

for range 可用于遍历数组、切片、字符串、map、通道

  1. package main
  2. import "fmt"
  3. func main() {
  4. str := "你好 hello world!"
  5. for k, v := range str {
  6. fmt.Printf("k =%v, v =%c\n", k, v)
  7. }
  8. }
  • for循环 + []rune(str) ```go package main

import “fmt”

func main() { str := “你好 hello world!” r := []rune(str) for i := 0; i < len(r); i++ { fmt.Printf(“k=%v, v=%c\n”, i, r[i]) } }

```