6.1 函数
6.1.1 函数基本介绍
为完成某一功能的程序指令(语句)的集合,称之为函数
在Go中,函数分为:自定义函数、系统函数(查看Go编程手册)
6.1.2 基本语法
func 函数名 (形参列表) (返回值类型列表) {
执行语句...
return 返回值列表
}
形参列表:表示函数的输入
函数中的语句:表示为了实现某一功能代码块
函数可以有返回值,也可以没有
6.1.3 案例入门
定义函数
引用函数
6.2 包
6.2.1 包的引入
一个需求
- 在实际的开发中,我们往往需要在不同的文件中,去调用其他文件定义的函数,比如:main.go中,去使用utils.go文件中的函数,如何实现?
- 现在两个程序员共同开发一个Go项目,程序员xiaoming希望定义函数Cal,程序员xiaoqing也想定义函数也叫Cal。两个程序员为此还吵了起来,怎么办?
6.2.2 包的原理图
包的本质实际上就是创建不同的文件夹,来存放程序文件。
6.2.3 包的基本概念
go中的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的
6.2.4 包的作用
包的三大作用
- 区分相同名字的函数,变量等标识符
- 当程序文件很多时,可以很好的管理项目
- 控制函数,变量等访问范围,即作用域
包的相关说明
- 打包基本语法
- package util
- 引入包的基本语法
- import “包的路径”
6.2.5 包使用的快速入门—分解
整体项目管理,目录结构
utils.go 文件内容 【也就是自己定义了一个名叫:utils.go的包,里面定义了一些函数】
main.go 文件内容 【在main.go中引入并使用 utils.go 这个包】
6.4.6 包的注意事项和细节说明
-
在给一个文件打包时,该包对应一个文件夹,比如这里的utils文件夹对应的包名就是utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母
-
当一个文件要使用其他包函数或变量时,需要先引入对应的包
-
引入方式1:import "包名"
-
引入方式2:
import {
"包名"
"包名"
}
-
package指令在文件第一行,然后是 import指令
-
在import包时,路径从 $GOPATH 的 src下 开始,不用带src,编译器会自动从src开始引入
-
-
为了让其他包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其他语言的public,这样才能跨包访问
-
在访问其他包函数、变量时,其语法是 包名.函数名
-
如果包名较长,Go支持给包取别名,注意细节:取别名后,原来的包名就不能使用了
-
在同一个包下,不能有相同的函数名(也不能有相同的全局变量名),否则就报重复定义。
-
如果你要编译成一个可执行程序文件,就需要将这个包声明为 main,即 package main,这个就是一个语法规范,如果你是写一个库,包名可以自定义。
6.3 函数-调用机制
6.3.1 通俗易懂的方式的理解
6.3.2 函数—调用过程
为了让大家更好的理解函数调用的过程,看两个案例,并画出示意图
-
传入一个数+1
对上图的说明
- 在调用一个函数时,会给该函数分配一个新的空间, 编译器会通过自身的处理让这个新的空间和其他的栈的空间区分开来
- 在每个函数对应的栈中,数据空间是独立的,不会混淆
- 当一个函数调用完(执行完毕)后,程序会销毁这个函数对应的栈空间。
-
计算两个数,并返回
6.3.3 return语句
6.3.3.1 基本语法
func 函数名 (形参列表) (返回值类型列表) {
语句....
return 返回值列表
}
- 如果返回多个值时,在接收时,希望忽略某个返回值,则使用 _符号表示占位忽略
- 如果返回值只有一个(返回值类型列表),可以不写()
案例1
一个细节说明:希望忽略某个返回值,则使用 _符号表示占位忽略
案例2
6.4 函数—递归调用
6.4.1基本介绍
一个函数在函数体内又调用了本身,我们称为递归调用。
代码1
上面代码的分析图
案例2
对上面代码的分析图
6.4.2 函数递归需要遵守的重要原则
- 执行一个函数时,就创建了一个新的受保护的独立空间(新函数栈)
- 函数的局部变量就是独立的,不会相互影响
- 递归必须向退出递归条件逼近,否则就是无限递归,
- 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用 ,就将结果返回给谁。
6.4.3 练习
案例1
斐波那契数,请使用递归的方式,求出斐波那契数1,1,2,3,5,8,13,21...
给你一个整数n,求出它的斐波那契数是多少
案例2
已知 f(1)=3;f(n) = 2*f(n-1)+1; 请使用递归的思想编程,求出 f(n)的值?
案例3
有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个,以后每天猴子都吃其中的一半,然后再多吃一个,当到第十天时,想再吃时(还没吃),发现只有1个桃子了,问题:最初共有多少个桃子?
思路分析
- 第10天只有1个桃子
- 第9天有几个桃子 = (第10天桃子数量 + 1) * 2
- 规律:第n天的桃子数量 peach(n) = (preach(n + 1) + 1) * 2
6.5 函数注意事项和细节讨论
- 函数的形参列表可以是多个,返回值列表也可以是多个
- 形参列表和返回值列表的数据类型可以是值类型和引用类型
- 函数名的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其他包文件使用,类似public,首字母小写,只能被本包文件使用,其他包文件不能使用,类似private
- 函数在的变量时局部的,函数外不生效
- 基本数据类型和数组默认都是值传递的,即进行值拷贝,在函数内修改,不会影响到原来的值
- 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量地址&,函数内以指针的方式操作变量,从效果上看类似引用
-
Go函数不支持重载
-
在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了,通过该变量可以对函数调用
-
函数即是一种数据类型,因此在Go中,函数可以作为参考,并且调用
-
为了简化数据类型的定义,Go支持自定义数据类型
- 基本语法: type 自定义数据类型名 数据类型 //理解:相当于一个别名
- 案例:type myint int //这时myint就等价于int来使用了
- 案例:type mySum func(int,int)int //这时mySum就等价于一个函数类型 func(int,int)int
-
支持函数返回值命名
-
使用 _标识符,忽略返回值
-
Go支持可变参数
// 支持0到多个参数
func sum(args... int) sum int {
}
//支持1到多个参数
func sum(n1 int, args... int) sum int {
}
说明
- args是slice,通过args[index] 可以访问到各个值
- 案例演示:编写一个函数sum 可以求出1到多个int的和
- 如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后
6.6 函数练习
1、
2、
**3、请编写一个函数,swap(n1 int, n2 int)可以交换n1和n2的值
6.7 init函数
6.7.1 基本介绍
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用
6.7.2 基本案例介绍
6.7.3 注意事项和细节
- 如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程是 :变量自定义 -> init函数 -> main函数
结果为:先返回test函数,再返回init函数,最后返回main函数
- init函数最主要的作用,就是去完成一些初始化的工作
-
面试题,案例如果main.go和utils.go都含有变量定义,init函数时,执行懂得流程又是怎么样么?
执行顺序:
6.8 匿名函数
6.8.1 介绍
Go支持匿名函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以多次调用
6.8.2 匿名函数使用方式
6.8.2.1 方式1
在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
6.8.2.2 方式2
将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
6.8.2.3 全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个你们函数,就成为一个匿名函数全局变量,可以在全局有效
6.9 闭包
6.9.1 介绍
闭包就是一个函数与其相关的引用环境组成的一个整体(实体)
6.9.2 案例演示
上面代码的解释说明
- AdderUpper是一个函数,返回的数据类型是 fun (int) int
- 闭包的说明
返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包
- 大家可以这样理解:闭包是类,函数是操作,n是字段,函数和它使用到n构成闭包
- 当我们反复调用f函数时,因为n是初始化一次,因此每调用一次就累计
- 我们要搞清楚这个闭包的关键,就是要分析出返回的函数他使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。
- 对上面代码的一个修改,加深对闭包的认识
6.9.3 闭包的最佳实践
请编写一个程序,具体要如下
- 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如:.jpg),并返回一个闭包
- 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(.jpg),则返回文件名.jpg,如果已经有.jpg后缀,则返回原文件名
- 要求使用闭包的方式完成
- string.HasSuffix ,该函数可以判断某个字符是否有指定的后缀
代码
代码说明
- 返回的 匿名函数 和 makeSuffix (suffix string) 的 suffix 变量组合成一个闭包,因为返回的函数引用到suffix这个变量
- 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入后缀名,比如:.jpg,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。
6.10 函数中— defer
6.10.1 为什么需要defer
在函数中,程序员经常需要创建资源(比如:数据库连接,文件句柄,锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)
6.10.2 入门案例
执行之后的输出结果
ok3 res = 30
ok2 n2 = 20
ok1 n1 = 10
res = 30
6.10.3 defer的细节说明
- 当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中[为了理解方便,暂时成为defer栈],然后继续执行函数下一句语句
- 当函数执行完毕后,在defer栈中,依次从栈顶取出语句执行(注:遵守栈,先入后出的机制),所以看到的前面案例的输出的顺序
- 在defer将语句放入到栈,也会讲相关的值拷贝同时入栈。
执行之后的输出结果
ok3 res = 32
ok2 n2 = 20
ok1 n1 = 10
res = 32
6.10.4 最佳实践
defer最主要的价值是在,当函数执行完毕之后,可以及时的释放函数创建的资源。
模拟代码
说明
- 在Golang编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源),可以立即执行 defer file.Close() de fer connect.Close()
- 在defer后,可以继续使用创建资源
- 当函数完毕后,系统会依次从defer栈中,取出语句,关闭资源
- 这种机制,非常简介,程序员不用再为什么时机关闭资源而烦心。
6.11 函数参数的传递方式
6.11.1 基本介绍
值类型参数默认就是值传递,而引用类型参数默认就是引用传递
6.11.2 两种传递方式
- 值传递
- 引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
6.11.3 值类型和引用类型
- 值类型:基本数据类型int系列、float系列、bool、string、数组和结构体struct
- 引用类型:指针、slice切片、map、管道chan、interface等都是引用类型
6.11.4 值传递和引用传递使用特点
- 值类型默认是值传递,变量直接存储值,内存通常在栈中分配
- 引用类型默认是引用传递,变量存储的是一个地址,这个地址对应的空间才真正存放数据(值),内存通常在堆上分配,当没有任何变量引出这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
- 如果希望函数内的E变量能修改函数外的变量,可以传入变量的,
6.12 变量作用域
6.12.1 说明
- 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部
- 函数外部声明/定义的变量叫做全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
- 如果变量是在一个代码块,比如:for / if 中,那么这个变量的作用域就在该代码块
小练习
6.13 函数联系(综合)
- 函数可以没有返回值案例,编写一个函数,从终端输入一个整数打印出对应的金字塔
- 编写一个函数,从终端输入一个整数(1-9),打印对应的乘法表
6.14 字符串中常用的系统函数
- 统计字符串长度,按字节 len(str)
- 字符串遍历,同时处理有中文的问题 r := []rune(str)
- 字符串转整数
- 整数转字符串:str = strconv.Itoa(12345)
-
字符串转[] byte
-
[] byte转字符串:str = string([]byte{97,98,99})
- 10进制转2,8,16进制
- 查找子串是否在指定的字符串中
- 统计一个字符串有几个指定的子串
- 不区分大小写的字符串比较[==是区分大小写的]
- 返回子串在字符串第一次出现的index值,如果没有返回-1
- 返回子串在字符串最后一次出现的index,如果没有返回-1
- 将指定的子串替换成另一个子串
- 按照指定的某个字符,为分隔标识,将一个字符串拆分成字符串数组
- 将字符串的字母进行大小写的转换
- 将字符串左右两边的空格去掉
- 将字符串左右两边指定的字符去掉
- 将字符串左边指定的字符去掉
- 将字符串右边指定的字符去掉
- 判断字符串是否以指定的字符串开头
- 判断字符串是否以指定的字符串结束
str = strings.HasSuffix("ftp://192.168.1.100", "ftp")
6.15 时间和日期相关函数
- 时间和日期相关函数,需要导入time包
- time.Time 类型,用于表示时间 获取当前时间的方法
- 如何获取到其他的日期信息
- 格式化 日期时间
方式1
方式2
"2006/01/02 15:12:14" 这个字符串的各个数字是固定的,必须是这样写 上面的代码有一个错误的示范,可以去看看
"2006/01/02 15:12:14" 这个字符串的各个数字可以自由的组合,这样可以按程序需求来返回时间和日期
-
时间的常量
常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到100毫秒 100 * time.Millisecond
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
时间常量和休眠的综合案例
- 获取当前 unix时间戳 和 unixnano时间戳(作用可以获取随机数字)
使用方法
实践案例
编写一段代码来统计函数test03执行的时间
6.16 内置函数
6.16.1 说明
Golang设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们成为Go的内置函数,有下面部分:
- len:用来求长度,比如:string、array、slice、map、channel
- new:用来分配内存,主要用来分配值类型,比如:int、float32、struct.....返回的是指针
内存图
- make:用来分配内存,主要用来分配引用类型,比如:chan、map、slice
6.17 Go错误处理机制
对上面代码的总结
- 在默认情况下,当发生错误后(panic),程序就会退出(崩溃)
- 如果我们希望,当发生错误后,可以捕获错误,并进行 处理,保证程序可以继续执行,还可以捕获到错误后给管理员一个提示(邮件,短信)
6.17.1 基本说明
-
Go语言追求简洁优雅,所以,Go语言不支持传统的try....catch....finally这种处理
-
Go中引入的处理方式为:defer,panic、recover
-
这几个异常的使用场景可以这么简单描述,Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
6.17.2 使用defer+recover来处理错误
6.17.3 错误处理的好处
进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮
6.17.4 自定义错误
Go程序中,也支持自定义错误,使用 errors.New 和 panic内置函数
- errors.New("错误说明"),会返回一个error类型的值,表示一个错误
- panic内置函数,接收一个interface()类型的值(也就是任何值了)作为参数,可以接收error类型的变量,输出错误信息,并退出程序