函数
为完成某一功能的程序指令(语句)的集合,称为函数
调用机制
- 在调用一个函数时,会为该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他的栈空间区分开来;
- 在每个函数对应的栈中,数据空间是独立的,不会混淆;
- 当一个函数执行完毕后,程序会销毁这个函数对应的栈空间;
return语句
- 如果返回多个值,在接收时,可以用_符号表示占位忽略;
func [函数名] ([形参列表]) ([返回值列表]) { // 基本语法
[指定语句]
return [返回值列表] // 当函数有return语句时,将结果返回给调用者
}
递归调用
函数体内调用了本身,称为递归调用;
当一件事需要不断地重复某个固定模式,并且下次模式的执行还依赖这次的执行结果时,就用递归来实现
func test(n int) {
if n > 2 {
n-- // 递归必须向退出递归条件逼近
test(n)
}
fmt.Println("n=",n)
}
func test2(n int) {
if n > 2 {
n--
test2(n)
} else {
fmt.Println("n=",n)
}
}
func main() {
test(4) // n = 2, n = 2, n = 3
test2(4) // n = 2
}
总结
- 执行一个函数时,就会创建一个新的受保护的独立空间,也可以称为函数栈
- 函数的局部变量是独立的,不会相互影响;
- 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁。同时当函数执行完毕或返回时,该函数本身也会被销毁;
可变参数
参数数量可变的函数称为可变函数。在声明可变函数的参数时,需要在参数列表的最后一个参数类型之前添加'...',表示该函数会接受任意数量的该类型的参数
- 调用者会隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调用的函数。如果原始参数已经是切片类型,需要在最后一个参数后加上'...'即可
- args是切片,通过
args[index]
可以访问到各个值;
func sum(args... int) sum int {} // 支持0到多个参数
func sum(n1 int, args... int) sum int {} // 支持1到多个参数
func Sum(n1 int,args... int)(result int){
result = n1
for index ,value := range args{
args[index] = value
result += args[index]
}
return
}
type MyFuncType func(int, int) int
func main() {
fmt.Println(Sum(1,6,6,2,3,4,1,541,21)) //585
value := []int{1,23,62,62,1,35}
fmt.Println(Sum(value...)) // 185
}
⭐函数注意事项和细节
-
形参与返回值列表可以是多个;
-
形参列表和返回值列表的数据类型可以是值类型和引用类型;
-
基本数据类型和数据默认为值传递,即值拷贝。所以在函数内修改时,不会影响到原来的值;
-
如果希望函数内的变量能修改函数外的变量,可以传入变量的地址
&
。函数内以指针的方式操作变量; -
Go函数不支持函数重载;
-
在Go中,函数也是一种数据类型。可以赋给一个变量,则该变量就是一个函数类型的变量,可以通过调用该变量来调用函数;
-
函数可以作为形参被调用;
- Go支持自定义数据类型
func GetSum(n1 int, n2 int) int {
return n1 + n2
}
func MyFunc(funvar MyFuncType, num1 int, num2 int ) (int) {
return funvar(num1, num2)
}
type MyFuncType func(int, int) int // 根据GetSum的数据类型直接定义一个新的数据类型供函数调用
func main() {
a := GetSum
fmt.Printf("a的数据类型=%T \nGetSum的数据类型=%T \n",a,GetSum) // GetSum的数据类型=func(int, int) int
fmt.Println(MyFunc(GetSum, 50, 60))
}
- 支持对函数返回值命名;
func GetSumAndSub(n1 int, n2 int) (sum int, sub int){ // 返回sum和sum两个int类型的值
sum = n1 + n2
sub = n1 - n2
return }
func main() {
b,c := GetSumAndSub(3, 2)
fmt.Printf("b=%v,c=%v",b,c)
}
init函数
每一个源文件都可以包含一个init函数。该函数会在main函数执行前,被Go运行框架调用。
如果一个文件同时包含全局变量定义,init函数和main函数,则执行流程是导入的包>全局变量定义>init函数>main函数
匿名函数
如果某个函数只是希望使用一次,可以考虑匿名函数。当然,匿名函数也可以实现多次调用,就是将匿名函数赋给一个变量,通过该变量来实现多次调用匿名函数。
匿名函数有以下两种使用方式且代码中还有'全局匿名函数':
- 在定义匿名函数时就直接调用
- 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
var (
Fun1 = func (n1 int, n2 int) int { // 定义fun1为全局匿名函数
return n1 * n2
}
)
func main() {
res1 := func (n1 int, n2 int) (int) { // 方式一,在定义匿名函数时就直接调用
return n1 + n2
}(10,20) // 参数
fmt.Println(res1) // 输出30
a := func (n1 int, n2 int) (int) { // 方式二,将函数赋给一个变量,通过变量来调用
return n1 + n2
}
res2 := a(20,30)
fmt.Println(res2)
res3 := Fun1(2,4) // 方式三,调用全局匿名函数
fmt.Println(res3)
}
细节注意:在for循环中生成的所有函数值都共享相同的循环变量,即函数值中记录的是循环变量的内存地址,所以当循环结束后,接收函数变量的值等于最后一次迭代的值,可以通过引入另一个变量来存储值。
使用go
和defer
语句的时候可能经常遇到该问题,是因为它们都会等待循环结束后,再执行函数值。
闭包
闭包就是一个函数和与其相关的引用环境组合的一个 (实体),具体使用看下面案例(从该案例可以看到变量的声明周期不由它的作用域决定,即AddUpper()返回后,变量n仍然隐式的存在于f中)。
在AddUpper()中定义的匿名内部函数可以访问和更新AddUpper()中的局部变量,这意味着匿名函数和AddUpper()中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。
func AddUpper() func (int) int { // AddUpper是一个函数,返回的数据类型时fun(int) int
var n int = 10
return func (x int) int {
n += x
return n
}
}
func main() {
f := AddUpper()
fmt.Println(f(3)) // 13
fmt.Println(f(1)) // 14
fmt.Println(f(2)) // 16
}
返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包。闭包可以保留上次引用的某个值。
- 可以理解为:闭包是类,函数是操作,n是字段。函数和它使用到n构成闭包;
- 当我们反复的调用f函数时,因为n时初始化一次,因此每调用一次就进行累计;
- 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包;
最佳实践
func makeSuffix(suffix string) func (string) string {
return func (name string) string{
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
f := makeSuffix(".jpg")
fmt.Println(f("winter")) // winter.jpg
fmt.Println(f("bird.jpg")) // bird.jpg
}
常用的系统函数
字符串中常用的系统函数
- strconv // 字符串类型转换
- strings // 字符串处理
func main() {
// 统计字符串长度,按字节
var str string = "daihaorui豪锐"
fmt.Println("str=",len(str))
// 字符串遍历,同时处理有中文的问题
str1 := []rune(str)
fmt.Println("str1=",len(str1))
// 字符串转整数
var str2 string = "123"
n, err := strconv.Atoi(str2)
fmt.Printf("str2=%v,str2err=%v \n",n,err)
// 整数转字符串
var num1 int = 22
n1 := strconv.Itoa(num1)
fmt.Printf("num1=%v,num1Type=%T \n",n1,n1)
// 字符串转[]byte: var bytes = []byte("hello go")
str3 := "haorui"
var bytes = []byte(str3)
fmt.Printf("bytes=%v \n",bytes)
// []byte 转字符串:str = string([]byte{97,98,99})
str = string([]byte{97,98,99})
fmt.Printf("str=%v \n",str)
// 10进制转2,8,16进制:str = strconv.FormatInt(123,2) // 2->8,16
str = strconv.FormatInt(123,2) // 2->8,16
fmt.Printf("str=%v \n",str)
// 查找子串是否在指定的字符串中:strings.Contains("seafood","foo") //true
fmt.Println(strings.Contains("seafood","foo"))
// 统计一个字符串有几个指定的字串:strings.Count("ceheese","e") // 4
fmt.Println(strings.Count("ceheese","e"))
// 不区分大小写的字符串比较(==是区分字母大小写的)fmt.Println(strings.EquaIfold("abc","Abc")) // true
fmt.Println("abc"=="Abc")
fmt.Println(strings.EqualFold("abc","Abc"))
// 返回子串在字符串中第一次出现的index值,如果没有则返回-1:strings.Index()
fmt.Println(strings.Index("NLT_abc","abc"))
// 对字符串进行替换:strings.Replace()
// 对字符串指定分隔符进行分隔:strings.Split()
// 对字符串进行字母大小写转换:strings.ToLower()
// 将字符串的指定字符或空格去掉:strings.Trim() & strings.TrimSpace() & strings.TrimRight()
// 判断字符串是否以指定字符串开头 & 结尾:strings.HasPrefix() & strings.HasSuffix()
}
时间和日期函数(time)
内建函数(buildin)
- len() //求长度
- new() // 用来分配内存,主要用来分配值类型,如int、float32、struct,返回的是指针
- make() // 用来分配内存,主要用来分配引用类型,如channel、map、slice
num2 := new(int) // new(int)会自动分配地址
var num2 *int // *int需要手动指定地址
defer函数
在函数中,开发经常需要创建资源(数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go提供了defer(延时机制),释放资源的defer应该直接跟在请求资源的语句后
细节说明:
- 遵循先入后出的方式,直到defer执行完毕后才会执行main区中的语句。(在执行到defer时,暂时不执行,会到defer的语句压入到独立的defer栈中,当函数执行完毕后,再从defer栈,按照先入后出的方式出栈)
- 在defer将语句放入到栈时,也会将相关的值拷贝同时入栈;
func Sum(n1 *int,n2 *int) int {
defer fmt.Println("ok1 n1=",*n1)
defer fmt.Println("ok1 &n1=",n1)
fmt.Println("ok1 &n1=",n1)
defer fmt.Println("ok2 n2=",*n2)
*n1++
*n2++
res := *n1 + *n2
fmt.Println("ok3 res=",res)
fmt.Println("ok3 &n1=",n1)
return res
}
func main() {
n1 := 10
n2 := 20
fmt.Println("&n1=",&n1)
res := Sum(&n1,&n2)
fmt.Println("res=",res)
fmt.Println("&n1=",&n1)
}
// 输出
&n1= 0xc000012088
ok1 &n1= 0xc000012088
ok3 res= 32
ok3 &n1= 0xc000012088
ok2 n2= 20
ok1 &n1= 0xc000012088
ok1 n1= 10
res= 32
&n1= 0xc000012088gg
- defer机制也常用于记录何时进入和退出函数
bigSlowOperation()
被调用时,trace()
会返回一个函数值,该函数值会在bigSlowOperation()
退出时被调用
func bigSlowOperation() {
defer trace("bigSlowOperation")() // don't forget the extra parentheses
// ...lots of work…
time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg,time.Since(start))
}
}
func main() {
bigSlowOperation()
}
$ go run main.go
2022/08/13 14:37:22 enter bigSlowOperation
2022/08/13 14:37:24 exit bigSlowOperation (2.000747904s)
Panic异常
当发生panic异常时,程序会执行以下操作:
- 中断运行,立即执行在该goroutine中被延迟的函数(defer机制)
- 程序崩溃并输出日志信息,日志信息包括panic value(通常是某种错误信息)和函数调用的堆栈跟踪信息
可以通过调用内置panic函数来引发panic异常,panic函数接受任何值作为参数。对于大部分漏洞,应该使用Go提供的错误机制(而不是panic),来尽量避免程序的崩溃。
我们应该假设函数的输入一直合法:当调用这输入了不应该出现的输入时,触发panic异常。
Recover捕获异常
如果在deferred
函数中调用了内置函数recover
,并且定义该defer
语句的函数发生了panic
异常,recover
会使程序从panic
中恢复,并返回panic value
。导致panic
异常的函数不会继续运行,但能正常返回。未发生panic
时调用revover
,recover
会返回nil
实现:使用panic和recover编写一个不包含return语句但能返回一个非零值的函数
func test() (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("报错=%v", p)
}
}()
panic("输入的报错")
}
func main() {
fmt.Println(test())
}
$ go run main.go
报错=输入的报错
错误处理(errors)
Go中大部分函数代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑。其错误处理方式为,抛出一个panic异常,然后在defer中通过recover捕获这个异常,然后正常处理,用到:
- defer
- panic
- recover
func test() {
defer func () {
err := recover()
if err != nil {
fmt.Println("res=",err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println(res)
}
func main() {
test()
fmt.Println("该行代码正常执行")
}
自定义错误
Go支持自定义错误,使用errors.New()
和panic
内置函数。
errors.New("错误说明")
,会返回一个error
类型的值,表示一个错误。panic
内置函数,接收一个interface{}
类型的值(也就是任何值)作为参数。可以接收error
类型的变量,输出错误信息,并退出程序
func test02 () {
err := ReadConf("c2onfig.ini")
if err != nil {
panic(err)
}
fmt.Println("test02()继续执行")
}
func ReadConf (name string) (err error) {
if name == "config.ini" {
return nil
} else {
return errors.New("读取文件错误")
}
}
func main() {
test02() // 执行到该函数的时候报'panic: 读取文件错误',后续代码不执行
fmt.Println("该行代码正常执行")
}
包
基本概念
Go的每一个文件都属于一个包,也就是Go是以包的形式来管理文件和项目目录结构的
包的作用:
- 区分相同名字的函数、变量等标识符;
- 当程序文件很多时,可以管理项目;
- 控制函数、变量等的访问范围,即作用域;
基本操作
package [包名] // 归属于指定包
import "[包的路径]"
注意事项和使用细节
- 文件包名通常和文件所在的文件夹名一致,一般为小写字母;
- 在import包时,路径从
$GOPATH下
的src下开始。不用带src,编译器会自动从src下开始引入; - 能被访问到的函数名的首字母大写;
- 访问其他包的函数、变量时,语法是
[包名].[函数名]
; - 可以对导入的包设置别名,在
import
的前面,与_
相对应; - 如果要编译成一个可执行程序,需要将该包声明为
main
,即package.main
。若是只是编写库,则可以自定义包名;
$ cd D:\MyNutcloud\wlhiot_manage\goproject // $GOPATH
$ tree
D:.
└─src
└─go_code // 源码管理
├─array // 项目名称
│ └─main // 包名
go build -o [可执行文件名称].exe go_code/array/main // 格式,以GOPATH路径
go build -o bin/my.exe .\src\go_code\array\main // 实际打包
案例
$ ls D:\MyNutcloud\wlhiot_manage\goproject\src\go_code\function02\funcinit
d----l 2022/7/13 20:32 utils
-a---l 2022/7/13 21:09 25 go.mod
-a---l 2022/7/13 21:18 291 main.go
$ ls D:\MyNutcloud\wlhiot_manage\goproject\src\go_code\function02\funcinit\utils\
-a---l 2022/7/13 21:16 151 utils.go
$ cat .\main.go
package main
import (
"fmt"
"funcinit/utils"
)
var age = test()
func test() int {
fmt.Println("test()")
return 90
}
func init(){
fmt.Println("init()...")
}
func main() {
fmt.Println("main()...age",age)
fmt.Println("Age=",utils.Age,"Name=",utils.Name)
}
$ cat .\utils\utils.go
package utils
import (
"fmt"
)
var Age int
var Name string
func init() {
fmt.Println("utils包的init()")
Age = 100
Name = "tom"
}
$ go run .\main.go
utils包的init()
test()
init()...
main()...age 90
Age= 100 Name= tom