go学习之函数知识

函数

文章目录

    • 函数
      • 1.函数入门
        • (1)为什么需要函数?
        • (2)什么是函数:
        • 2.包
        • 3.函数的调用机制
          • 通俗理解
          • 调用过程:
          • return语句
          • 递归调用
        • 4.函数注意事项和细节讨论
        • 5.init函数
        • 6.匿名函数
        • 7.闭包
        • 8.defer
        • 9.函数参数的传递方式
        • 10.字符串中常用的函数
        • 11.时间和日期相关的函数
        • 12.内置函数
        • 13.go的错误处理机制

1.函数入门

(1)为什么需要函数?

完成一个需求:输入两个数,再输入一个运算符(±*/),得到结果

使用传统方法:

func main(){
	/*
    请大家完成这样一个需求:
	输入两个数,在输入一个运算符(+-*   / ,得到结果)*/
	var n1 float64 =1.2
	var n2 float64 =2.3
	var operator byte ='/'
	var res float64
	switch operator {
	case '+':
          res = n1 + n2
	case '-':
		  res = n1 - n2
	case '*':
		  res = n1 * n2
	case '/':
		  res = n1 / n2	  
	default:
		 fmt.Println("操作符错误")  
	}
	fmt.Println("res=",res)
}

分析代码上的问题:

  • 代码冗余太长
  • 不好改动,不利于代码进行维护
  • 函数可以解决这个问题
(2)什么是函数:

为完成某一个功能的程序指令(语句)的集合,称为函数

在Go中,函数分为:自定义函数、系统函数(查看GO编程手册)

函数基本语法:

func 函数名 (形象列表) (返回值列表){
   执行语句...
   return 返回值列表
}

1)形参列表:表示函数的输入

2)函数中的语句:表示为了实现某一个功能的代码块

3)函数可以有返回值,也可以没有

案例入门:

//将计算的功能放到一个函数中,在需要的时候进行调用
func cal (n1 float64,n2 float64,operator byte) float64{
	var res float64
	switch operator {
	case '+':
          res = n1 + n2
	case '-':
		  res = n1 - n2
	case '*':
		  res = n1 * n2
	case '/':
		  res = n1 / n2	 
    case '%':
		  res = n1 % n2    
	default:
		 fmt.Println("操作符错误")  
	}
	return res
}
//调用函数:
func main(){
result := cal(3.2,2.1,'-') //输入参数就可以对这个函数调用
fmt.Println("result=",result) //1.1
}
2.包

为什么要用包

1)在实际的开发中,我们往往需要在不同的文件中,去调用其他文件的定义的函数,比如main.go中,去使用utils.go文件中的函数,如何实现?

2)现在有两个程序员共同开发一个go项目,两个程序员都想定义定义个交cal名字的函数怎么办

包的原理图

包的本质实际上就是创建不同的文件夹,来存储程序文件

此图用来说明一下包的原理图:

go学习之函数知识_第1张图片

包的示意图

go学习之函数知识_第2张图片

包的介绍

包的基本概念:

说明:go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的

包的三大作用

  • 区分相同名字的函数、变量等标识符
  • 当程序文件很多时,可以很好的管理项目
  • 控制函数、变量等访问访问范围,即作用域

打包的基本语法

pacakage 包名

引入包的基本语法

import "包的路径"

包使用的快速入门案例

go学习之函数知识_第3张图片
utils包:
package utils

import (
“fmt”
)

//将计算的功能放入一个函数中,然后在需要的时候进行使用
//为了让马哥其他的包文件使用Cal函数,需要将c大写,类似java中public
func Cal (n1 float64,n2 float64,operator byte) float64{

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

}

引入utils包
import (
“fmt”
“go_code/functions/utils”
)
调用
func main(){
result := utils.Cal(3.2,2.1,‘-’)
fmt.Println(“result=…”,result)
}
//这样就完事了


包的注意事项以及细节

1)在给一个文件打包时,该包对应一个文件夹,比如在这里的utils文件夹对应的包名就是utils,文件的包名通常和文件夹名一致,一般为小写字母

2).当一个文件要使用其他的包函数或变量是需要先引入对应的包

引入方式1: import “包名”

引入方式2:

import(
“name”
“name”

)


3)pacakage指令在文件第一行,然后是、import指令

4)在import包是路径从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入

5)为了让其他包的文件,可以访问到本包的函数,则该函数名的首字母需要大写

6)在访问其他包的函数名时,其语法是包.函数,

utils.Cal(1,2)


7)如果包名较长,Go支持给包取别名,注意细节:取别名后,原来的包名就不能使用了

