文章目录
6.1 函数
6.1.1 函数的基本语法
func 函数名 (形参列表) (返回值列表){
执行语句...
return 返回值列表
}
-
形参列表:表示函数的输入
-
函数中的语句:表示为了实现某一功能代码块
-
函数可以有返回值,也可以没有
6.1.2 函数使用的注意实现和细节讨论
-
函数的形参列表可以是多个,返回值列表也可以是多个。
-
形参列表和返回值列表的数据类型可以是值类型和引用类型。
-
函数的命名遵循标识符命名规范,首字母不能是数字。首字母大写该函数可以被本包文件和其他包文件使用;首字母小写只能被本包文件使用,其它包不能使用。
-
函数中的变量是局部的,函数外不生效。
-
基本数据类型和数组是默认值传递的,即进行值拷贝。在函数内修改不会影响到原来的值。
-
如果希望函数内的变量能修改函数外的变量(指默认以值传递的方式传递的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量,从效果上看类似引用。
-
Go函数不支持函数重载。
-
在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量,通过该变量对函数调用。 因此,函数也可以作为形参并且调用。
-
为了简化数据类型定义,Go支持自定义数据类型。
type 自定义数据类型名 数据类型
。 -
支持对函数返回值命名,如:
a, b = myFunction(c,d)
,a、b就是对返回值命名。 -
使用"_"标识符,忽略返回值。
-
Go支持可变参数,可变参数需要放在形参列表最后。
//args是slice切片,通过args[index]可以访问到各个值
//支持0到多个参数
func sum(args... int) sum int {
}
//支持1到多个参数
func sum(n1 int, args... int) sum int {
}
//例子
func sum(n1 int, args... int) int {
sum := n1
for i := 0; i < len(args); i++ {
sum += args[i]
}
return sum
}
res := sum(10, -1, 2, 90, 0) // 10是n1,-1开始是args
fmt.Println("res=", res)
6.1.3 init函数
每一个源文件都可以包含一个init函数,该函数会在main函数执行前被Go运行框架调用。
-
注意事项
-
如果一个文件同时包含全局变量定义、init函数和main函数,则执行的流程:全局变量定义—init函数—main函数。
-
如果main包(全局变量定义、init函数、main函数)引用了go包(全局变量定义、init函数),且执行的流程:go全局变量定义—go init函数—main全局变量定义—main init函数—main main函数。
-
6.1.4 匿名函数
Go支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
- 使用方式1(只能调用一次):
res := func (n1 int, n2 int) int {
return n1 + n2
} ( 10, 20)
fmt.Println("res = ", res) //30
- 使用方式2(将匿名函数赋给一个变量,再通过这个变量来调用匿名函数):
a := func (n1 int, n2 int) int {
return n1 - n2
}
res = a(10, 30)
fmt.Println("res = ", res) //-20
- 全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数就成为一个全局匿名函数,可以在程序有效。
6.1.5 函数的defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时地释放资源,Go的设计者提供defer。
package main
import "fmt"
func sum(n1 int, n2 int) int {
//当执行到defer时,会将defer后面的语句压入到独立的栈,暂时不执行
//当函数执行完毕后,再从栈中按照先进后出的方式执行(即栈的特性)
defer fmt.Println("ok1 n1 = ", n1) //执行3
defer fmt.Println("ok2 n2 = ", n2) //执行2
res := n1 + n2
fmt.Println("ok3 res = ", res) //执行1
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res = ", res) //执行4
}
-
defer的注意事项和细节:
- 当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中,然后继续执行函数下一语句。
-
当函数执行完毕后,再从defer中依次从栈顶取出语句执行(遵循栈先进后出的原则)。
-
在defer将语句放入栈时,也会将相关的值拷贝同时入栈。
package main
import "fmt"
func sum(n1 int, n2 int) int {
//当执行到defer时,会将defer后面的语句压入到独立的栈,暂时不执行
//当函数执行完毕后,再从栈中按照先进后出的方式执行(即栈的特性)
defer fmt.Println("ok1 n1 = ", n1) //执行3
defer fmt.Println("ok2 n2 = ", n2) //执行2
n1++ //这句话不会影响栈中存放的n1值,仍然为10
n2++ //同理,栈中的n2仍然为20
res := n1 + n2
fmt.Println("ok3 res = ", res) //执行1
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res = ", res) //执行4
}
-
defer的最佳实践:defer最重要的价值在于当函数执行完毕后,可以及时的释放函数创建的资源。
-
在Go编程中的通常做法是,创建资源后(如打开了文件、获取了数据库的连接、或者锁资源),可以执行
defer file.Close()
或defer connect.Close()
-
在defer后,可以继续使用创建资源
-
当函数完毕后,系统会依次从栈中,取出语句关闭资源
-
//模拟代码如下
func test(){
file = openfile(文件名)
defer file.close()
//其他代码
connect = openDatabase()
defer connect.close()
//其他代码
}
6.1.6 函数参数传递方式
- 两种传递方式:值传递和引用传递。
不管是值传递还是引用传递,传递给函数的都是变量的副本。值传递的是值的拷贝,引用传递是地址的拷贝。一般来说,地址拷贝效率高,因为数据量小,而值拷贝取决于拷贝的数据大小,数据越大,效率越低。
-
值类型和引用类型
-
值类型:基本数据类型int系列、float系列、bool、string、数组和结构体struct
-
引用类型:指针、slice切片、map、管道chan、interface等
-
-
值传递和引用传递使用特点
-
值类型默认是值传递,变量直接存储值,内存通常在栈中分配。
-
引用类型默认是引用传递,变量存储的是一个地址,这个地址对应的空间才真正存储数据,内存通常在堆上分配。当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收。
-
如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量,从效果上看类似引用。
-
6.1.7 变量作用域
-
函数内部声明/定义的变量叫做局部变量,作用域仅限于函数内部。
-
函数外部声明/定义的变量叫做全局变量,作用域在整个包都有效。如果其首字母为大写,则作用域在整个程序有效。
-
如果变量是在一个代码块,比如for/if中,那么这个变量的作用域就在该代码块。
6.2 包
6.2.1 包的基本概念
go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的。
6.2.2 包的三大作用
-
区分相同名字的函数、变量等标识符
-
当程序文件很多时,可以很好地管理项目
-
控制函数、变量等访问范围,即作用域
6.2.3 包的相关说明
-
打包基本语法:
package 包名
-
引入包的基本语法:
import "包的路径"
-
如果要引用别的包的变量、函数,注意变量名/函数名首字母要大写,相等于public的作用
6.2.4 包使用的注意事项和细节讨论
-
在给一个文件打包时,该包对应一个文件夹,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
-
当一个文件要使用其它包函数或变量时,需要先引入对应的包。
- 在import包时,路径从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入。
-
为了让其它包的文件可以访问到本包的函数,则该函数名的首字母需要大写。
-
在访问其它包函数、变量时,其语法是
包名.函数名
。 -
如果包名比较长,Go支持给包取别名,取别名后,原来的包名就不能用了,只能用别名来访问该包的函数和变量,如:
util ".../.../.../utls"
,util就是别名,后面是原来的包名。 -
在同一包下,不能有相同的函数名/全局变量名,否则报重复定义。
-
如果你要编译成一个可执行程序文件,就需要将这个包声明为main,即package main。如果是需要写一个库,包名可以自定义。
6.2.5 闭包
闭包就是一个函数和其相关的引用环境组合的一个整体。
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
-
闭包的说明:返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数和n形成了一个整体,构成闭包。
-
可以理解为:闭包是类,函数是操作,n是字段。函数和它使用的n构成闭包。
-
当我们反复调用f函数时,因为n是初始化一次,因此每调用一次就进行累加。
-
闭包的关键:分析出返回的函数它使用(引用)到那些变量,因为函数和它引用到的变量共同构成闭包。
-
请编写一个程序,具体要求如下:
(1)编写一个函数makeSuffix(suffix string)可以接受一个文件后缀名(比如.jpg),并返回一个闭包
(2)调用闭包,可以传入一个文件名,如果该文件名没有执行的后缀(比如.jpg),则返回 文件名.jpg;如果已经有.jpg后缀,则返回原文件名
(3)要求使用闭包的方式完成
(4)strings.HasSuffix,该函数可以判断某个字符串是否有指定的后缀
package main
import (
"fmt"
"strings"
)
/*
请编写一个程序,具体要求如下:
(1)编写一个函数makeSuffix(suffix string)可以接受一个文件后缀名(比如.jpg),并返回一个闭包
(2)调用闭包,可以传入一个文件名,如果该文件名没有执行的后缀(比如.jpg),则返回 文件名.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() {
//测试makeSuffic使用
//返回一个闭包
f := makeSuffix(".jpg")
fmt.Println("文件名winter处理后 = ", f("winter")) //winter.jpg
fmt.Println("文件名bird.jpg处理后 = ", f("bird.jpg")) //bird.jpg
}
- 上述代码的说明:
- 返回的匿名函数和makeSuffic(suffix string)的suffix变量组合成一个闭包,因为返回的函数引用到suffix这个变量
- 闭包的好处:如果使用传统的方法也可以实现这个功能,但是每次都需要传入后缀名(如.jpg),而闭包可以保留上次引用的某个值,所以传入一次就可以反复引用。
6.3 字符串常用的系统函数
- 统计字符串的长度,按字节:
len(str)
//统计字符串的长度,按字节 len(str)
str := "hello北" //Go的编码的utf-8,字母和数字占1个字节,汉字占3个字节
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) //str = 12345, str = string
-
字符串转[]byte:```var bytes = []byte(“hello go”)
-
[]byte转字符串:
str = string([]byte{97,98,99})
-
10进制转2、8、16进制:
str = strconv.FormatInt(123,2) // 2->8,16
-
查找子串是否在指定的字符串中:```strings.Contains(“seafood”, “foo”) // true
-
统计一个字符串有几个指定的子串:
strings.Count("ceheese", "e") //4
-
不区分大小写的字符串比较(==是区分字母大小写的):
strings.EqualFold("abc", "ABC") //true
-
返回子串在字符串第一次出现的index值,如果没有返回-1:
strings.Index("NLT_abc", "abc") //4
-
返回子串在字符串最后一次出现的index,如果没有返回-1:
strings.LastIndex("go golang", "go") //3
-
将指定的子串替换成另外一个子串:
strings.Replace("go go hello", "go" ,"go语言", n) //n可以指定你希望替换几个,如果n=-1那么全部替换
-
按照指定的某个字符为分割标识,将一个字符串拆分成字符串数组:
strings.Split("hello,world,ok", ",")
-
将字符串的字母进行大小写的转换:
strings.ToLower("Go") //go strings.ToUpper("Go") //GO
-
将字符串左右两边的空格去掉:
strings.TrimSpace(" tn a lone gopher ntn ")
-
将字符串左右两边指定的字符去掉:
strings.Trim("!?hello?", "?!") //将左右两边的?和!去掉
-
将字符串左边指定的字符去掉:
strings.TrimLeft("!?hello?", "?!") //将左边的?和!去掉
-
将字符串右边指定的字符去掉:
strings.TrimRight("!?hello?", "?!") //将右边的?和!去掉
-
判断字符串是否以指定的字符串开头:
strings.HasPrefix("ftp://192.168.1.1", "ftp") //true
-
判断字符串是否以指定的字符串结束:
strings.HasSuffix("NLT_abc.jpg", "abc") //false
6.4 时间和日期相关函数
-
时间和日期相关函数,需要导入time包
-
time.Time类型,用于表示实时间
-
查看日期信息
//1.获取当前时间
now := time.Now()
fmt.Println("当前时间为:", now) //当前时间为: 2020-05-12 11:17:16.561951 +0800 CST m=+0.000098979
//2.通过now可以欧取到年月日时分秒
fmt.Printf("年 = %v\n", now.Year()) //年 = 2020
fmt.Printf("月 = %v\n", now.Month()) //月 = May,可以用int()转为数字5
fmt.Printf("日 = %v\n", now.Day()) //日 = 12
fmt.Printf("时 = %v\n", now.Hour()) //时 = 11
fmt.Printf("分 = %v\n", now.Minute()) //分 = 20
fmt.Printf("秒 = %v\n", now.Second()) //秒 = 1
-
格式化日期时间
-
使用Printf或者Springtf
-
使用time.Format()方法:2006/01/02 15:04:05这几个数字不能修改,但是/可以改成-或者其他,各个数字也可以自由组合。
-
//3.格式化日期时间:方式1
fmt.Printf("当前年月日 %d-%d-%d %d:%d:%d", now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(), now.Second()) //当前年月日 2020-5-12 11:23:15
dateStr := fmt.Sprintf("当前年月日 %d-%d-%d %d:%d:%d", now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(), now.Second())
fmt.Printf("dateStr = %v\n", dateStr) //dateStr = 当前年月日 2020-5-12 11:24:31
//方式2:数字不能修改,这是固定的格式,但是/可以改成-或者空格
fmt.Printf(now.Format("2006/01/02 15:04:05")) //2020/05/12 11:26:44
fmt.Println()
fmt.Printf(now.Format("2006/01/02")) //2020/05/12
fmt.Println()
fmt.Printf(now.Format("15:04:05")) //11:26:44
fmt.Println()
- 时间的常量
常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到100毫秒:100*time.Millisecond
,不能用除法来表示。 - time的Unix和UnixNano方法
都是从January 1, 1970 UTC到时间点t所经过的时间,Unix单位是秒,UnixNano的单位是纳秒。
6.5 内置函数
- len:用来求长度,如:string、array、slice、map、channel。
- new:用来分配内存,主要用来分配值类型,比如int、float32、struct…返回的是指针。
- make:用来分配内存,主要用来分配应用类型,比如channel、map、slice。
6.6 错误处理
- 基本说明
- Go语言追求简洁优雅,所以不支持传统的try-catch-finally这种方式。
- Go中引入的处理方式是:defer、panic、recover。
- 这几个异常的使用场景可以简单的描述为:Go中可以抛出一个panic异常,然后在defer中通过recover捕获这个异常,然后正常处理。
- 使用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 = ", 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 test02() {
err := readConf("config.ini")
if err != nil {
//读取文件错误,输出这个错误,并终止程序
panic(err)
}
fmt.Println("test02()后面的代码...")
}
func main() {
//测试
test02()
fmt.Println("main()下面的代码...")
}