【Go笔记】第 6 章 函数、包和错误处理
# 6.1 为什么需要函数
# 6.1.1请大家完成这样一个需求:
输入两个数,再输入一个运算符(+,-,* ,/),得到结果.。
# 6.1.2使用传统的方法解决
🏷 走代码
func main(){
//请大家完成这样一个需求:
//输入两个数,再输入一个运算符(+,-,*,),得到结果。
//分析思路..
var n1 float64 =1.2
var n2 float64 2.3
var operator byte ='-'
var res float64
switch operator {
case "+":
res n1 n2
case "-":
res n1-n2
case "*":
res n1 n2
case "/"
res n1 n2
default:
fmt.Print1n("操作符号错误..")
}
fmt.Printin("res=",res)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
🏷 分析一下上面代码问题
- 上面的写法是可以完成功能, 但是代码冗余
- 同时不利于代码维护
- 函数可以解决这个问题
# 6.2 函数的基本概念
为完成某一功能的程序指令(语句)的集合,称为函数。 在 Go 中,函数分为: 自定义函数、 系统函数(查看 Go 编程手册)
# 6.3 函数的基本语法
func 函数名 (形参列表)(返回值列表){ 执行语句 return 返回值列表 }
- 形参列表:表示函数的输入
- 函数中的语句:表示为了实现某一功能代码块
- 函数可以有返回值也可以没有
# 6.4 快速入门案例
使用函数解决前面的计算问题。 走代码:
func TestFuncTest1(t *testing.T) {
cal := cal(1,2,'+')
fmt.Println(cal)
}
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 6.5 包的引出
- 在实际的开发中, 我们往往需要在不同的文件中, 去调用其它文件的定义的函数, 比如 main.go 中, 去使用 utils.go 文件中的函数, 如何实现? -> 包
- 现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang 也想定义函数也叫 Cal。 两个程序员为此还吵了起来,怎么办? -> 包
# 6.6 包的原理图
包的本质实际上就是创建不同的文件夹,来存放程序文件。画图说明一下包的原理
# 6.7 包的基本概念
说明: go 的每一个文件都是属于一个包的, 也就是说 go 是以包的形式来管理文件和项目目录结构的
# 6.8 包的三大作用
区分相同名字的函数、 变量等标识符 当程序文件很多时,可以很好的管理项目 控制函数、 变量等访问范围, 即作用域
# 6.9 包的相关说明
🏷 打包基本语法 package 包名
🏷 引入包的基本语法 import "包的路径"
# 6.10 包使用的快速入门
包快速入门-Go 相互调用函数, 我们将 func Cal 定义到文件 utils.go , 将 utils.go 放到一个包中, 当其它文件需要使用到 utils.go 的方法时, 可以 import 该包, 就可以使用了. 【为演示: 新建项目目录结构】
package main
import (
"../utils"
"fmt"
)
func main() {
var n1 float64 = 1.2
var n2 float64 = 1.3
var operator byte = '+'
res := utils.Cal(n1,n2,operator)
fmt.Println(res)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 6.11 包使用的注意事项和细节讨论
在给一个文件打包时, 该包对应一个文件夹, 比如这里的 utils 文件夹对应的包名就是 utils,文件的包名通常和文件所在的文件夹名一致, 一般为小写字母。
当一个文件要使用其它包函数或变量时, 需要先引入对应的包
引入方式 1: import "包名"
引入方式 2:
import (
"包名"
"包名"
)
2
3
4
package 指令在 文件第一行, 然后是 import 指令。在 import 包时, 路径从 $GOPATH 的 src 下开始, 不用带 src , 编译器会自动从 src 下开始引入
- 为了让其它包的文件, 可以访问到本包的函数, 则该函数名的首字母需要大写, 类似其它语言的 public ,这样才能跨包访问。 比如 utils.go 的
在访问其它包函数, 变量时, 其语法是 包名.函数名, 比如这里的 main.go 文件中
如果包名较长, Go 支持给包取别名, 注意细节: 取别名后, 原来的包名就不能使用了
说明: 如果给包取了别名, 则需要使用别名来访问该包的函数和变量。
在同一包下, 不能有相同的函数名(也不能有相同的全局变量名) , 否则报重复定义
如果你要编译成一个可执行程序文件, 就需要将这个包声明为 main , 即 package main .这个就是一个语法规范, 如果你是写一个库 , 包名可以自定义
# 6.12 函数的调用机制
# 6.12.1 通俗易懂的方式的理解
# 6.12.2 函数-调用过程
介绍: 为了让大家更好的理解函数调用过程, 看两个案例, 并画出示意图, 这个很重要
- 传入一个数+1
- 在调用一个函数时, 会给该函数分配一个新的空间, 编译器会通过自身的处理让这个新的空间和其它的栈的空间区分开来
- 在每个函数对应的栈中, 数据空间是独立的, 不会混淆
- 当一个函数调用完毕(执行完毕)后, 程序会销毁这个函数对应的栈空间。
计算两个数,并返回
func main() {
n1 := 10 //定义
//调用
test(n1)
fmt.Println("main() n1 =",n1) //?调用输出什么结果
var sum int = getSum(1,2)
fmt.Println("main() sum=",sum)
}
func test(n1 int) {
n1 = n1 +1
fmt.Println("test() n1 =",n1) //?调用输出什么结果
}
func getSum(n1 int,n2 int) int {
return n1 + n2
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6.12.3 return 语句
🏷 基本语法和说明
- 如果范湖多个值时,在接收时,希望忽略某个返回值,则使用"_ " 符号标识占位符忽略
- 如果返回值只有一个,(返回值类型列表)可以不写()
🏷 案例演示 1 请编写要给函数,可以计算两个数的 和和 差,并返回结果。
func
main(){
n1:=10
//调用test
test(n1)
fmt.Print1n("main()n1=",n1)//?输出结果=?
sum :getsum(10,20)
fmt.Println("main sum "sum)//30
//调用getsumAndsub
res1,res2 :getsumAndsub(1,2)//resl 3 res2 =-1
fmt.Printf("resi=%v res2=%v\n",resl,res2)
}
//请编写要给函数,可以计算两个数的和和差,并返回结果
func getsumAndsub(n1 int,n2 int)(int,int){
sum :n1 n2
sub :n1 n2
return sum,sub
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
🏷 案例演示 2 一个细节说明: 希望忽略某个返回值,则使用 _ 符号表示占位忽略
//希望忽略某个返回值,则使用符号表示占位忽略
_,res3 = getsumAndsub(3,9)
fmt.Println("res3=",res3)
2
3
4
# 6.13 函数的递归调用
# 6.13.1 基本介绍
一个函数在函数体内又调用了本身,我们称为递归调用(自己调用自己)
6.13.2 递归调用快速入门 🏷 代码 1
//rec1(4)
//4 > 2 true 4-1 3
//3 > 2 true 3-1 2
//2 > 2 false 2 2 3
func rec1(n int) {
if n > 2{
n-- //3
rec1(n) //3
}
fmt.Println("n=",n) //2 2 3
}
//rec2(4)
//4 > 2 true 4-1
//3 > 2 true 3-1
//2 > 2 false 2
func rec2(n int) {
if n > 2 {
n--
rec2(n)
}else{
fmt.Println("n=",n) //2
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
上面代码的分析图:
🏷 代码 2
func test2(n int){
if n>2{
n--
test2(n)
}else{
fmt.Println("n=",n)
}
}
func main(){
//看一段代码
//test(4) // ?通过分析来看下递归调用的特点
test2(4) //?通过分折来看下递归调用的特点
}
2
3
4
5
6
7
8
9
10
11
12
13
14
对上面代码分析的示意图:
6.13.3 递归调用的总结
- 执行一个函数时, 就创建一个新的受保护的独立空间(新函数栈)
- 函数的局部变量是独立的, 不会相互影响
- 递归必须向退出递归的条件逼近, 否则就是无限递归, 死龟了:)
- 当一个函数执行完毕, 或者遇到 return, 就会返回, 遵守谁调用, 就将结果返
- 回给谁, 同时当函数执行完毕或者返回时, 该函数本身也会被系统销毁
# 6.14 函数使用的注意事项和细节讨论
- 函数的形参列表可以是多个,返回值列表也可以是多个。
- 形参列表和返回值列表的数据类型可以是值类型和引用类型。
- 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 privat
- 函数中的变量是局部的,函数外不生效【 案例说明】
//函数中的变量是局部的,函数外不生效
func test(){
//n1是test函数的局部变量,只能在test函数中使用
var n1 int = 10
}
func main(){
//这里不能使用n1,因为n1是test函数的局部变量
//fmt.Printin("ni=",n1)
}
2
3
4
5
6
7
8
9
10
- 基本数据类型和 数组默认都是 值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
func teste2(n1 int){
n1 = n1 + 10
fmt.Println("teste2()ni="n1)
}
func main(){
num := 20
test02(num)
fmt.Println("main()num=",num)
}
2
3
4
5
6
7
8
9
10
- 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。
- Go 函数不支持函数重载
- 在 Go 中, 函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该 变量可以对函数调用
//在G0中,函数也是一种数据类型,
//可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
func getSum(n1 int,n2 int)int {
return n1 + n2
}
func main(){
a := getSum
fmt.Printf("a的类型灯,get5um类型是Tn",a,getSum)
res:= a(10,40)//等价res:=getSum(10,40)
fmt.Printin("res=",res)
}
2
3
4
5
6
7
8
9
10
11
12
13
- 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
//看案例
res2 := myFun(getSum,50,60)
fmt.Println("res2=",res2)
//函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用
func myFun(funvar func(int,int)int,num1 int ,num2 int) int{
return funvar(num1,num2)
}
2
3
4
5
6
7
8
为了简化数据类型定义,Go 支持自定义数据类型 基本语法:type 自定义数据类型名 数据类型 // 理解: 相当于一个别名 案例:type myInt int // 这时 myInt 就等价 int 来使用了.
// 这时 mySum 就等价 一个 函数类型 func (int, int) int 案例:type mySum func (int, int) int
举例说明自定义数据类型的使用:
//给int取了别名,在go中myInt和int虽然都是int类型,但是go认为myInt和int两个类型
type myInt int
var numl1 myInt
var num2 int
num1 = 40
num2 = int(numl) //各位,注意这里依然需要显示转换,go认为nyInt和int两个类型
fmt.Println("numi=",numl,"num2=",num2)
//-------------------------------------------------------
//再加一个案例
//这时myFun就是func(int,int)int类型
type myFunType func(int,int)int
//函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用
func myFun2(funvar myFunType,num1 int,num2 int )int{
return funvar(num1,num2)
}
// 看案例
res3 := myFun2(getSum,500,600)
fmt.Println("res3=",res3)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 支持对函数返回值命名
//支持对函数返回值命名
func getsumAndsub(n1 int,n2 int)(sum int,sub int){
sub n1 - n2
sum n1 + n2
return
}
//看案例
al,b1 :getsumAndsub(1,2)
fmt.Printf("a=%v b=%vIn",al,b1)
2
3
4
5
6
7
8
9
- 使用 _ 标识符,忽略返回值
func cal(n1 int,n2 int)(sum int,sub int){
sum = n1+n2
sub = n1-n2
return
}
func main()
res1,_ := cal(10,20)
fmt.Printf("res1=%d "res1)
}
2
3
4
5
6
7
8
9
- Go 支持可变参数
//支持0到多个参数
func sum(args...int)sum int{
}
//支持1到多个参数
func sum(n1 int,args...int)sum int{
}
2
3
4
5
6
7
8
说明:
(1)args是slice切片,通过args[index)]
可以访问到各个值。
(2)案例演示:编写一个函数sum,可以求出1到多个int的和
(3)如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后。
代码演示:
//案例演示:编写一个函数sum,可以求出 1到多个int的和
//可以参数的使用
func sum(n1 int,args...int)int {
sum := n1
//遍历args
for i :=0;i<len(args);i++{
sum+=args[i]//args[o]表示取出args切片的第一个元素值,其它依次类推
}
return sum
}
//测试一下可变参数的使用
res4 := sum(10,0,-1,90,10,100)
fmt.Println("res4=",res4)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.16 init 函数
# 6.16.1 基本介绍
每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用。
# 6.16.2 案例说明:
package main
import "fmt"
//init函数,通常可以在init函数中完成初始化工作
func init(){
fmt.Println("init()...")
}
func main(){
fmt.Println("main()...")
}
//输出的结果是:
init()...
main()...
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.16.3 inti 函数的注意事项和细节
- 如果一个文件同时包含 全局变量定义,init 函数和 main 函数,则执行的流程 全局变量定义->init函数->main 函数
package main
import "fmt"
var age test()
//为了看到全局变量是先被初始化的,我们这里先写函数
func test()int {
fmt.Println("test()")//1
return 90
}
//init函数,通常可以在init函数中完成初始化工作
func init(){
fmt.Println("init()...")//2
}
func main(){
fmt.Println("main()...age=",age)//3
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- init 函数最主要的作用,就是完成一些 初始化的工作,比如下面的案例
package main
import (
"fmt"
//引入包
go_code/chapter06/funcinit/utils"
)
var age test()
//为了看到全局变量是先被初始化的,我们这里先写函数
func test() int {
fmt.Println("test()")//1
return 90
}
//init函数,通常可以在init函数中完成初始化工作
func init(){
fmt.Println("init()...")//2
func main(){
fmt.Println("main()...age=",age)//3
fmt.Println("Age=",utils.Age,"Name=",utils.Name)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 细节说明: 面试题:案例如果 main.go 和 utils.go 都含有 变量定义,init 函数时,执行的流程又是怎么样的呢?
# 6.17 匿名函数
# 6.17.1 介绍
Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
# 6.17.2 匿名函数使用方式 1
在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。 【案例演示】
func main()
//在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
//案例演示,求两个数的和,使用匿名函数的方式完成
res1 := func (n1 int,n2 int)int{
return n1 + n2
}(10,28)
fmt.Println("res1=",resl)
2
3
4
5
6
7
# 6.17.3 匿名函数使用方式 2
将 匿名函数赋给一个变量(函数变量),再通过该数 变量来调用匿名函数 【案例演示】
//将匿名函数func(n1int,n2int)int赋给a变量
//则a的数据类型就是函数类型,此时,我们可以通过:完成调用
a := func (nl int,n2 int)int {
return n1 - n2
}
res2 := a(10,30)
fmt.Println("res2=",res2)
res3 := a(90,30)
fmt.Println("res3=",res3)
2
3
4
5
6
7
8
9
10
6.17.4 全局匿名函数 如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
var (
//fun1就是一个全局匿名函数
Fun1 func (n1 int,n2 int)int {
return n1 * n2
}
)
//全局匿名函数的使用
res4 := Fun1(4,9)】
fmt.Println("res4=",res4)
2
3
4
5
6
7
8
9
10
# 6.18 闭包
# 6.18.1 介绍
基本介绍:闭包就是 一个函数和与 其相关的引用环境组合的 一个整体(实体)
# 6.18.2 案例演示:
//累加器
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))//11
fmt.Println(f(2))//13
fmt.Println(f(3))//16
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🏷 对上面代码的说明和总结
- AddUpper 是一个函数,返回的数据类型是 fun (int) int
- 闭包的说明
var n int = 10
return func (x int) int {
n = n + x
return n
}
2
3
4
5
返回的是一个匿名函数, 但是这个匿名函数引用到函数外的 n ,因此这个匿名函数就和 n 形成一个整体,构成闭包。 3) 大家可以这样理解: 闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。 4) 当我们反复的调用 f 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。 5) 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。 6) 对上面代码的一个修改,加深对闭包的理解
//累加器
func Addupper()func (int) int {
var n int 10
var str "hello"
return func (x int)int {
n = n + x
str += string(36) //=>36='$
fmt.Println("str=",str)//1.str="hellos"2.str="helloss"3.str="hellosss"
return n
}
}
func main(){
//使用前面的代码
f := Addupper()
fmt.Println(f(1))//11
fmt.Println(f(2))//13
fmt.Println(f(3))//16
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 6.18.3 闭包的最佳实践
🏷 请编写一个程序,具体要求如下
- 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
- 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。
- 要求使用闭包的方式完成
- strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。
代码:
package main
import (
"fmt"
"strings")
//1)编写一个函数makeSuffix(suffix string)可以接收一个文件后缀名(比如.jpg),并返回一个闭包
//2)调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jPg),则返回文件名.jPg,
//3)要求使用闭包的方式完成
//4)strings.HasSuffix,该函数可以判断某个字符串是否有指定的后缀。
func makesuffix(suffix string) func(string) string {
return func(name string) string {
//如果name没有指定后缀,则加上,否则就返回原来的名字
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
//测试makeSuffix的使用
//返回一个闭包
f2 := makesuffix(".jpg")
fmt.Println("文件名处理后=", f2("winter")) // winter.jgP
fmt.Println("文件名处理后=", f2("bird.jPg")) //bird.jpg
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
🏷 上面代码的总结和说明:
- 返回的匿名函数和 makeSuffix (suffix string) 的 suffix 变量 组合成一个闭包,因为 返回的函数引用到 suffix 这个变量
- 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入 后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。大家可以仔细的体会一把!
# 6.19 函数的 defer
# 6.19.1 为什么需要 defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在 函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。
# 6.19.2 快速入门案例
package main
import "fmt"
func sum(n1 int, n2 int) int {
//当执行到deferl时,暂时不执行,会将defer)后面的语句压入到独立的栈(defer栈)
//当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
defer fmt.Println("okl n1=", n1) //defer 3.okl nl 10
defer fmt.Println("ok2 n2=", n2) //defer 2.ok2 n2=20
res := n1 + n2 //res 30
fmt.Println("ok3 res=", res) //1.ok3 res=30
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res=", res) //4.res=30
}
//执行
//ok3 res = 30
//ok2 n2 = 20
//ok1 n1 = 10
//res = 30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 6.19.3 defer 的注意事项和细节
- 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈
中
[我为了讲课方便,暂时称该栈为 defer 栈]
, 然后继续执行函数下一个语句。 - 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制),所以同学们看到前面案例输出的顺序。
- 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。 请看一段代码:
package main
import "fmt"
func sum(n1 int, n2 int) int {
//当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈)
//当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
defer fmt.Println("okl ni=", n1) //defer 3.okl n1 10
defer fmt.Println("ok2 n2=", n2) //defer 2.ok2 n2=20
//增加一句话
n1++ //n1=11
n2++ //n2=21
res := n1 + n2 //res 32
fmt.Println("ok3 res=", res) //1.ok3 res=32
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res=", res) //4.res=32
}
//输出
//ok3 res = 32
//ok2 n2 = 20
//ok1 n1 = 10
//res = 32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 6.19.4 defer 的最佳实践
defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。看下 模拟代码。
func test(){
//关闭文件资源
fi1e=openfi1e(文件名)
defer file.close()
//其它代码
}
func test(){
//释放数据库资源
connect openDatabse()
defer connect.close()
//其它代码
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
说明:
- 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.Close() defer connect.Close()
- 在 defer 后,可以继续使用创建资源.
- 当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源.
- 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。
# 6.20 函数参数传递方式
# 6.20.1 基本介绍
我们在讲解函数注意事项和使用细节时,已经讲过值类型和引用类型了,这里我们再系统总结一下,因为这是重难点,值类型参数默认就是值传递,而引用类型参数默认就是引用传递。
# 6.20.2 两种传递方式
- 值传递
- 引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
# 6.20.3 值类型和引用类型
- 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
- 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型
# 6.20.4 值传递和引用传递使用特点
- 值类型默认是值传递:变量直接存储值,内存通常在栈中分配
2. 引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才是真正存储数据(值),内存通常在堆上分配,当没有任务变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收。
- 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。这个案例在前面详解函数使用注意事项的
# 6.21 变量作用域
- 函数内部声明/定义的变量叫局部变量, 作用域仅限于函数内部
//函数
func test(){
//age和Name的作用域就只在test函数内部
age := 10
Name := "tom~"
}
func main(){
}
2
3
4
5
6
7
8
9
10
- 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
package main
import (
"fmt"
)
//函数外部声明/定义的变量叫全局变量,
//作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
var age int = 50
var Name string = "jack~"
//函数
func test() {
//age和Name的作用域就只在test函数内部
age := 10
Name := "tom~"
fmt.Println("age=", age) //10
fmt.Println("Name=", Name) //tom~
}
func main() {
fmt.Println("age=", age) //50
fmt.Println("Name=", Name) //jack~
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 如果变量是在一个代码块,比如 for / if 中,那么这个变量的的作用域就在该代码块
package main
import "fmt"
func main() {
//如果变量是在一个代码块,比如for/1f中,那么这个变量的的作用域就在该代码块
for i := 0; i <= 10; i++ {
fmt.Println("i=", i)
}
var i int //局部变量
for i = 8; i <= 10; i++ {
fmt.Println("i=", i)
}
fmt.Println("i=", i)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 6.21.1 变量作用域的课堂练习
package main
import "fmt"
var name = "tom" //全局变量
func test01() {
fmt.Println(name) //tomtom
}
func teste2() { //编译器采用就近原则
name := "jack"
fmt.Println(name) //jack
}
func main() {
fmt.Println(name) //tom
test01() //tom
teste2() //jack
test01() //tom
}
//输出的结果是: tom tom jack tom
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
6.22 函数课堂练习(综合)
- 函数可以没有返回值案例,编写一个函数,从终端输入一个整数打印出对应的金子塔
分析思路:就是将原来写的打印金字塔的案例,使用函数的方式封装,在需要打印时,直接调用 即可。
package main
import "fmt"
//将打印金字塔的代码封装到函数中
func printPyramid(totalLevel int) {
//表示层数
for i := 1; i <= totalLevel; i++ {
//在打印*前先打印空格
for k := 1; k <= totalLevel-i; k++ {
fmt.Print(" ")
}
//行表示每层打印多少*
for j := 1; j <= 2*1-1; j++ {
fmt.Print("*")
}
fmt.Println()
}
}
func main() {
//调用printPyramid函数,就可以打印金字塔
//从终端输入一个整数打印出对应的金子塔
var n int
fmt.Println("请输入打印金字塔的层数")
fmt.Scanln(&n)
printPyramid(n)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- 编写一个函数,从终端输入一个整数(1—9),打印出对应的乘法表
分析思路:就是将原来写的调用九九乘法表的案例,使用函数的方式封装,在需要打印时,直接调用即可 代码:
package main
import "fmt"
//编写一个函数调用九九乘法表
func printMulti(num int) {
//打印出九九乘法表
//表示层数
for i := 1; i <= num; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%v * %v = %v \t", j, i, j*i)
}
fmt.Println()
}
}
func main() {
//从终端输入一个整数表示要打印的乘法表对应的数
var num int
fmt.Println("请输入九九乘法表的对应数")
fmt.Scanln(&num)
printMulti(num)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 编写函数,对给定的一个二维数组(3×3)转置,这个题讲数组的时候再完成
6.23 字符串常用的系统函数
说明:字符串在我们程序开发中,使用的是非常多的,常用的函数需要同学们掌握[带看手册或者官方编程指南]
:
- 统计字符串的长度,按字节 len(str)
package main
import (
"fmt"
)
func main() {
//统计字符串的长度,按字节len(str)
//golang的编码统一为utf-8(asciil的字符(字母和数字)占一个字节,汉字占用3个字节)
str := "hello北"
fmt.Println("str len=", len(str)) //8
}
2
3
4
5
6
7
8
9
10
11
12
- 字符串遍历,同时处理有中文的问题
r := []rune(str)
package main
import (
"fmt"
)
func main() {
str2 := "hello北京"
//字符串遍历,同事处理有中文的问题 r := []rune(str) r := []rune(str2)
for i := 0; i < len(r); i++ {
fmt.Printf("字符=%c\n", r[i])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
- 字符串转整数: n, err := strconv.Atoi("12")
package main
import (
"fmt"
"strconv")
func main() {
//字符串转整数:n ,err := strconv.Atoi("12")
n, err := strconv.Atoi("hello")
if err != nil {
fmt.Println("转换错误", err)
} else {
fmt.Println("转成的结果是", n)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 整数转字符串 str = strconv.Itoa(12345)
func main() {
str := strconv.Itoa(1234)
fmt.Printf("str=%v,str=%T", str, str)
}
2
3
4
- 字符串 转
[]byte: var bytes = []byte("hello go")
package main
import (
"fmt"
)
func main() {
//字符串 转 `[]byte: var bytes = []byte("hello go") var bytes = []byte("hello,go")
fmt.Printf("bytes=%v\n", bytes)
}
2
3
4
5
6
7
8
9
10
[]byte 转 字符串: str = string([]byte{97, 98, 99})
package main
import (
"fmt"
)
func main() {
//6)[]byte转字符串:str=string([]byte{97,98,99)
str := string([]byte{97, 98, 99})
fmt.Printf("str=%v\n", str)
}
2
3
4
5
6
7
8
9
10
11
- 10 进制转 2, 8, 16 进制: str = strconv.FormatInt(123, 2) // 2-> 8 , 16
package main
import (
"fmt"
"strconv")
func main() {
//10进制转2,8,16进制:str=strconv.FormatInt(123,2),返回对应的字符串
str := strconv.FormatInt(123, 2)
fmt.Printf("123对应的二进制是=%v\n", str)
str = strconv.FormatInt(123, 16)
fmt.Printf("123对应的16进制是=%v\n", str)
}
2
3
4
5
6
7
8
9
10
11
12
13
- 查找子串是否在指定的字符串中: strings.Contains("seafood", "foo") //true
package main
import (
"fmt"
"strings")
func main() {
//查找子串是向在指定的字符串中 strings.Contains("seafood", "foo") //true
b := strings.Contains("seafood", "mary")
fmt.Printf("b=%v\n", b)
}
2
3
4
5
6
7
8
9
10
11
- 统计一个字符串有几个指定的子串 : strings.Count("ceheese", "e") //4
package main
import (
"fmt"
"strings")
func main() {
//统计一个字符串有几个指定的子串:strings.Count("ceheese","e")/4
num := strings.Count("ceheese", "e")
fmt.Printf("num=%v\n", num)
}
2
3
4
5
6
7
8
9
10
11
- 不区分大小写的字符串比较
(==是区分字母大小写的): fmt.Println(strings.EqualFold("abc","Abc")) // true
package main
import (
"fmt"
"strings")
func main() {
b := strings.EqualFold("abc", "Abc")
fmt.Printf("b=%v\n", b) //true
fmt.Println("结果", "abc" == "Abc") //fa1se//区分字母大小写
}
2
3
4
5
6
7
8
9
10
11
12
- 返回子串在字符串第一次出现的 index 值,如果没有返回-1 : strings.Index("NLT_abc", "abc") // 4
package main
import (
"fmt"
"strings")
func main() {
//strings.Index("NLT_abc","abc")//4
index := strings.Index("NLT_abcabcabc", "abc") //4
fmt.Printf("index=%v\n", index)
}
2
3
4
5
6
7
8
9
10
11
- 返回子串在字符串最后一次出现的 index,如没有返回-1 : strings.LastIndex("go golang", "go")
package main
import (
"fmt"
"strings")
func main() {
//12)返回子串在字符串最后一次出现的index,
//如没有返回-1:strings.LastIndex("go golang”,"go")
index := strings.LastIndex("go golang", "go") //3
fmt.Printf("index=%v\n", index)
}
2
3
4
5
6
7
8
9
10
11
12
- 将指定的子串替换成 另外一个子串: strings.Replace("go go hello", "go", "go 语言", n) n 可以指定你希望替换几个,如果 n=-1 表示全部替换
package main
import (
"fmt"
"strings")
func main() {
str2 := "gogo hello"
str := strings.Replace(str2, "go", "北京", -1)
fmt.Printf("str=%v str2=%v\n", str, str2)
}
2
3
4
5
6
7
8
9
10
11
- 按 照 指 定 的 某 个 字 符 , 为 分 割 标 识 , 将 一 个 字 符 串 拆 分 成 字 符 串 数 组 :strings.Split("hello,wrold,ok", ",")
package main
import (
"fmt"
"strings")
func main() {
//按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组:
//strings.Split("hello,wrold,ok",","
strArr := strings.Split("hello,wrold,ok", ",")
for i := 0; i < len(strArr); i++ {
fmt.Printf("str[%v]=%v\n", i, strArr[i])
}
fmt.Printf("strArr=%v\n", strArr)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 将字符串的字母进行大小写的转换: strings.ToLower("Go") // go strings.ToUpper("Go") // GO
package main
import (
"fmt"
"strings")
func main() {
//15)将字符串的字母进行大小写的转换:
//strings.ToLower("Go")//go strings.Toupper("Go")//GO
str := "goLang Hello"
str = strings.ToLower(str)
str = strings.ToUpper(str)
fmt.Printf("str=%v\n", str) //golang hello
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 将字符串左右两边的空格去掉: strings.TrimSpace(" tn a lone gopher ntrn ")
package main
import (
"fmt"
"strings")
func main() {
//将字符串左右两边的空格去掉:strings.TrimSpace(”tn a lone gopher ntrn")
str := strings.TrimSpace("tn a lone gopher ntrn")
fmt.Printf("str=%s\n", str)
}
2
3
4
5
6
7
8
9
10
11
- 将字符串左右两边指定的字符去掉 : strings.Trim("! hello! ", " !") //
["hello"]
//将左右两边 ! 和 " "去掉
package main
import (
"fmt"
"strings")
func main() {
//17)将字符串左右两边指定的字符去掉
//strings,Trim("Ihe11o!","I")/["he1lo"]/将左右两边!和""去掉
str := strings.Trim("!hel1lo!", "I")
fmt.Printf("str=%q\n", str)
}
2
3
4
5
6
7
8
9
10
11
12
13
将字符串左边指定的字符去掉 : strings.TrimLeft("! hello! ", " !") //
["hello"]
//将左边 ! 和 ""去掉将字符串右边指定的字符去掉 :strings.TrimRight("! hello! ", " !") //
["hello"]
//将右边 ! 和 " "去掉判断字符串是否以指定的字符串开头: strings.HasPrefix("
ftp://192.168.10.1
", "ftp") // true
package main
import (
"fmt"
"strings")
func main() {
//20)判断字符串是否以指定的字符串开头:
//strings.HasPrefix("ftp://192.168.10.1","ftp")//true
b := strings.HasPrefix("ftp://192.168.10.1", "hsp") //true
fmt.Printf("b=%v\n", b)
}
2
3
4
5
6
7
8
9
10
11
12
- 判断字符串是否以指定的字符串结束: strings.HasSuffix("NLT_abc.jpg", "abc") //false
# 6.24 时间和日期相关函数
# 6.24.1 基本的介绍
说明:在编程中,程序员会经常使用到日期相关的函数,比如:统计某段代码执行花费的时间等 等。
- 时间和日期相关函数,需要导入 time 包
- time.Time 类型,用于表示时间
func main(){
//看看日期和时间相关函数和方法使用
//1. 获取当前时间
now := time.Now()
fmt.Printf("now=%v now type=%T",now,now)
//now = xxxx
}
2
3
4
5
6
7
8
9
- 如何获取到其它的日期信息
package main
import (
"fmt"
"time"
)
func main() {
//看看日期和时间相关函数和方法使用
//1,获取当前时间
now := time.Now()
fmt.Printf("now=%v now type=%T\n", now, now)
//2.通过now可以获取到年月日,时分秒
fmt.Printf("年=%v\n", now.Year())
fmt.Printf("月=%v\n", now.Month())
fmt.Printf("月=%v\n", int(now.Month()))
fmt.Printf("日=%v\n", now.Day)
fmt.Printf("时=%v\n", now.Hour())
fmt.Printf("分=%v\n", now.Minute())
fmt.Printf("秒=%v\n", now.Second())
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 格式化日期时间 方式 1: 就是使用 Printf 或者 SPrintf
方式二: 使用 time.Format() 方法完成:
//格式化日期时间的第二种方式
fmt.Printf(now.Format("2006-01-02 15:04:05"))
fmt.Println()
fmt.Printf(now.Format("2006-01-02"))
fmt.Println()
fmt.Printf(now.Format("15:04:05"))
fmt.Println()
2
3
4
5
6
7
对上面代码的说明: "2006/01/02 15:04:05" 这个字符串的各个数字是固定的,必须是这样写。 "2006/01/02 15:04:05" 这个字符串各个数字可以自由的组合,这样可以按程序需求来返回时间和日期
- 时间的常量
const (
Nanosecond Duration = 1 //纳秒
Microsecond = 1000 * Nanosecond //微秒
Millisecond = 1000 * Microsecond //毫秒
Second = 1000 * Millisecond //秒
Minute = 60 * Second //分钟
Hour = 60 * Minute //小时
)
2
3
4
5
6
7
8
常量的作用: 在程序中可用于获取指定时间单位的时间,比如想得到 100 毫秒 100 * time. Millisecond 6) 结合 Sleep 来使用一下时间常量
package main
import (
"fmt"
"time")
func main() {
//需求,每隔1秒中打印一个数字,打印到1©阳时就退出
//需求2:每隔0.1秒中打印一个数字,打印到100时就退出
i := 0
for {
i++
fmt.Println(i)
//休眠
//time.sleep(time.second)
time.Sleep(time.Millisecond * 100)
if i == 100 {
break
}
}}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- time 的 Unix 和 UnixNano 的方法
# 6.24.2 时间和日期的课堂练习
编写一段代码来统计 函数 test03 执行的时间
package main
import (
"fmt"
"strconv"
"time"
)
func test03() {
str := ""
for i := 0; i < 100000; i++ {
str += "hello" + strconv.Itoa(i)
}
}
func main() {
//在执行test03前,先获取到当前的unix时间截
start := time.Now().Unix()
test03()
end := time.Now().Unix()
fmt.Printf("执行test03()耗费时间为%v秒n", end-start)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 6.25 内置函数
# 6.25.1 说明:
Golang 设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们称为 Go 的内置函数。文档:https://studygolang.com/pkgdoc -> builtin
- len:用来求长度,比如 string、array、slice、map、channel
- new:用来分配内存,主要用来分配值类型,比如 int、float32,struct...返回的是指针 举例说明 new 的使用:
func main() {
num1 := 100
fmt.Printf("num1的类型%T,num1的值=%w,num1的地址%w八n", num1, num1, &num1)
num2 := new(int) //*int
//num2的类型%T=>*int
//num2的值=地址8xc04204c098(这个地址是系统分配)
//num2的地址%w=地址 0xc04206a020 (这个地址是系统分配)
//num2指向的值=180
*num2 = 100
fmt.Printf("num2的类型%T,num2的值=%v,num2的地址%w八nnum2这个指针,指向的值=%v",
num2, num2, &num2, *num2)
}
2
3
4
5
6
7
8
9
10
11
12
上面代码对应的内存分析图:
- make:用来 分配内存,主要用来 分配引用类型,比如 channel、map、slice。这个我们后面讲解。
# 6.26 错误处理
# 6.26.1 看一段代码,因此错误处理
package main
import (
"fmt"
)
func test() {
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}
func main() {
//测试
test()
fmt.Println("main()下面的代码..")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
对上面代码的总结
- 在默认情况下,当发生错误后(panic) ,程序就会退出(崩溃.)
- 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
- 这里引出我们要将的错误处理机制
# 6.26.2 基本说明
- Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try…catch…finally 这种处理。
- Go 中引入的处理方式为:defer, panic, recover
- 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理
# 6.26.3 使用 defer+recover 来处理错误
package main
import(
"fmt”
"time"
)
func test(){
//使用defer+recover来捕获和处理异常
defer func(){
err:=recover()//recover()内置函数,可以捕获到异常
if err=ni1{//说明捕获到错误
fmt.Println("err=",err)
}()
num1 :=10
num2 :=0
res := num1 / num2
fmt.Println("res=",res)
}
func main(){
//测试
test()
for {
fmt.Println("main()下面的代码...")
time.sleep(time.Second)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 6.26.4 错误处理的好处
进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮。看一个案例演示:
func test() {
//使用defer+recover 来捕获和处理异常
defer func() {
err := recover() //recover()内置函数,可以捕获到异常
if err != nil { //说明捕获到错误
fmt.Println("err=", err)
//这里就可以将错误信息发送给管理员.·.:
fmt.Println("发送邮件给admin@sohu.com~")
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6.26.5 自定义错误
# 6.26.6 自定义错误的介绍
Go 程序中,也支持自定义错误, 使用 errors.New 和 panic 内置函数。
- errors.New("错误说明") , 会返回一个 error 类型的值,表示一个错误
- panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类 型的变量, 输出错误信息, 并退出程序.
# 6.26.7 案例说明
package main
import (
"errors"
"fmt")
//函数去读取以配置文件init.conf的信息
//如果文件名传入不正确,我们就返回一个自定义的错误
func readconf(name string) (err error) {
if name == "config.ini" {
//读取..,
return nil
} else {
//返回一个目定义错碳
return errors.New("读取文件错误..")
}
}
func test02() {
err := readconf("config2.ini")
if err != nil {
//如果读取文件发送错误,就输出这个错误,并终止程序
panic(err)
}
fmt.Println("test02()继续执行..:.")
}
func main() {
//测试
//test()
//for{ //10 //fmt.PrintIn("main()下面的代码..")
//time.sleep(time.Second) //}
//测试自定义错误的使用
test02()
fmt.Println("main()下面的代码..")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37