函数、包、错误处理
函数
为什么需要函数?
以下面这个例子来说明:
需求:
实现简易计算器:输入两个数、一个运算符,得到运算结果。
实现:
// 通过传统方式实现
// 缺点:
// 1、如果需要实现相同功能,代码需要重复编写,无法直接复用(如果有多组数据需要计算,如何优雅的实现呢?)
// 2、代码冗余、可维护性、可读性差
package main
import "fmt"
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)
switch expr {
case "+":
result = a + b
case "-":
result = a - b
case "*":
result = a * b
case "/":
result = a / b
default:
fmt.Println("输入的计算符号不支持")
}
fmt.Println("result=", result)
}
// 通过函数的方式实现
// 优点:
// 1、可重复调用
package main
import "fmt"
func cal(a float64, b float64, expr string) float64 {
var result float64
switch expr {
case "+":
result = a + b
case "-":
result = a - b
case "*":
result = a * b
case "/":
result = a / b
default:
fmt.Println("输入的计算符号不支持")
}
return result
}
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 = cal(a, b, expr) //此处通过调用cal函数进行数据运算
fmt.Println("result=", result)
}
基本介绍
为了完成某一个功能的代码集合,称为函数。
在Go中,函数可以分为:自定义函数、系统函数
基本语法
func <函数名> (形参列表) (返回值列表) {
<执行语句>
return <返回值列表>
}
- 函数名:编程人员自定义的函数名称,符合Go标识符命名规范即可
- 形参列表:定义调用该函数需要传入的参数
- 返回值列表:定义调用该函数后,会返回的值列表;如果没有返回值,也可以不定义,留空
- 执行语句:函数具体执行的代码语句
入门案例
这里通过函数实现8.1.1章节中的数据运算需求
// 通过函数的方式实现数据运算
// 优点:
// 1、可重复调用、维护性高、可读性好
package main
import "fmt"
func cal(a float64, b float64, expr string) float64 {
var result float64
switch expr {
case "+":
result = a + b
case "-":
result = a - b
case "*":
result = a * b
case "/":
result = a / b
default:
fmt.Println("输入的计算符号不支持")
}
return result
}
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 = cal(a, b, expr) //此处通过调用cal函数进行数据运算
fmt.Println("result=", result)
}
包
为什么需要包?
在实际的开发中,我们往往需要在不同的文件中,去调用其他文件中定义的函数,比如在main.go中调用utils.go文件中定义的函数,那么如何实现这些操作呢?就是通过包(package)来实现的。
包的原理图
包的本质实际上就是创建不同的文件夹来存放程序文件
如上图
- cmd目录用于存放需要编译成二进制可执行文件的main包:main.go
- db目录用于存放数据库相关操作的包文件db.go
- utils目录用于存放实用程序的包文件utils.go
基本介绍
每一个.go文件都属于一个包,也就是说go是以包的形式来管理文件和项目目录结构的
作用:
- 不同包内可以使用相同名称的函数、变量等标识符
- 当程序文件很多时,包可以很好的进行项目管理
- 可以控制函数、变量等标识符的作用范围,即作用域
基本语法
- 创建包:package <包名称>
- 引用包:import <包路径+包名称>
入门案例
将前面8.1.4章节中的cal函数定义在包utils中,并将其首字母大写,变成可导出的函数Cal,当其他包需要使用到Cal函数时,可以import utils包
// utils.go
package utils
import "fmt"
// 这里定义Cal函数时,首字母须大写,使其称为可导出的函数,才能被其他包引用
func Cal(a float64, b float64, expr string) float64 {
var result float64
switch expr {
case "+":
result = a + b
case "-":
result = a - b
case "*":
result = a * b
case "/":
result = a / b
default:
fmt.Println("输入的计算符号不支持")
}
return result
}
// main.go
package main
import (
"fmt"
"learning/app/utils" // 路径从go module的位置开始写
)
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 = utils.Cal(a, b, expr) //此处通过调用utils包中的Cal函数进行数据运算
fmt.Println("result=", result)
}
注意事项
- 在给一个文件打包时,该包对应一个文件夹,比如这里的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) }
<a name="X8YyH"></a>
## 函数调用
<a name="bHCTN"></a>
### 调用过程
![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)
- 在调用一个函数时,会给这个函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他栈空间区分开
- 在每个函数对应的栈中,数据空间时独立的,不会混淆
- 当一个函数调用完毕,程序会销毁这个函数对应的栈空间
<a name="wxW9E"></a>
### return语句
<a name="z4Kn6"></a>
#### 基本语法
Go函数支持返回多个值,这一点是其他编程语言没有的<br />**func <函数名> (形参列表) (返回值类型列表) {<br /> <代码块>**<br />**return 返回值列表**<br />**}**
- 如果返回多个值时,在接收时,希望忽略某个返回值,可以使用 **_** 进行占位忽略
- 如果返回值只有一个,**(返回值类型列表) **可以不写**( )**
<a name="AhYhc"></a>
#### 案例演示
编写一个函数计算两个数的和与差,并返回结果
```go
package main
import "fmt"
func cal(a, b float64) (sum, sub float64) {
sum = a + b
sub = a - b
return sum, sub
}
func main() {
var a, b float64
fmt.Println("请输入变量a的值")
fmt.Scan(&a)
fmt.Println("请输入变量b的值")
fmt.Scan(&b)
sum, sub := cal(a, b)
fmt.Printf("sum=%v, sub=%v",sum, sub)
}
希望忽略返回值中两个数的差
package main
import "fmt"
func cal(a, b float64) (sum, sub float64) {
sum = a + b
sub = a - b
return sum, sub
}
func main() {
var a, b float64
fmt.Println("请输入变量a的值")
fmt.Scan(&a)
fmt.Println("请输入变量b的值")
fmt.Scan(&b)
sum, _ := cal(a, b)
//fmt.Printf("sum=%v, sub=%v",sum, sub)
fmt.Printf("sum=%v",sum)
}
函数递归调用
基本介绍
一个函数在函数体内又调用的自己,称之为函数递归调用
入门案例
package main
import "fmt"
func test(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println("n=", n)
}
func main() {
test(4)
}
/*
test(4)
4 > 2
n=3
test(3)
3 > 2
n = 2
test(2)
2 !> 2 此时if条件不满足,执行test(2)中的print
n = 2
Print n=2
Print n=2 test(3) if语句执行完毕后,开始执行test(3)的print
Print n=3 test(4) if语句执行完毕后,开始执行test(4)的print
因此程序运行结果是
n= 2
n= 2
n= 3
*/
package main
import "fmt"
func test2(n int) {
if n > 2 {
n--
test2(n)
} else {
fmt.Println("n=", n)
}
}
func main() {
test2(4)
}
/*
test2(4)
4 > 2
n = 3
test2(3)
3 > 2
n = 2
test2(2)
2 !> 2
n = 2
print n=2
test2(3) 阶段符合了if条件,不会执行else后的print语句
test2(4) 阶段符合了if条件,不会执行else后的print语句
因此程序运行结果是
n= 2
*/
注意事项
函数递归需要遵守的重要原则:
- 当执行一个函数时,会创建一个新的保护的独立空间(新函数栈)
- 函数的局部变量时独立的,不会相互影响
- 每次递归必须趋向退出递归的条件,否则就是无限递归了
- 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁
- 函数执行完毕/返回时,该函数本身栈空间也会被系统销毁
函数调用注意事项
- 函数的形参列表可以是多个,返回值列表也可以是多个
- 形参列表和返回值列表的数据类型可以是 值类型,也可以是引用类型
- 函数命名须符合Go标识符命名规范,可以由字母、数字、下划线组成,不能以数字开头;函数名首字母大写表示该函数可导出的,可被其他包访问
- 函数内部定义的变量是局部变量,在函数外不生效
- 基本数据类型、数组默认都是值传递,即进行值拷贝。在函数内修改,不会影响到原来的值
- 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,使得函数内以指针的方式操作变量
- Go中函数不支持重载,会报错重复定义
- 在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量,通过该变量可以对函数进行调用
- 函数既然是一种数据类型,因此在Go中,函数可以作为其他函数的形参,进行函数调用
- 为了简化数据类型定义,Go支持自定义数据类型
- 支持对函数返回值进行命名
- 使用 下划线_ 可以对返回值进行占位忽略
- Go支持可变参数
init函数
基本介绍
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用
init函数一般用于在main函数之前执行一些初始化的工作
入门案例
// 演示Go语言 init函数
package main
import "fmt"
func init() {
fmt.Println("exec init func")
}
func main() {
fmt.Println("exec main func")
}
/*
从函数运行结果可以看出,init函数是先于main函数执行的
函数运行结果展示:
exec init func
exec main func
*/
注意事项
- 如果一个文件同时包含全局变量定义、init函数、main函数,则执行的流程是:
- init函数最主要的作用,就是完成一些初始化的工作
- 细节说明:如果main.go、utils.go都含有变量定义,那么init函数执行流程是怎么的呢?
匿名函数
基本介绍
Go支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
基本语法
- 方式一:在定义匿名函数时,直接调用,这种方式下匿名函数只能被调用一次 ```go // 演示匿名函数使用方式一 package main
import “fmt”
func main() { result := func(n1 int, n2 int) int { return n1 + n2 }(10, 20) fmt.Println(“result=”, result) }
- 方式二:将匿名函数赋予给一个变量(函数变量),再通过该变量来调用匿名函数
```go
// 演示匿名函数使用方式二
package main
import "fmt"
func main() {
a := func(n1 int, n2 int) int {
return n1 - n2
}
result01 := a(10, 20)
result02 := a(100, 200)
fmt.Printf("result01=%v\nresult02=%v", result01, result02)
}
- 方式三:全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在整个包/程序有效(如果变量首字母大写,则整个程序都可以调用)
// 演示将匿名函数赋予给全量变量
package main
import (
"fmt"
)
var (
Fvar = func(n1 int, n2 int) int {
return n1 * n2
}
)
func main() {
result := Fvar(10, 20)
fmt.Println("result=", result)
}
闭包
基本介绍
闭包就是一个函数 和 与其相关的引用环境 组合的一个整体
案例演示
// 演示Go中闭包的使用
package main
import "fmt"
func AddUpper() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}
func main() {
f := AddUpper()
fmt.Println(f(1))
fmt.Println(f(2))
fmt.Println(f(3))
}
/*
AddUpper是一个函数,返回的数据类型是func (int) int
运行结果展示:
11
13
16
*/
闭包的说明
- 返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包
- 当我们反复的调用函数f时,因为n仅初始化一次,因此每调用一次就进行累计
- 要搞清楚闭包,需要分析出返回的函数使用了哪些变量,因为函数和它所引用的变量共同构成了闭包
defer
为什么需要defer?
在函数中,程序员经常需要创建资源(例如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go语言提供了defer(延时机制)
入门案例
// 演示defer的用法
package main
import "fmt"
func sum(n1, n2 int) int {
defer fmt.Println("ok01 n1=", n1)
defer fmt.Println("ok02 n2=", n2)
res := n1 + n2
fmt.Println("ok03 res=", res)
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res=", res)
}
注意事项
- 当Go执行到一个defer时,不会立即执行defer后的语句,而是将defer语句及其当时环境变量的值一起暂存起来
- 当函数执行完毕后,依据defer先入后出的原则,执行defer语句
函数参数传递
基本介绍
函数参数传递方式有两种:值传递、引用传递
值类型参数默认就是值传递,引用类型参数默认就是引用传递
分类
- 值类型:基本数据类型(int系列、float系列、bool、string)、数组、结构体
- 引用类型:指针、切片、map、channel、interface等是引用类型
使用特点
- 值类型默认就是值传递:变量直接存储值,内存通常在栈中分配
- 引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才是真正存储数据的空间,内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
- 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用。
常用内置函数
字符串处理
字符串长度
- len(str)
仅可用于统计不含中文的字符串长度
package main
import "fmt"
func main() {
strA := "helloWorld!"
fmt.Println("strA len =", len(strA))
strB := "hello world!"
fmt.Println("strB len =", len(strB))
strC := "你好 hello world!"
fmt.Println("strC len =", len(strC))
strD := "你好~ hello world!"
fmt.Println("strD len =", len(strD))
}
/*
如上程序运行结果是:
strA len = 11
strB len = 12
strC len = 19
strD len = 20
可以看到,当字符串中含有中文时,通过len(str) 函数得到的结果与期望不一致
这是因为len(str) 函数统计字符串长度是按照字节来统计的,而一个中文字符通常会占有3个字节
golang的编码使用的是UTF-8,ascii码(字母和数字)的字符占用一个字节,汉子占用3个字节
*/
- len([]rune(str))
先将字符串转为rune类型的切片,再使用len(str)函数,可用于统计含中文的字符串长度
package main
import "fmt"
func main() {
strC := "你好 hello world!"
fmt.Println("strC len =", len([]rune(strC)))
}
/*
如上程序运行结果是:
strC len = 15
*/
字符串遍历
- for range
for range 可用于遍历数组、切片、字符串、map、通道
package main
import "fmt"
func main() {
str := "你好 hello world!"
for k, v := range str {
fmt.Printf("k =%v, v =%c\n", k, v)
}
}
- 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]) } }
```