函数
基本语法
func 函数名 (形参列表) (返回值列表) {
执行语句...
return 返回值列表
}
函数可以有返回值,也可以没有
包的引入
包的本质实际上就是创建不同的文件夹,来存放程序文件
go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的
包的作用
-
区分相同名字的函数、变量等标识符
-
当程序文件很多时,可以很好的管理项目
-
控制函数、变量等访问范围,即作用域
包的基本语法
打包:package 包名
引入包:import "包的路径"
示例:
//main.go
package main
import(
"fmt"
"go_code/project2/utils"
)
func main(){
var n1 float64 =1.2
var n2 float64 =3.2
var operator byte ='-'
res :=utils.Cal(n1,n2,operator)
fmt.Printf("res=%T, res=%f",res,res)
}
//utils.go
package utils
import(
"fmt"
)
func Cal(n1 float64, n2 float64, operator byte) (float64){ //Cal要大写
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
}
-
由于go是以包的形式来管理文件和项目目录结构的,则一个包就是一个文件夹,因此go的每个文件都要属于一个文件夹(包)
-
包的路径:环境变量
${GOPATH}/src
开始,到包的文件夹
包的注意事项
-
在给一个文件打包时,该包对应一个文件夹。比如utils文件夹对应的包名就是utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
-
当一个文件要使用其他包函数或变量时,需要先引入对应的包
-
package在第一行,然后是import
-
在import包时,路径从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入
-
-
为了让其他包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其他语言的public,这样才能跨包访问。
-
在访问其他包函数时,语法是
包名.函数名
-
如果包名较长,Go支持给包取别名,取别名后,原来的包名就不能使用了
import( "fmt" test_pac "go_code/project2/utils" ) func main(){ var n1 float64 =1.2 var n2 float64 =3.2 var operator byte ='-' res :=test_pac.Cal(n1,n2,operator) fmt.Printf("res=%T, res=%.2f",res,res) }
-
在同一个包下,不能有相同的函数名,否则报重复定义
-
如果要编译成一个可执行文件,就需要将这个包声明为main,即package main。这个就是一个语法规范
编译时需要编译main包所在的文件夹
编译后生成一个有默认名的可执行文件,在$GOPATH目录下,可以指定名字和目录,比如:放在bin目录下:
go build -o bin/my.exe go_code/project/main
。(路径默认带src) -
编译完后会生成一个和src同级的文件夹pkg,里面包含一个.a的库文件
return语句
-
如果返回多个值时,在接收时,希望忽略某个返回值,则使用
_
符号表示占位忽略 -
如果返回值只有一个,(返回值类型列表)可以不写()
函数注意事项和细节
-
函数的形参列表可以是多个,返回值列表也可以是多个
-
形参列表和返回值列表的数据类型可以是值类型和引用类型。
-
函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其他包文件使用,类似public,首字母小写,只能被本包文件使用,其他包文件不能使用,类似private
-
函数中的变量是局部的,函数外不生效
-
基本数据类型和数组默认都是值传递,即进行值拷贝。在函数内修改,不会影响到原来的值
-
如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用
-
Go函数不支持重载!
-
在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
func getSum(n1 int, n2 int) int { return n1 + n2 } func main(){ a := getSum fmt.Printf("a的类型%T,getSum类型是%T\n",a,getSum) res := a(10,40)//等价于 res:= getSum(10,40) fmt.Println("res=",res) }
- 函数既然是一种数据类型,那么在Go中,函数可以作为形参,并且调用
func getSum_Sub(n1 int, n2 int) (sum int, sub int) { sum =n1 + n2 sub =n1 - n2 return } func myFun(funvar func(int,int) (int,int), num1 int, num2 int) (int , int ){ return funvar(num1, num2) } func main(){ a := getSum_Sub fmt.Printf("a的类型%T,getSum类型是%T\n",a,getSum_Sub) res,_ := a(10,40)//等价于 res:= getSum(10,40) fmt.Println("res=",res) b:=myFun fmt.Printf("b的类型%T,myFun类型是%T\n",b,myFun) sum,sub:=b(getSum_Sub,50,20) fmt.Println("sum=",sum,"sub=",sub) }
-
为了简化数据类型定义,Go支持自定义数据类型
基本语法:
type 自定义数据类型名 数据类型
(相当于一个别名,相当于typename)。但是Go语言会认为这两个是不同类型//自定义变量类型 type myInt int var num1 myInt var num2 int num1 =40 num2= int(num1)//需要显示转换成int,因为Go认为两个是不同类型 //自定义函数类型 func getSum_Sub(n1 int, n2 int) (sum int, sub int) { sum =n1 + n2 sub =n1 - n2 return } type mySum func(int,int)(int,int) func myFun(funvar mySum, num1 int, num2 int) (int , int ){ return funvar(num1, num2) }
- 支持对函数返回值命名
func cal(n1 int,n2 int)(sum int, sub int){ sum = n1 + n2 sub = n1 - n2 return }
-
使用
_
标识符,忽略返回值 -
Go中支持可变参数
//支持0到多个参数 func sum(args... int) (sum int){ } //支持1到多个参数 func sum(n1 int, args... int) (sum int){ }
-
args是slice切片,通过args[index] 可以访问到各个值。
-
如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后
func sum_test(n1 int, args... int) (sum int) { sum =n1 //遍历args for i:=0;i<len(args);i++ { sum += args[i] //args[0]表示取出args切片的第一个元素值,其他依此类推 } return } func main(){ res2:= sum_test(10) fmt.Println("res2=",res2) res3 := sum_test(1,2,3,4,5) fmt.Println("res3=",res3) }
-
init函数
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用。
通常可以在init函数中,完成初始化工作
注意事项和细节
-
如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程是变量定义->init函数->main函数
-
init函数最主要的作用,就是完成一些初始化工作。可以在引入包中进行初始化,这样main执行时,先执行包的初始化
如果main.go和utils.go都含有变量定义,init函数时,执行流程如下:
名函数
Go支持匿名函数,如果某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
使用方式
-
在定义匿名函数时直接使用
func main(){ res1 := func (n1 int,n2 int) int { return n1+n2 }(10,20) //定义到}为止,后面(10,20)直接调用 fmt.Println("res1=",res1) }
- 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
a := func (n1 int,n2 int) int { return n1+n2 } res1:=a(1,2) fmt.Println("res1=",res1)
-
全局匿名函数
如果将匿名函数赋值给一个全局变量,那么这个匿名函数,就称为一个全局匿名函数,可以在程序有效
闭包
基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
//累加器
func AddUpper() func(int) int {
var n int =10
return func(x int) int{
n+=x
return n
}
}
func main(){
a:=AddUpper()
var res int
res=a(1)
fmt.Println("res=",res)
res=a(2)
fmt.Println("res=",res)
}
上面代码说明:
-
AddUpper
是一个函数,返回的数据类型是func(int) int
-
闭包说明
var n int =10 return func(x int) int{ n+=x return n }
AddUpper
返回的是一个匿名函数,但是这个匿名函数引用到函数外的n
。因此这个匿名函数就和n形成一个整体,构成闭包。 -
可以理解为:闭包是类,函数是操作,
n
是字段。函数和它使用到的n
构成闭包 -
当我们反复的调用f函数时,因为
n
初始化一次,因此每次调用n
都会进行累加 -
要搞清楚闭包的关键,就是要分析出返回的函数使用到哪些变量。因为函数和它引用到的变量共同构成闭包
Go语言能通过escape analyze识别出变量的作用域,自动将变量在堆上分配。
//由于n1没有形成闭包,因此会在栈上创建,而n在堆上
func AddUpper() func(int) int {//类型为func() func(int) int
var n int =10 //堆上
var n1 int =20 //栈上
return func(x int) int{
n+=x
fmt.Println("n=",n,"n1=",n1)
return n
}
}
a:=AddUpper()//这里不能省略这个(),类似于类的构造函数。称作返回一个闭包 此时a的类型是func(int) int
res=a(1)
fmt.Println("res=",res)//n= 11 n1= 20 res= 11
res=a(1)
fmt.Println("res=",res)//n= 12 n1= 20 res= 12
示例:
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(){
f:=makeSuffix(".jpg")
fmt.Println("output:",f("a.jpg"))
fmt.Println("output:",f("b"))
}
传统函数虽然也能实现,但是每次都需要重新传入后缀名。用闭包可以像类一样确定好就不用再写了
defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时释放资源,Go的设计者提供defer(延时机制)
func sum (n1 int,n2 int) int{
//当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈)
//当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
defer fmt.Println("ok1 n1=",n1)
defer fmt.Println("ok2 n2=",n2)
n1++
n2++
res := n1 +n2
fmt.Println("ok3 res=",res)
return res
}
func main(){
res := sum(10,20)
fmt.Println("res=",res)
}
结果:
ok3 res= 32
ok2 n2= 20
ok1 n1= 10
res= 32
defer的细节说明
-
当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈,然后继续执行函数下一个语句。
-
当函数执行完毕后,再从defer栈中,依次从栈顶取出语句执行(遵从先入后出)
-
在defer将语句放入到栈时,也会将相关的值拷贝同时入栈
主要价值
defer最主要的价值在于,当函数执行完毕后,可以及时的释放函数创建的资源。
func test(){
file = openfile("xxx")
defer file.close()
}
参数传递方式
-
值传递:复制变量
-
引用传递:相当于C++的地址传递,复制地址
变量作用域
和c++差不多
注意:
var Age int =20//ok
Name := "tom" // 报错:相当于 var Name string Name = "tom"。赋值语句不能在函数体外执行
func main(){
fmt.Println("name",Name)
}
可以在函数体外进行全局变量定义及初始化,但是不能赋值
var Age int =20//ok
var Name string //ok
//不能赋值
Name = "tom"//不行
Test:= "tom"//同样不行
字符串常用的系统函数
-
统计字符串的长度,按字节
len(str)
。中文字符3字节,比如hello北
,输出为8 -
字符串遍历,同时处理有中文的问题
r:=[]rune(str)
-
字符串转整数:
n,err := strconv.Atoi("12")
n,err := strconv.Atoi("123") if err!= nil { fmt.Println("转换错误",err) }else { fmt.Println("转成的结果是",n) }
-
整数转字符串:
str = strconv.Itoa(12345)
- 字符串转
[]byte
var bytes = []byte("hello go") fmt.Printf("bytes=%v\n",bytes)
[]byte
转字符串str = string([]byte{97,98,99})//对应结果为abc(对应ascii码)
- 10进制转2,8,16进制(字符串):
str := strconv.FormatInt(123,2) fmt.Printf("123对应的二进制是=%v\n",str) str = strconv.FormatInt(123,16) fmt.Printf("123对应的16进制是=%v\n",str)
- 查找子串是否在指定的字符串中,返回
bool
b:=strings.Contains("seafood","foo") fmt.Printf("是否含有=%v\n",b)
- 统计一个字符串有几个指定的子串:
num := strings.Count("ceheEs","e") fmt.Printf("num=%v",num)//结果:2
- 不区分大小写的字符串比较(==是区分字母大小写的)
b = strings.EqualFold("abc","Abc") fmt.Printf("b=%v\n",b)//true
- 返回子串在字符串第一次出现的index值,如果没有,返回
-1
index := strings.Index("NLT_abc","abc") fmt.Printf("index=%v\n",index)//4
- 返回子串在字符串最后一次出现的index,如没有,返回
-1
index := strings.LastIndex("go golang","go") fmt.Printf("lasindex=%v\n",index)//3
- 将指定的子串替换成另外一个子串:
func Replace(s, old, new string, n int) string
(n
可以指定你希望替换几个,如果n=-1
表示全部替换)str := strings.Replace("go golang","go","北京",-1)//返回一个新串 fmt.Printf("str=%v\n",str)
- 按照指定的某个字符(如
,
)为分割标识,将一个字符串拆分成字符串数组strArr := strings.Split("hello,world,ok",",") fmt.Printf("strArr=%v\n",strArr)
- 将字符串的字母进行大小写转换:
str="Hello goLang" str=strings.ToLower(str)//转小写,ToUpper转大写 fmt.Printf("str=%v\n",str)//hello golang
- 将字符串左右两边的空格去掉:
str=strings.TrimSpace(" Hello goLang ") fmt.Printf("str=%q\n",str)//%q 会加上双引号:"Hello goLang"
- 将字符串左右两边指定的字符去掉:
str=strings.Trim("! ! Hello goLang! !!"," !") fmt.Printf("str=%q\n",str)
-
去掉左边(或右边):
strings.TrimLeft
(strings.TrimRight
) - 判断字符串是否以指定的字符串开头:
b=strings.HasPrefix("ftp://192.168.10.1","ftp") fmt.Printf("b=%v\n",b)
-
判断字符串是否以指定的字符串结束:
strings.HasSuffix
时间和日期相关函数
-
需要导入
time
包 -
time.Time
类型,用来表示时间now := time.Now()//获取当前时间 fmt.Printf("Now: %v,类型%T",now,now) //Now: 2022-08-03 15:47:48.2543597 +0800 CST m=+0.003611801,类型time.Time
- 如何获取到其他日期信息
//通过now可以获取到年月日,时分秒 fmt.Printf("年: %v\n",now.Year()) //年: 2022 fmt.Printf("月: %v\n",now.Month()) //月: August fmt.Printf("月: %v\n",int(now.Month())) //月: 8 fmt.Printf("日: %v\n",now.Day()) //日: 3 fmt.Printf("时: %v\n",now.Hour()) //时: 15 fmt.Printf("分: %v\n",now.Minute()) //分: 53 fmt.Printf("秒: %v\n",now.Second()) //秒: 0
-
格式化日期时间
方式1:就是使用Printf或者Sprintf
fmt.Printf("当前年月日 %d-%d-%d %d:%d:%d\n",now.Year(),now.Month(),now.Day(), now.Hour(),now.Minute(),now.Second()) dateStr:= fmt.Sprintf("当前年月日 %d-%d-%d %d:%d:%d",now.Year(),now.Month(),now.Day(), now.Hour(),now.Minute(),now.Second()) fmt.Println(dateStr) //当前年月日 2022-8-3 15:58:19 //当前年月日 2022-8-3 15:58:19
方式2:
fmt.Printf(now.Format("2006/01/02 15:04:05"))
这个时间是固定的(间隔符可以改变),必须这么写。但是可以自由组合,只显示日期,或者时间
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() /* 2022/08/03 16:02:19 2022-08-03 16 02 19 */
-
时间的常量
定义在time包中
const ( Nanosecond Duration = 1//纳秒 Microsecond = 1000 * Nanosecond//微秒 Millisecond = 1000 * Microsecond//毫秒 Second = 1000 * Millisecond//秒 Minute = 60 * Second//分钟 Hour = 60 * Minute//小时 ) //使用 100*time.Millisecond
- 结合使用
sleep()
使用常量i:=0 for { i++ fmt.Println(i) //休眠 time.Sleep(time.Millisecond*20)//只能乘以大于1的整数,不能小于或者除 if i==10{ break } }
-
Unix时间戳和Unixnano时间戳(作用是可以获取随机数字)
下面的形式叫方法,与函数有一些区别
fmt.Printf("Unix:%v,UnixNano:%v\n",now.Unix(),now.UnixNano()) //Unix:1659514601,UnixNano:1659514601927962800
- 获取间隔秒数
start := time.Now().Unix() test() end:= time.Now().Unix() fmt.Printf("执行test()耗费的时间为%v秒",end-start)
内置函数
-
len
-
new:用来分配内存,主要用来分配值类型,如
int
,float64
,返回指针num1 :=100 fmt.Printf("num1的类型%T, 值=%v, 地址 %v\n",num1,num1,&num1) num2 := new(int) *num2 =200 fmt.Printf("num2的类型%T, 值=%v, 地址 %v ,存的值=%v\n",num2,num2,&num2,*num2) /* num1的类型int, 值=100, 地址 0xc000014088 num2的类型*int, 值=0xc0000140c0, 地址 0xc000006030 存的值=200 //因为是指针,所以值是地址 */
-
make:用来分配内存,主要用来分配引用类型,比如
channel
、map
、slice
。
错误处理机制
-
在默认情况下,当发生错误后(panic),程序就会退出
-
如果希望发生错误时,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示
基本说明
-
不支持
try...catch...finally
-
Go处理方式:
defer
,panic
,recover
-
Go中可以抛出一个
panic
的异常,然后在defer
中通过recover
捕获这个异常,然后正常处理package main import( "fmt" "time" ) func test(){ //使用defer + recover 来捕获和处理异常 defer func(x int){ err := recover() //recover()内置函数,可以捕获到异常 if err != nil { //说明捕获到错误 fmt.Println("err=",err,x) //这里就可以将错误信息发送给管理员... fmt.Println("发送给管理员") } }(1)//匿名函数调用加个()就行,因为此函数不用参数,如果有参数就填入参数 num1 :=10 num2 :=0 res := num1/num2 fmt.Println("res=",res) } func main(){ for{ test() fmt.Println("main()下面的代码...") time.Sleep(time.Second) } }
自定义错误
Go程序中,也支持自定义错误,使用erroes.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("config2.ini") if err != nil { //如果读取文件发生错误,就输出这个错误,并终止程序 panic(err) } fmt.Println("test02继续执行....") } func main(){ test02() fmt.Println("main()下面的代码...") }