```go
import (
	"fmt"
	//前面的是别名
	ut "go_code/functions/utils"
)
//别名调用,原来的包名就会失效,就必须使用别名调用
result := ut.Cal(3.2,2.1,'-')
	fmt.Println("result=..",result)

8)在同一个包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义的错误

9)如果你要编译成一个可执行文件,就需要将这个包声明为main,即pacakage main这就是一个语法规范,如果你写一个库,包名可以自定义

go学习之函数知识_第4张图片

注意:-o表示输出 bin\my.exe表示在当前目录中的bin下面

3.函数的调用机制
通俗理解

go学习之函数知识_第5张图片

调用过程:

go学习之函数知识_第6张图片

对上图的说明:

(1)在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他栈的空间区分开来

(2)在每个函数对应的栈中,数据空间都是独立的,不会混淆

(3)当一个函数调用完毕后,程序会销毁这个函数的栈空间

案例理解:

1)传入一个数进行加1的操作

package main

import (
	"fmt"
)
//编写一个函数 test
func test(n1 int){
	n1 = n1 + 1
    fmt.Println("n1=",n1)
}
func main(){
	n1 :=10
  
  //调用test
  test(n1)

}

2)计算两个数,并返回 getSum

func getSum(n1 int,n2 int) int {
//当函数有、return语句时,就是将结果返回给调用者
//即谁调用我,我就返回给谁
	return n1 + n2
}
//调用
func main(){
	n1 :=10
  
  //调用test
  test(n1)
  sum := getSum(1,3)
  fmt.Println("getSum得到的值是:",sum)//4

}
return语句

基本语法

Go函数支持返回多个值,这一点是其他编程语言没有的

func 函数名 (形参列表) (返回值类型列表){
    语句...
    return 返回值列表
}

注意:

(1)如果返回多个值时,在接收时,希望忽略某个返回值则使用_符号表示占位忽略

(2)如果返回值只有一个,(返回值类型列表) 可以不写()

案例演示

请编写函数,可以计算两个数的和和差,并返回结果

func getSumAndSub(n1 int,n2 int) (int,int) {
	sum := n1 +n2
	sub := n1 -n2
	return sum,sub
}
调用
func main(){
  //调用getSumAndSub()函数
  res1,res2 := getSumAndSub(1,2)
  fmt.Printf("res1=%v,res2=%v",res1,res2)//res1=3 res2=1
}

一个细节说明:希望忽略某个返回值则使用_符号表示占位忽略

//希望忽略某个返回值则使用_符号表示占位忽略
_, res3 := getSumAndSub(1,2)
fmt.Println("res3=",res3) //1-2=-1

递归调用

基本介绍:

一个函数在函数体内又调用了本身,我们称为递归调用

案例入门:

func test(n int){
   if n > 2 {
       n--
       test(n)
   }
   fmt.Println("n=",n) //n=2 n=2 n=3
   //这里是if里面执行完了就会执行下面的输出
}
func test2 (n int) { //这里走了if就不会走else,
    if n > 2{
      n-- //递归必须向退出递归逼近
      test2(n)
    }else{
       fmt.Println("n=",n) //n=2 
    }
}
以上代码传入test(4)分别输出什么

![在这里插入图片描述](https://img-blog.csdnimg.cn/79c35b1280e64b798cf1a9cf06abcf67.jpeg#pic_center)

函数递归需要遵守的重要原则

1)执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)

2)函数额局部变量是独立的,不会相互影响

3)递归必须向退出递归的条件逼近,否则就是无限递归了,死龟了

4)当一个函数执行完毕,或遇到return就会返回遵守递归调用,就将结果返回给谁,当函数执行完毕或者返回时,该函数也会被销毁

117集

练习题:

题1:斐波那契数列

请使用递归的方式,求出斐波那契数1,1,2,3,5,8,13…给你一个整数n求出他的值是多少

//斐波那契数
 func feibo(n int) int{
	if n==1 || n==2{
		return 1
	}else{
	return feibo(n-1)+feibo(n-2)
	}
 }

题2:求函数值

已知f(1)=3; f(n)= 2*f(n-1)+1

请使用递归的思想编程,求出f(n)的值

//求函数的值 当f(1)=3,f(n)=2*f(n-1)+1
func han(n int) int {
	if n==1{
		return 3
	}else{
		return 2*han(n-1)+1
	}
}

题3猴子吃桃问题

有一堆桃子,猴子第一天吃了其中的一半,并且多吃了一个:以后每天猴子都吃了其中的一半,然后再多吃一个,想再吃时(还没吃),发现只有1个桃子。问题:最初共有多少个桃子

思路分析:

  1. 第10天只有1个桃子
  2. 第九天有几个桃子=(第10天桃子数 + 1)*2
  3. 规律第n天桃子数peach(n)=(peach(n+1)+1)*2
//猴子吃桃问题
func MonkeyPeach(n int) int {
	if n>10 || n<0{
		fmt.Println("输入的天数不对")
        return 0
	}
	if n==10 {
		return 1
	}else{
		return (MonkeyPeach(n+1)+1)*2
  }
}

//反过来
func maonkey2(n int) int {
	if n==1 {
		return 1
	}else{
		return (maonkey2(n-1)+1)*2
	}
}
4.函数注意事项和细节讨论

1)函数的形参列表可以是多个。返回值列表也可以是多个

2)形参列表和返回值列表的数据类型可以是值类型和应用类型

3)函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其他包文件使用,类似于public,首字母小写,只能被本包文件使用,其他包文件不能使用,类似于private

4)函数中的变量是局部的,函数外不生效

package main
import (
	"fmt"
)
//函数中的变量是局部的,函数外不生效
func test(){
	//n1是test函数的局部变量,只能在test函数中使用
	var n1 int = 10

}
func main(){
	//这里不能使用n1,因为n1是test函数的局部变量
   fmt.Println("n1=",n1)
}

5)基本数据类型和数组默认都是值传递,即进行值拷贝。在函数内修改,不会影响到原来的值。

func test2(n1 int) {
   n1 = n1 + 10
   fmt.Println("test2(n1)=",n1)
}
func main(){
	//这里不能使用n1,因为n1是test函数的局部变量
  // fmt.Println("n1=",n1)
  n1 := 20
  test2(n1)//30 不会影响到外面的值
  fmt.Println("n1=",n1) //20


}

6)如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上来看类似引用

//n1就是*int类型
func test3(n1 *int) {
	*n1 = *n1 + 10
	fmt.Println("test03(n1)=",*n1)//30
 }
func main(){
	
num := 20
test3(&num)//传的是地址,函数内部通过指针修改变量
fmt.Println("main() num=",num)//30

}

go学习之函数知识_第7张图片

7)Go函数不支持重载

func test2(n1 int) {
	
   n1 = n1 + 10
   fmt.Println("test2(n1)=",n1)
}
func test2(n1 int,n2 int) {//错误不支持重载会报重复定义的错
	
}

8)在Go中,**函数也是一种数据类型,**可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数进行调用。

package main
import (
	"fmt"
)
//在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)
}

9)函数既然是一种数据类型,因此在go中,函数可以作为形参,并且调用

package main
import (
	"fmt"
)
//在go中,函数也是一种数据类型
//可以赋给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数进行调用
func getSum(n1 int,n2 int) int{
	return n1+n2
}

//函数既然可以作为一种数据类型,因此在Go中,函数可以作为形参,并且调用
func myFunc(funvar func(int,int)int,num1 int,num2 int) int{
	return funvar(num1,num2)
}
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)

   //看案例
   res2 :=myFunc(getSum,50,60)
   fmt.Println("res2=",res2) //110

}

10)为了简化数据类型定义,Go支持定义数据类型

基本语法:type 自定义数据类型名 数据类型 //理解:相当于一个别名
案例:type myInt int //这时myInt就等价int来使用了
-----
 type myInt int //给int取了别名 在go中myInt和int虽然都是
   //int类型,但是在go中还是认为myInt 和int是不同的数据类型

   var num1 myInt
   var num2 int
   num1 = 40
  // num2= num1 //报错不是同一个类型
  num2 = int(num1) //这样进行转换就可以
   fmt.Println("num1=",num1) //40
---------------------------------------   
  
案例:type mySum func(int,int)int //这时mySum就等价一个函数类型 func(int,int)int
 -----  
    //在举一个案例
 type myFunt func(int,int)int //这时myFun就是 func(int,int)int类型

 func myFunc2(funvar myFunt,num1 int,num2 int) int{
	return funvar(num1,num2)
}
//main函数进行调用
//案例结果
   res3 :=myFunc2(getSum,500,600)
   fmt.Println("res2=",res3) //1100
}
-------------------------------------

11)支持对函数返回值命名

func cal(n1 int,n2 int) (sum int,sub int){
sum = n1 +n2
sub = n1 - n2
return  //这样就不用这样return sum sub之类额
}

//main中调用
 a,b := cal(1,2)
   fmt.Printf("a=%v,b=%v",a,b) //3,-1,a是sum b是sub
}

12)使用_标识符,忽略返回值

func cal(n1 int,n2 int) (sum int,sub int){
sum = n1 +n2
sub = n1 - n2
return 
}
fun main(){
res,_:=cal(10,20)
fmt.Printf("res=%d",res1) //30
}

13)Go支持可变参数

//支持0到多个参数
func sum(arg.. int)sum int{
}
//支持1到多个参数
func sum(n1 int,args.. int)sum int{
}

说明

(1)args是slice,通过args[index]可以访问到各个值

(2)案例演示:编写一个函数sum,可以求出1到多个int的和

(3)如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后

package main

import (
	"fmt"
)

//编写一个sum函数,可以求出 1到多个int的和
func sum(n1 int,args... int) int{
	sum := n1
	//遍历arges
	for i :=0;i <len(args);i++{
		sum += args[i] //args[0]表示取出args切片的第一个元素的值,其他依次类推
	}
	return sum
}
func main(){
//测试可变参数的使用
res :=sum(10,0,-1,90,10)
fmt.Println("res=",res)
}

函数练习题

func sum2(n1,n2 float32)float32{
	fmt.Printf("n1 type=%T\n",n1) //n1 type=float32
	return n1 +n2
}
func main(){
fmt.Println("sum2=",sum2(1,2)) //3


}

go学习之函数知识_第8张图片

最后一句会报错因为myfunc就接收两个int的参数但是b是三个int的参数,类型不匹配。

练习题三

请编写一个函数swap(n1 int,n2 int)可以交换n1和n2的值

func swap(n1 *int,n2 *int){
	//定义一个临时变量
	t :=*n1
	*n1=*n2
	*n2=t
} 
func main(){
a :=10
b :=20
fmt.Printf("a=%v,b=%v",a,b)//10,20
swap(&a,&b)//传入的是地址
fmt.Printf("a=%v,b=%v",a,b) //20,10
}
5.init函数

基本介绍

每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用

案例说明

package main
import (
	"fmt"
)

//init函数,通常可以在init函数中完成初始调用
func init(){//先执行
	fmt.Println("init()....")
}

func main(){
   fmt.Println("main()....")
}
//执行结果
D:\myfile\GO\project\src\go_code\functions\funint>go run main.go
init()....
main()....

init函数的注意事项和注意细节

1)如果一个文件同时包含变量定义,init函数和main函数,则执行的流程是先init函数后main函数

package main
import (
	"fmt"
)

var age = test()

//为了看到全局变量是先被初始化的,我们这里先写函数
func test()int{  //1
	fmt.Println("test")
	return 90
}

//init函数,通常可以在init函数中完成初始调用
func init(){//先执行 2
	fmt.Println("init()....")
}

func main(){ //3
  fmt.Println("main()...age=",age)
}

2)init函数最主要的作用,就是完成一些初始化的工作

utils.go
package utils
import (
	"fmt"
)
var Age int
var Name string
//Aage 和name全局变量我们需要在main.go中使用
//但是我们需要初始化Age和Name

//init函数完成初始化
func init(){
	fmt.Println("init函数执行")
  Age = 100
  Name= "初始化"
}
//调用
package main
import (
	"fmt"
	//引入包
	"go_code/functions/funint/utils"
)

var age = test()

//为了看到全局变量是先被初始化的,我们这里先写函数
func test()int{  //1
	fmt.Println("test")
	return 90
}

//init函数,通常可以在init函数中完成初始调用
func init(){//先执行 2
	fmt.Println("init()....")
}

func main(){ //3
  fmt.Println("main()...age=",age)
  fmt.Println("Age=",utils.Age,"Name",utils.Name)
}

3)面试题:案例如果main.go和utils.go都含有变量定义,init函数时,执行的流程又是怎么样

go学习之函数知识_第9张图片

6.匿名函数

介绍:

Go支持匿名函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次使用

匿名函数使用方式1

在定义匿名函数是就直接调用,这种方式匿名函数只能调用一次

func main(){
	//在定义匿名函数是就直接调用,这种方式匿名函数只能调用一次
   
	//案例演示,求两个数额和,使用匿名函数实现
	res1 :=func (n1 int,n2 int)int {
		return n1+n2
	}(10,20)

	fmt.Println("res1=",res1)  //30
}

匿名函数使用方式2

将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数

//第二种方法,将匿名函数func (n1 int,n2 int)int献给a变量
	//则a的数据类型就是函数类型,此时我们可以通过a完成调用
	a := func (n1 int,n2 int)int{
		return n1 -n2
	}
	res2 := a(10,30)
	
	fmt.Println("res2=",res2) //-20

全局匿名函数

如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效

package main
import (
	"fmt"
)
var (
	Fun1 =func (n1 int,n2 int) int {
		return n1*n2
	}
)

func main(){
	//全局匿名函数的使用
	res4 := Fun1(4,9)
    fmt.Println("res4=",res4)//36
}
7.闭包

介绍:

闭包就是一个函数与其相关的引用环境组合成一个整体(实体)

案例演示:

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
}

对上面代码的说明和总结

1)AddUpper是一个函数,返回额度数据类型是fun(int)int

2)闭包的说明:

var n int = 10
	return func (x int)int{
		n = n+ x
		return n
	}

返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成了一个整体,构成闭包

3)可以这样理解:闭包是类,函数是操作,n是字段

4)当我们反复调用f函数时,因为n是初始化1次,因此每调用一次就进行累加

5)我们要搞清楚闭包的关键,就是要分析出返回的函数他使用(引用)到哪些变量,因为函数和它引用的变量是一个整体

//累加器
func Addupper() func (int) int {
	var n int = 10
	var str string ="hello"
	return func (x int)int{
		n = n+ x
		str += "x"
		fmt.Println("str=",str)
		return n
	}
}
func main(){
	//使用前面的代码
	f := Addupper()
	fmt.Println(f(1)) //11
	fmt.Println(f(2)) //13
	fmt.Println(f(3)) //16
}
//运行结果;
str= hellox
11
str= helloxx
13
str= helloxxx
16

闭包的最佳实践

请编写一个程序,具体要求如下

1)编写一个函数makeSuffix(suffix string)可以接收一个文件后缀名(比如.jpg).并返回一个闭包

2)调用闭包,可以传入一个文件名,如果改文件名没有指定的后缀(比如.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(){
//测试makeSuffix使用
//返回一个闭包
f :=makeSuffix(".jpg")
fmt.Println("文件名处理后=",f("winter")) //输出winter.jpg
fmt.Println("文件名处理后=",f("bird.jpg")) //输出bird.jpg
}

代码说明:

1)返回的函数和makeSuffix(suffix string)的suffiix变量和返回的函数组合成一个闭包,因为返回的函数引用到suffix这个变量

2)我们体会一下闭包的好处,如果使用传统的方法,也可以实现这个轻松的功能,但是传统方法需要每次都传入后缀名,比如.jpg,而闭包可以保留上次引用的某个值,所以我们传入一次就可以反复使用,仔细体会把

//传统方法
func makeSuffix2(suffix string,name string) string{
    
		//如果name没有指定的后缀,则加上。否则就返回原来的名字
	   if !strings.HasSuffix(name,suffix){
		return name +suffix
	   }
	   return name
	}
	func main(){
//传统方法就要传入两个参数
fmt.Println("文件名处理后=",makeSuffix2(".jpg","winter")) //输出winter.jpg

}
8.defer

为什么需要defer

在函数中,程序员经常需要创建资源(比如,数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)

快速入门案例

package main
import (
	"fmt"
)
func sum(n1 int,n2 int)int{
    //当执行到defer时,暂时不会执行会将defer后面的语句压入到独立的栈中(defer栈)
    //当函数执行完毕后,再从defer栈,按陷入后出的方式中出栈,执行
	defer fmt.Println("ok1 n1=",n1)
	defer fmt.Println("ok2 n2=",n2)
	res := n1 + n2
	fmt.Println("ok3 res=",res)
	return res

}
func main(){
 sum(10,20)
 //输出:
//  ok3 res= 30
// ok2 n2= 20
// ok1 n1= 10
// }

defer的注意事项和最佳实践

1)当go执行到了一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中,然后继续执行函数的下一个语句

2)当函数执行完毕后,从defer栈中,依次从栈顶取出语句执行(遵从栈 先入后出的机制)

3)在defer将语句放入栈中也会将相关的值拷贝同时入栈

func sum(n1 int,n2 int)int{
	defer fmt.Println("ok1 n1=",n1)
	defer fmt.Println("ok2 n2=",n2)
	n1++ //n1=11
    n2++ //n2=21
    res := n1 + n2
	fmt.Println("ok3 res=",res)
	return res

}
func main(){
 sum(10,20)
 }
//输出结果仍然不变
ok3 res= 32
ok2 n2= 20
ok1 n1= 10

defer主要的价值是在:当函数执行完毕后,可以及时的释放函数创建的资源

func test(){
//关闭文件资源
file = openfile(文件名)
defer file.close()
}

func test2(){
//释放数据库资源
connect = openDatabase()
defer connect.close()
//其他代码
}

1)在golang编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的连接,或者是锁资源,可以理解为defer file.Close() defer connect.Close())

2)在defer后,可以继续创建资源

3)当函数完毕后,系统依次从defer栈中,取出语句,关闭资源

4)这种机制非常简洁,程序员们不用再为什么时机关闭资源而烦心。

9.函数参数的传递方式

基本介绍

我们在讲解函数注意事项和使用细节时,已经讲过值类型和引用类型了,这里我们在系统的总结一下,因为这是重难点,值类型参数默认就是值传递了,而引用类型参数默认就是引用传递

两种传递方式

1)值传递

2)引用传递

其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是拷贝引用传递的是地址的拷贝,一般来说,地址拷贝的效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低

值类型和引用类型

1)值类型:基本数据类型int系列,float系列,bool,string、数组和结构体struct

2)引用类型:指针、slice切片、map、管道cha、interface等都是引用类型

值传递和引用传递的使用特点

1)值类型默认值传递:变量直接存储值,内存通常在栈中分配

2)引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆中分配,当任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC回收

变量作用域

说明

1)函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部

package main
import (
	"fmt"
)
var name string="tom" //全局变量
func test01(){
	fmt.Println(name)
}

func test02(){//编译器采用就近原则
	name ="jack"
	fmt.Println(name) //jack
}

func main(){
fmt.Println(name) //tom
test01()//tom
test02()//jack
test01()//jack

2)函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果首字母为大写,则作用域在整个程序有效

package main
import (
	"fmt"
)

var age int= 50
var Name string= "jhon"
//函数
func test(){
	//age和Name作用域就只在test函数内部
	 age := 10
	 Name := "tom"
	fmt.Println("age=",age)
	fmt.Println("Name=",Name)
}
func main(){
	test() //10 tom
//   fmt.Println("age=",age) //会报错
  fmt.Println("age=",age)//50 
  fmt.Println("Name=",Name)//jhon
}

3)如果变量是在一个代码块,比如for/if中,那么这个变量的作用域就在该代码块

for i :=0 ;i<=10;i++{
	fmt.Println("i=",i)
  }
//  fmt.Println("i=",i)会报错

下列代码是否报错

var Age int = 20
Name :="tom" //相当于 var Name string Name="tom"在函数外不能够这样写
func main(){
fmt.Println("name",name1)
}

函数的课堂练习

1)函数可以没有返回值,编写一个函数,从终端输入一个整数,打印对应的金字塔

//打印金字塔的案例
func jinzi(){
	var toleve int
	fmt.Println("请输入toleve值:")
	fmt.Scanln(&toleve)
	for i := 1;i<=toleve;i++{ //行
	   //在打印星号前打印空格
	   for k :=1;k<=toleve-i;k++{
		   fmt.Print(" ")
	   }
	   for j :=1;j<=2*i-1;j++{
		 fmt.Printf("*")
	   }
	   fmt.Println(" ")
	}
   }	

2)编写一个函数从终端输入一个整数(1-9),打印出对应的九九乘法表

//打印九九乘法表的函数
func chengfa(){
	var n int 
	fmt.Println("请输入n的值:")
	fmt.Scanln(&n)
	for i :=1; i<=n;i++{
       for j :=1;j<=i;j++{
		fmt.Printf("%v*%v=%v ",i,j,i*j)
	   }
	   fmt.Println(" ")
	}
10.字符串中常用的函数

说明:字符串在我们程序开发中,使用的是非常多的,常用的函数需要参考官方编程手册

1)统计字符串的长度,按字节len(str)

func main(){
	//统计字符串的长度,按字节len(str)
    str := "hello"
    str2 := "hello我"//go的编码统一utf8(asciii的字符(字母和数字)占一个字节,一个汉字占三个字节)
	fmt.Println("str len =",len(str)) //str len =5
	fmt.Println("str2 len =",len(str2)) //str len =8(5个字母一个汉字)
}

2)字符串遍历,同时处理有中文的问题r :=[]rune(str)

str3 := "hello北京"
   //字符串遍历,同时处理有中文的问题r :=[]rune(str)
   r := []rune(str3)
   for i :=0;i< len(r); i++{
	fmt.Printf("字符=%c\n",r[i])
   }

3)字符串转整数: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) //转成的结果是123
   }
//如果传入hello
/字符串转整数:n,err :=strconv.Atoi("12")
   n,err :=strconv.Atoi("hello")
   if err != nil{
	fmt.Println("转换错误",err)//转换错误 strconv.Atoi: parsing "hello": invalid syntax
   }else{
	fmt.Println("转成的结果是",n) 
   }

4)整数转字符串:str := strconv.ltoa(12345)

/整数转字符串:str = strconv.Itoa(12345)
   str = strconv.Itoa(12345)
   fmt.Printf("str=%v,str=%T",str,str)//str=12345,str=string

5)字符串转[]byte: var bytes =[]byte(“hello go”)

//字符串转[]byte: var bytes =[]byte("hello go")
   var bytes = []byte("hello go")
   fmt.Printf("bytes=%v",bytes)
   //输出结果为:
  bytes=[104 101 108 108 111 32 103 111]

6)[]byte转字符串:str=string([]byte{97,98,99})

//[]byte转字符串:str=string([]byte{97,98,99})
   str = string([]byte{97,98,99})
   fmt.Printf("str=%v",str)//str=abc

7)10进制转2,8,16进制:str=strconv.FormatInt(123,2) //2>8,16

//10进制转2,8,16进制:str=strconv.FormatInt(123,2) //2>8,16
   str=strconv.FormatInt(123,2)
   fmt.Printf("123对应的二进制是%v\n",str)//1111011
   
   str=strconv.FormatInt(123,16)
   fmt.Printf("123对应的十六进制是%v\n",str)//7b

8)查找子串是否在指定的字符串中:string.Contains(“sefood”,“food”) //true

//查找子串是否在指定的字符串中:string.Contains("sefood","food") //trues
  b := strings.Contains("seafood","foo")
  fmt.Printf("b=%v",b)//true

9)统计一个字符串有几个指定的子串: strings.Count(“ceheese”,“e”)//4

//统计一个字符串有几个指定的子串: 
 //strings.Count("ceheese","e")//4
 c :=strings.Count("ceheese","e")
 fmt.Printf("c=%v\n",c) //4

10)不区分大小写的字符串比较(==是区分字母大小写的):fmt.Println(strings.EqualFold(“abc”,“Abc”)) //true

//不想区分大小写的字符串比较(==是区分字母大小写的):
//fmt.Println(strings.EqualFold("abc","Abc")) //true
fmt.Println(strings.EqualFold("abc","Abc"))//true
fmt.Println("abc"=="Abc")//false ==是区分大小写的

11)返回子串在字符串第一次出现的index值,如果没有返回-1:strings.Index(“NLT_abc”,“abc”) //4

//返回子串在字符串第一次出现的index值
// ,如果没有返回-1:strings.Index("NLT_abc","abc") //4
e := strings.Index("NLT_abc","abc")
fmt.Printf("e=%v",e) //e=4  如果返回的是-1那说明就没有包含

12)返回子串在字符串最后一次出现的index,如没有就返回-1:strings.LastIndex("go ")

//返回子串在字符串最后一次出现的index,如没有就返回-1:strings.LastIndex("go ")
index :=strings.LastIndex("go golang","go")
fmt.Printf("index=%v\n",index)//3

13)将指定的子串替换成另外一个子串strings.Peplace(“go go hello”,“go”,“go语言”,n)n可以指定你希望替换几个,如果n=1表示全部替换

str4 :="go go hello"
str = strings.Replace(str4,"go","go语言",1)
fmt.Printf("str=%v\n",str) //str=go语言 go hello
//将n变成-1时,表示全部替换
str = strings.Replace(str4,"go","go语言",-1)
fmt.Printf("str=%v\n",str) //str=go语言 go语言 hello

14)按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组:strings.Split(“hello,world,ok”,“,”)

//按照指定的某个字符,为分割标识,
// 将一个字符串拆分成字符串数组:strArr :=strings.Split("hello,world,ok",",")
for i :=0;i<len(strArr);i++{
   fmt.Printf("str[%v]=%v\n",i,strArr[i])
}
fmt.Printf("strAr=%v\n",strArr)//strAr= [hello world ok]
}
//输出结果为:
str[0]=hello
str[1]=world
str[2]=ok
strAr=[hello world ok]

15)将字符串的字母进行大小写转换:strings.ToLower(“GO”) //go strings.ToUpper(“GO”) //GO

str = "goLang Hello"
str= strings.ToLower(str)
fmt.Printf("str=%v\n",str)//str=golang hello
//将字符串的字母全部转换成大写
str= strings.ToUpper(str)
fmt.Printf("str=%v\n",str)//str=GOLANG HELLO

16)将字符串左右两边的空格去掉:strings.TrimSpace(" tn a lone gropher ntrn ")

//将字符串左右两边的空格去掉:
// strings.TrimSpace(" tn a lone gropher ntrn  ")
str=strings.TrimSpace(" tn a lone gropher ntrn  ")
fmt.Printf("str=%v\n",str)//str=tn a lone gropher ntrn

17)将字符串左右两边指定的字符去掉:strings.Trim(“!hello!”,“!”)//[“hello”]//将左右两边!和""去掉

//将字符串左右两边指定的字符去掉
// :strings.Trim("!hello!","!")//["hello"]//将左右两边!和""去掉
str =strings.Trim("!hello!","!")
fmt.Printf("str=%v\n",str)//str=hello

18)将字符串左边指定的字符去掉;strings.TrimLeft(“! hello!”,“!”)//[“hello”]//将左边!和“”去掉

//将字符串左边指定的字符去掉;
// strings.TrimLeft("! hello!","!")//["hello"]//将左边!和“”去掉
str=strings.TrimLeft("! hello!","!")
fmt.Printf("str=%v\n",str)//str= hello!

19)将字符串右边指定的字符串去掉:strings.TrimRight(“! hello!”,“!”)//[“hello”]//将右边的!和""去掉

str=strings.TrimRight("! hello!","!")
fmt.Printf("str=%v\n",str)//str= !hello

20)判断字符串是否以指定的字符串开头:strings.HasPrefix(“ftp://192.168.10.1”,“ftp”)//true

//判断字符串是否以指定的字符串开头
// :strings.HasPrefix("ftp://192.168.10.1","ftp")//true
boo := strings.HasPrefix("ftp://192.168.10.1","ftp")
fmt.Printf("boo=%v\n",boo) //boo=true

21)判断字符串是否以指定的字符结束:strings.HasSuffix(“NLT_abc.jpg”,“abc”)//false

//判断字符串是否以指定的字符结束
// :strings.HasSuffix("NLT_abc.jpg","abc")//false
boo=strings.HasSuffix("NLT_abc.jpg","abc")
fmt.Printf("boo=%v\n",boo)//false
11.时间和日期相关的函数

说明:在编程中,程序员经常使用到日期相关的函数,比如:统计某段代码执行花费的时间

1)时间和日期相关的函数,需要导入time包

go学习之函数知识_第10张图片

2)time.Time类型,用于表示时间

//看看日期和时间相关的函数和方法使用
   //1.获取当前时间
   now := time.Now()
   fmt.Printf("now=%v now type=%T",now,now)
   //输出结果如下
//    now=2023-08-23 18:37:45.9279802 +0800 CST m=+0.008001001 now type=time.Time

3)获取当前时间的方法

now :=time.Now()//now的类型就是time.Time

4)如何获取其他的日期

//2.通过now可以获取到年,月,日,时分秒
fmt.Printf("年=%v\n",now.Year())
fmt.Printf("月=%v\n",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())

fmt.Printf("月=%v\n",int(now.Month()))//转成我们喜欢的数字

5)格式化日期时间

方式1:就是使用Printf 或者SPrintf

//格式化日期和时间
//way1
fmt.Printf("当前年月日 %02d-%02d-%02d %02d:%02d:%02d \n",
now.Year(),now.Month(),now.Day(),
now.Hour(),now.Minute(),now.Second())//当前年月日 2023-08-23 19:17:28

dateStr := fmt.Sprintf("当前年月日 %d-%d-%d-%d-%d-%d \n",now.Year(),
now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second())

fmt.Printf("dateStr=%v",dateStr)//dateStr=当前年月日 2023-8-23-19-21-36

方式2

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()
//输出结果如下所示
2023/08/23 19:26:21
2023-08-23
19:26:21

说明:“2006/01/02 15:04:05” 这个字符串的各个数字是固定的,必须这样写。

“2006/01/02 15:04:05”这个字符串各个数字可以自由组合,这样可以按照程序需求来返回时间和日期

6)时间的常量

const{
  Nanosecond Duration =1 //纳秒
  Microsecond =1000 * Nanosecond //微秒
  Millisecond =1000 * Microsecond //毫秒
  Second         =1000 * Millisecond //秒
  Minute         = 60 *Second//分钟
  Hour           = 60 *Minute//小时
}

常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到的100毫秒 100 * time.Millisecond

7)休眠

func Sleep(d Duration)

案例:

time.Sleep(100 * time.Millisecond)//休眠100毫秒
//每隔0.1秒就打出一个数字,打印到100时就退出
i := 0
for {
	i++
	fmt.Println(i)
	//休眠
	time.Sleep(time.Millisecond * 100)
	if i== 100{
		break
	}
}

8)获取当前Uix时间戳和unixnano时间戳(作用是可以获取随机数字)

unix时间戳

unixnano时间戳

go学习之函数知识_第11张图片

//unix和unixnano的使用
fmt.Printf("unix时间戳=%v unixnano时间戳=%v",now.Unix(),now.UnixNano())
//输出有以下结果:
//unix时间戳=1692792690 unixnano时间戳=1692792690199200800

实践案例:

1)编写一段代码来统计函数test3()执行的时间

package main
import(
	"fmt"
	"time"
	"strconv"
)
//编写一个函数我们来算出他执行的时间
func test03(){
	str := ""
	for i :=0;i <100000;i++{
		str += "hello" +strconv.Itoa(i)
	}
}

func main(){
	//在执行test03前,先获取unix时间戳
	start := time.Now().Unix()
	test03()
	end := time.Now().Unix()
    fmt.Printf("执行这个函数一共花了%v秒",end-start)
}
12.内置函数

说明:

Golang设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们称为go的内置函数文档:https://studygolang.com/pkgdoc ->builtin

1)len:用来求长度,比如string、array、map、channel

2)new:用来分配内存,主要用来配置类型,比如int,float32,struct…返回的是指针

func main(){
	num1 := 100
	fmt.Printf("num1的类型是%T,num1的值是%v,num1的地址是%v\n",num1,num1,&num1)
    //输出结果如下
	//num1的类型是int,num1的值是100,num1的地址是0xc0420120a0
    
	
	num2 := new(int)//*int
    //num2的类型%T ==> *int
	//num2的值 = 地址0xc0420120b8(这个地址是系统分配,不是固定的值)
	//num2的地址%v==地址0xc042004030(系统分配)
	*num2 = 100
	fmt.Printf("num2的类型是%T,num2存放的值是%v,num2指向的值是%v,num2的地址是%v",num2,num2,*num2,&num2)
    //输出结果如下所示:
	//num2的类型是*int,num2存放的值是0xc0420120b8,num2指向的值是0,num2的地址是0xc042004030

}

go学习之函数知识_第12张图片

3)make:用来分配内存,主要用来分配引用类型,比如chan、map、slice这个我们后面讲解

13.go的错误处理机制

先看示例代码,看看输出了什么

func test(){
	num1 :=10
	num2 :=0
	res :=num1/num2
	fmt.Println("res=",res)
}
func main(){
	//测试
	test()

	fmt.Println("main()下面的代码...")
}

对上面的代码进行总结

1)在默认情况下,当发生错误后(panic),程序就会退出(崩溃)

2)如果我们希望。当发生错误后,可以捕获到错误,并进行处理,保证代码可以继续执行。还可以在捕获到错误后给管理员一个提示(邮件或短信)

3)这里引出我们对错误进行处理

错误处理

1)Go语言追求简洁优雅,所以Go语言不支持传统的try…catch…finally这种处理。

2)Go中引入的处理方式是: defer,panic,recover

3)这几个异常的使用场景可以简单描述: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=",res)
}
func main(){
	//测试
	test()

	fmt.Println("main()下面的代码...")
    //执行结果如下:
err= runtime error: integer divide by zero
main()下面的代码...

错误处理的好处

进行错误处理后,程序不会轻易挂掉。如果加入预警代码,就可以让程序更加健壮,看一个案例演示:

func test(){
	//使用defer +recover来捕获处理异常
	defer func() {
		//err :=recover() //recover是内置函数,可以捕获到异常
		if err :=recover(); err != nil { //说明捕获到错误
			fmt.Println("err=",err)
			//这里就可以将错误发送给管理员...
			fmt.Println("发送邮件给@wew")
		}
	}()
	num1 :=10
	num2 :=0
	res :=num1/num2
	fmt.Println("res=",res)
}
func main(){
	//测试
	
for {
	test()
	time.Sleep(time.Second)
	}
	fmt.Println("main()下面的代码...")
}

自定义错误

Go程序中,也支持自定义错误,使用errros.New和panic内置函数

1)errors.New(“错误说明”),会返回一个erro类型的值,表示一个错误

2)panic内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。可以接收erro类型的变量,输出错误信息,并退出程序

案例演示:

//函数去读取以配置文件init.conf的信息
//如果文件名不正确,我们就返回一个自定义的错误

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()继续执行")
}

对上面的代码进行总结

1)在默认情况下,当发生错误后(panic),程序就会退出(崩溃)

2)如果我们希望。当发生错误后,可以捕获到错误,并进行处理,保证代码可以继续执行。还可以在捕获到错误后给管理员一个提示(邮件或短信)

3)这里引出我们对错误进行处理

错误处理

1)Go语言追求简洁优雅,所以Go语言不支持传统的try…catch…finally这种处理。

2)Go中引入的处理方式是: defer,panic,recover

3)这几个异常的使用场景可以简单描述: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=",res)
}
func main(){
	//测试
	test()

	fmt.Println("main()下面的代码...")
    //执行结果如下:
err= runtime error: integer divide by zero
main()下面的代码...

错误处理的好处

进行错误处理后,程序不会轻易挂掉。如果加入预警代码,就可以让程序更加健壮,看一个案例演示:

func test(){
	//使用defer +recover来捕获处理异常
	defer func() {
		//err :=recover() //recover是内置函数,可以捕获到异常
		if err :=recover(); err != nil { //说明捕获到错误
			fmt.Println("err=",err)
			//这里就可以将错误发送给管理员...
			fmt.Println("发送邮件给@wew")
		}
	}()
	num1 :=10
	num2 :=0
	res :=num1/num2
	fmt.Println("res=",res)
}
func main(){
	//测试
	
for {
	test()
	time.Sleep(time.Second)
	}
	fmt.Println("main()下面的代码...")
}

自定义错误

Go程序中,也支持自定义错误,使用errros.New和panic内置函数

1)errors.New(“错误说明”),会返回一个erro类型的值,表示一个错误

2)panic内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。可以接收erro类型的变量,输出错误信息,并退出程序

案例演示:

//函数去读取以配置文件init.conf的信息
//如果文件名不正确,我们就返回一个自定义的错误

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()继续执行")
}

预知后事,请看我下一篇博客啦!

你可能感兴趣的:(golang学习,学习日记,golang,学习)