函数、包、错误处理
函数
为什么需要函数?
以下面这个例子来说明:
需求:
实现简易计算器:输入两个数、一个运算符,得到运算结果。
实现:
// 通过传统方式实现// 缺点:// 1、如果需要实现相同功能,代码需要重复编写,无法直接复用(如果有多组数据需要计算,如何优雅的实现呢?)// 2、代码冗余、可维护性、可读性差package mainimport "fmt"func main() {var (a, b, result float64expr string)fmt.Println("请输入数值a")fmt.Scan(&a)fmt.Println("请输入数值b")fmt.Scan(&b)fmt.Println("请输入计算符号")fmt.Scan(&expr)switch expr {case "+":result = a + bcase "-":result = a - bcase "*":result = a * bcase "/":result = a / bdefault:fmt.Println("输入的计算符号不支持")}fmt.Println("result=", result)}
// 通过函数的方式实现// 优点:// 1、可重复调用package mainimport "fmt"func cal(a float64, b float64, expr string) float64 {var result float64switch expr {case "+":result = a + bcase "-":result = a - bcase "*":result = a * bcase "/":result = a / bdefault:fmt.Println("输入的计算符号不支持")}return result}func main() {var (a, b, result float64expr 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 mainimport "fmt"func cal(a float64, b float64, expr string) float64 {var result float64switch expr {case "+":result = a + bcase "-":result = a - bcase "*":result = a * bcase "/":result = a / bdefault:fmt.Println("输入的计算符号不支持")}return result}func main() {var (a, b, result float64expr 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.gopackage utilsimport "fmt"// 这里定义Cal函数时,首字母须大写,使其称为可导出的函数,才能被其他包引用func Cal(a float64, b float64, expr string) float64 {var result float64switch expr {case "+":result = a + bcase "-":result = a - bcase "*":result = a * bcase "/":result = a / bdefault:fmt.Println("输入的计算符号不支持")}return result}
// main.gopackage mainimport ("fmt""learning/app/utils" // 路径从go module的位置开始写)func main() {var (a, b, result float64expr 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>### 调用过程- 在调用一个函数时,会给这个函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他栈空间区分开- 在每个函数对应的栈中,数据空间时独立的,不会混淆- 当一个函数调用完毕,程序会销毁这个函数对应的栈空间<a name="wxW9E"></a>### return语句<a name="z4Kn6"></a>#### 基本语法Go函数支持返回多个值,这一点是其他编程语言没有的<br />**func <函数名> (形参列表) (返回值类型列表) {<br /> <代码块>**<br />**return 返回值列表**<br />**}**- 如果返回多个值时,在接收时,希望忽略某个返回值,可以使用 **_** 进行占位忽略- 如果返回值只有一个,**(返回值类型列表) **可以不写**( )**<a name="AhYhc"></a>#### 案例演示编写一个函数计算两个数的和与差,并返回结果```gopackage mainimport "fmt"func cal(a, b float64) (sum, sub float64) {sum = a + bsub = a - breturn sum, sub}func main() {var a, b float64fmt.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 mainimport "fmt"func cal(a, b float64) (sum, sub float64) {sum = a + bsub = a - breturn sum, sub}func main() {var a, b float64fmt.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 mainimport "fmt"func test(n int) {if n > 2 {n--test(n)}fmt.Println("n=", n)}func main() {test(4)}/*test(4)4 > 2n=3test(3)3 > 2n = 2test(2)2 !> 2 此时if条件不满足,执行test(2)中的printn = 2Print n=2Print n=2 test(3) if语句执行完毕后,开始执行test(3)的printPrint n=3 test(4) if语句执行完毕后,开始执行test(4)的print因此程序运行结果是n= 2n= 2n= 3*/
package mainimport "fmt"func test2(n int) {if n > 2 {n--test2(n)} else {fmt.Println("n=", n)}}func main() {test2(4)}/*test2(4)4 > 2n = 3test2(3)3 > 2n = 2test2(2)2 !> 2n = 2print n=2test2(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 mainimport "fmt"func init() {fmt.Println("exec init func")}func main() {fmt.Println("exec main func")}/*从函数运行结果可以看出,init函数是先于main函数执行的函数运行结果展示:exec init funcexec 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 mainimport "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 mainimport ("fmt")var (Fvar = func(n1 int, n2 int) int {return n1 * n2})func main() {result := Fvar(10, 20)fmt.Println("result=", result)}
闭包
基本介绍
闭包就是一个函数 和 与其相关的引用环境 组合的一个整体
案例演示
// 演示Go中闭包的使用package mainimport "fmt"func AddUpper() func(int) int {var n int = 10return func(x int) int {n = n + xreturn n}}func main() {f := AddUpper()fmt.Println(f(1))fmt.Println(f(2))fmt.Println(f(3))}/*AddUpper是一个函数,返回的数据类型是func (int) int运行结果展示:111316*/
闭包的说明
- 返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包
- 当我们反复的调用函数f时,因为n仅初始化一次,因此每调用一次就进行累计
- 要搞清楚闭包,需要分析出返回的函数使用了哪些变量,因为函数和它所引用的变量共同构成了闭包
defer
为什么需要defer?
在函数中,程序员经常需要创建资源(例如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go语言提供了defer(延时机制)
入门案例
// 演示defer的用法package mainimport "fmt"func sum(n1, n2 int) int {defer fmt.Println("ok01 n1=", n1)defer fmt.Println("ok02 n2=", n2)res := n1 + n2fmt.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 mainimport "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 = 11strB len = 12strC len = 19strD len = 20可以看到,当字符串中含有中文时,通过len(str) 函数得到的结果与期望不一致这是因为len(str) 函数统计字符串长度是按照字节来统计的,而一个中文字符通常会占有3个字节golang的编码使用的是UTF-8,ascii码(字母和数字)的字符占用一个字节,汉子占用3个字节*/
- len([]rune(str))
先将字符串转为rune类型的切片,再使用len(str)函数,可用于统计含中文的字符串长度
package mainimport "fmt"func main() {strC := "你好 hello world!"fmt.Println("strC len =", len([]rune(strC)))}/*如上程序运行结果是:strC len = 15*/
字符串遍历
- for range
for range 可用于遍历数组、切片、字符串、map、通道
package mainimport "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]) } }
```
