函数
![image-20220121130720244](https://i-blog.csdnimg.cn/blog_migrate/3a874a05a4913e43f162969004b9b40e.png)
![image-20220125212743464](https://i-blog.csdnimg.cn/blog_migrate/ed2474a14473151c2a123da30857d616.png)
(1)在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其它的栈的空间区分开来
(2)在每个函数对应的栈中,数据空间是空间的,不会混淆
(3)当一个函数调用完毕(执行完毕)后,程序念销毁这个函数对应的栈空间。
return语句
![image-20220125212833783](https://i-blog.csdnimg.cn/blog_migrate/abdaf0b5a756c6fee34bee8ce6746cb6.png)
递归调用
一个函数在函数体内有调用了本身
递归的规则
- 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
- 函数的局部变量是独立的,不会相互影响
- 递归必须向退出递归的条件逼近,否则就是无限循环
- 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁。
函数的细节
- 函数的形参列表可以是多个,返回值列表也可以是多个。
- 形参列表和返回值列表的数据类型可以是值类型和引用类型。
- 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似public ,首字母小写,只能被本包文件使用,其它包文件不能使用,类似private.
- 函数中的变量是局部的,函数外不生效
- 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
- 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用传递。
- Go函数不支持函数重载。
- 在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
- 函数既然是一种数据类型,因此在Go中,函数可以作为形参,并且调用
- 为了简化数据类型定义,Go支持自定义数据类型。基本语法: type 自定义数据 类型名 数据类型 理解:相当于一个别名案例:type myint int这时myInt就等价int来使用了,go认为不是同一个类型。
- 支持函数返回值命名
- 使用下划线(_)标识符,忽略返回值
- Go支持可变参数,可变参数需要放在形参列表的后面
init函数
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用。
![image-20220121172059992](https://i-blog.csdnimg.cn/blog_migrate/0a366040c4018c4884add8d23f9e9b82.png)
init函数的细节
-
如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程是变量定义->init函数->main函数
-
init函数最主要的作用,就是完成一些初始化的工作
匿名函数
Go支持匿名函数(没有名字的函数),如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
匿名函数使用方式
- 在定义匿名函数时就直接调用,
- 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
![image-20220121173443997](https://i-blog.csdnimg.cn/blog_migrate/d6551f28c394d23d504fbfe620589ad3.png)
全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
![image-20220121173715345](https://i-blog.csdnimg.cn/blog_migrate/d58b82d4fd44d01d10746eb64c34ded2.png)
函数中的-defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)。
package main
import "fmt"
func sum(n1, n2 int) int {
//当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈中(defer栈)
//当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
defer fmt.Println("ok1 n1=", n1) // 3. ok1 n1 = 10
defer fmt.Println("ok2 n2=", n2) // 2. ok2 n2 = 20
res := n1 + n2
fmt.Println("ok3 res=", res) //1. ok3 res = 30
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res=", res) //4. res = 30
}
- 当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中[暂时称该栈为defer栈],然后继续执行函数下一个语句。
- 当函数执行完毕后,在从defer栈中,依次从栈顶取出语句执行(注:遵守栈先入后出的机制)
- 在defer将语句放入到栈时,也会将相关的值拷贝同时入栈。
package main
import "fmt"
func sum(n1, n2 int) int {
//当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈中(defer栈)
//当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
defer fmt.Println("ok1 n1=", n1) // 3. ok1 n1 = 10
defer fmt.Println("ok2 n2=", n2) // 2. ok2 n2 = 20
n1++ //n1 = 11
n2++ //n2 = 21
res := n1 + n2
fmt.Println("ok3 res=", res) //1. ok3 res = 32
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res=", res) //4. res = 32
}
defer的实践
defer最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。
![image-20220121212616832](https://i-blog.csdnimg.cn/blog_migrate/00be8dcdbc06017acae6766ec19e66e9.png)
- 在golang编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源),可以执行 defer file.Close( )defer connect.CloseO
- 在defer后,可以继续使用创建资源.
- 当函数完毕后,系统会依次从defer栈中,取出语句,关闭资源.
- 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。
两种传递方式
- 值传递
- 引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
值传递
值类型:基本数据类型int系列, float系列, bool, string·数组和结构体struct
引用传递
引用类型:指针、slice切片、map、管道chan、interface等都是引用类型
![image-20220121213518750](https://i-blog.csdnimg.cn/blog_migrate/2fecfbaac1303aee8e5fb8251cd274cb.png)
如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。
变量作用域
- 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部。
- 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效。
- 如果变量是在一个代码块,比如 for / if中,那么这个变量的的作用域就在该代码块
包
在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如main.go中,去使用utils.go 文件中的函数,如何实现?—>包
现在有两个程序员共同开发一个Go项目,程序员xiaoming希望定义函数cal ,程序员xiaoqiang也想定义函数也叫cal。两个程序员为此还吵了起来,怎么办?---->包
包的本质
包的本质实际上就是创建不同的文件夹,来存放程序文件。
包的基本概念
go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的
包的作用
- 区分相同名字的函数、变量等标识符
- 当程序文件很多时,可以很好的管项目
- 控制函数、变量等访问范围,即作用域
包的细节
-
引入包的路径从GOPATH的src下开始,不用带src,编译器自动从src下开始定位的
-
包内的函数名和变量的首字母一定要大写,表示公有,才能访问。
-
如果包名比较长可以取别名,则原来的包名就不能使用了(在引用的路径前添加别名)
-
在同一个包下,不能有同一个函数名
-
如果你要编译成一个可执行程序文件,就需要将这个包声明为main,即package main.这个就是一个语法规范,如果你是写一个库,包名可以自定义。
闭包
基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
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)) //11
fmt.Println(f(2)) //13
fmt.Println(f(3)) //16
}
AddUpper是一个函数,返回的数据类型是fun (int) int
var n int = 10
return func(x int) int {
n = n + x
return n
}
返回的是一个匿名函数。但是这个匿名函数引用到函数外的n ,因此这个匿名函数就和n形成一个整体,构成闭包。(可以这样理解:闭包是类。函数是操作,n是字段。函数和它使用到n 构威闭包。)
当我们反复的调用f函数时,因为n是初始化一次,因此每调用一次就进行累计。
我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。
package main
import (
"fmt"
"strings"
)
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"))
fmt.Println("文件名处理后=", f("bird.jpg"))
}
返回的匿名函数和 makeSuffix (suffix string)的suffix变量组合成一个闭包,因为返回的函数引用到suffix这个变量
闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入后缀名,比如.jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入—次就可以反复使用。
字符串函数
统计字符串的长度,按字节len(str)
//统计字符串的长度,按字节len(str)
str := "hello北"
fmt.Println("str len=", len(str)) //8
字符串遍历,同时处理有中文的问题r :=rune(str)
//字符串遍历,同时处理有中文的问题r :=rune(str)
str2 := "hello北京"
r := []rune(str2)
for i := 0; i < len(r); i++ {
fmt.Printf("字符=%c\n", r[i])
}
字符串转整数:n, err := strconv.Atoi(“12”)
//字符串转整数:n, err := strconv.Atoi("12")
n, err := strconv.Atoi("123")
if err != nil {
fmt.Println("转换错误", err)
} else {
fmt.Println("转换成功结果是", n)
}
整数转字符串str = strconv.Itoa(12345)
//整数转字符串str = strconv.Itoa(12345)
str = strconv.Itoa(12345)
fmt.Printf("str=%v,str=%T", str, str)
字符串转[]byte: var bytes = []byte(“hello go”)
//字符串转[]byte: var bytes = []byte("hello go")
var bytes = []byte("hello go")
fmt.Printf("bytes=%v\n", bytes)
[]byte转字符串:str = string([]byte{97,98,99})
//[]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),返回对应的字符串
//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)
查找子串是否在指定的字符串中:strings.Contains("“seafood”, “foo”) /true
//查找子串是否在指定的字符串中:strings.Contains(""seafood", "foo") /true
b := strings.Contains("seafood", "food")
fmt.Printf("b=%v\n", b)
统计一个字符串有几个指定的子串:strings.Count(“ceheese” , “e”)//4
//统计一个字符串有几个指定的子串:strings.Count("ceheese" , "e")//4
num := strings.Count("ceheese", "e")
fmt.Printf("num = %v\n", num)
不区分大小写的字符串比较(==-是区分字母大小写的): fmt.PrintIn(strings.EqualFold(“abc” ,“Abc”)) /l true
//不区分大小写的字符串比较(==是区分字母大小写的): fmt.PrintIn(strings.EqualFold("abc" ,"Abc")) /l true
b = strings.EqualFold("abc", "Abc")
fmt.Printf("b = %v\n", b)
fmt.Println("结果", "abc" == "Abc")
返回子串在字符串第一次出现的index值,如果没有返回-1: strings.Index(“NLT_abc”, “abc”)// 4
//返回子串在字符串第一次出现的index值,如果没有返回-1:
// strings.Index("NLT_abc", "abc")// 4
index := strings.Index("NLT_abc", "abc")
fmt.Printf("index = %v\n", index)
返回子串在字符串最后一次出现的index,如没有返回-1 : strings.LastIndex(“go golang”. “go”)
//返回子串在字符串最后一次出现的index,如没有返回-1 : strings.LastIndex("go golang". "go")
index = strings.LastIndex("go golang", "go")
fmt.Printf("index = %v\n", index)
将指定的子串替换成另外一个子串; strings.Replace(“go go hello” ,“go” ,“go 语言”, n)n可以指定你希望替换几个,如果n=-1表示全部替换
//将指定的子串替换成另外一个子串; strings.Replace("go go hello" ,"go" . "go 语言", n)
//n可以指定你希望替换几个,如果n=-1表示全部替换
str = strings.Replace("go go hello", "go", "go 语言", 2)
fmt.Printf("str=%v\n", str)
按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组:strings.Split(“hel1o,world,ok”, “,”)
//按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组:
//strings.Split("hel1o.world.ok", ".")
strArr := strings.Split("hel1o,world,ok", ",")
fmt.Printf("strArr=%v\n", strArr)
将字符串的字母进行大小写的转换: strings.ToLowfr(“Go”)/ go strings.ToUpper(“Go”)//Go
//将字符串的字母进行大小写的转换: 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)
将字符串左右两边的空格去掉:strings.TrimSpace(" tn a lone gopher ntrn ")
//将字符串左右两边的空格去掉:strings.TrimSpace(" tn a lone gopher ntrn) "
str = strings.TrimSpace(" tn a lone gopher ntrn ")
fmt.Printf("str=%v\n", str)
将字符串左右两边指定的字符去掉:
strings.Trim(" ! hello! “,”!") //[“hello”]//将左右两边!和“ "去掉
//将字符串左右两边指定的字符去掉:
//strings.Trim(" ! hello! "," !") //["hello"]//将左右两边!和“"去掉
str = strings.Trim(" ! hel ! lo ", " !")
fmt.Printf("str=%v\n", str)
将字符串左边指定的字符去掉:strings.TrimLeft("! hello! “, " !”) // [“hello”]!/将左边!和" "去掉
将字符串右边指定的字符去掉:strings.TrimRight("’! hello! “,” !") //[“hello”]/1将右边!和" "去掉
判断字符串是否以指定的字符串开头: strings.HasPrefix(“ftp://192.168.10.1” , “ftp”)// true
//判断字符串是否以指定的字符串开头: strings.HasPrefix("ftp://192.168.10.1" , "ftp")// true
b = strings.HasPrefix("ftp://192.168.10.1", "ftp")
fmt.Printf("b=%v\n", b)
判断字符串是否以指定的字符串结束: strings.HasSuffix者"“NLT_abc.jpg”, “abc”)//false
时间和日期相关的函数
时间和日期相关函数,需要导入time包
time.Time类型,用于表示时间
time.now()获取当前时间
now := time.Now()
fmt.Printf("now = %v type=%T", now, now)
通过now可以获取到年月日,时分秒
//通过now可以获取到年月日,时分秒
fmt.Printf("年=%v\n", now.Year())
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())
格式化日期和时间
![image-20220122155524002](https://i-blog.csdnimg.cn/blog_migrate/3b4bc838bdc838f128c60aa3015945a7.png)
//格式化日期和时间
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()
时间常量
![image-20220122160522679](https://i-blog.csdnimg.cn/blog_migrate/4c99974bcc345b29f0241a70cd99bc0f.png)
//需求,每隔1秒中打印一个数字,打印到100时就退出
//0.1秒打印一个数字
i := 0
for {
i++
fmt.Println(i)
//time.Sleep(time.Second)
time.Sleep(time.Millisecond * 100)
if i == 100 {
break
}
}
获取当前unix时间截和 unixnano时间戳。(作用是可以获取随机数宁
![image-20220122161153816](https://i-blog.csdnimg.cn/blog_migrate/af9cf0c87968a01d7cea99c939823707.png)
//Unix和UnixNano的使用
fmt.Printf("unix=%v unixnano=%v", now.Unix(), now.UnixNano())
//运算test运行时间
func test() {
str := ""
for i := 0; i < 100000; i++ {
str += "hello" + strconv.Itoa(i)
}
}
func main() {
//在执行test前,先获取到当前的unix时间戳
start := time.Now().Unix()
test()
end := time.Now().Unix()
fmt.Printf("test执行消耗的秒数为%v秒", end-start)
}
内置函数
Golang 设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们称为Go的内置函数。文档: https://studygolang.com/pkgdoc -> builtin
-
len:用来求长度,比如string、array、slice、map、channel
-
new:用来分配内存,主要用来分配值类型,比如 int、float32,struct…返回的是指针
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
-
make:用来分配内存,主要用来分配引用类型,比如chan、map、slice。这个我们后面讲解。
错误处理
在默认情况下,当发生错误后(panic),程序就会退出(崩溃)
如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
- Go语言追求简洁优雅,所以,Go语言不支持传统的 try …catch…finally这种处理。
- Go中引入的处理方式为: defer, panic, recover
- 这几个异常的使用场景可以这么简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
package main
import (
"fmt"
)
func test() {
//使用defer+ recover
defer func() {
err := recover() //recover内置函数,可以捕获异常
if err != nil {
fmt.Println("err=", err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println(res)
}
func main() {
test()
fmt.Println("main的代码")
}
自定义错误
Go程序中,也支持自定义错误,使用errors.New和panic内置函数。
errors.New(“错误说明”),会返回一个error类型的值,表示一个错误
panic内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。可以接收error类型的变量,输出错误信息,并退出程序
//读取配置文件的信息
func readConf(name string) (err error) {
if name == "config.ini" {
//读取
return nil
} else {
return errors.New("读取文件错误...")
}
}
func test2() {
err := readConf("config2.ini")
if err != nil {
//如果文件发送错误,就输出这个错误,并终止程序
panic(err)
}
fmt.Println("test2后面的代码继续进行")
}
func main() {
test2()
fmt.Println("main的代码")
}