Go语言の函数、包和错误处理

本文章主要讲解了Go语言的函数,包和错误处理等知识。

为什么需要函数

完成需求

输入两个数,再输入一个运算符(+-*/),得到结果。

使用传统的方法解决

  • 代码
func main(){
	var n1 float64 = 1.2
	var n2 float64 = 2.3
	var operator byte = '-'
	switch operator {
	case '+':
		fmt.Printf("%v + %v = %v\n",n1, n2, n1 + n2)
	case '-':
		fmt.Printf("%v - %v = %v\n",n1, n2, n1 - n2)
	case '*':
		fmt.Printf("%v * %v = %v\n",n1, n2, n1 * n2)
	case '/':
		fmt.Printf("%v / %v = %v\n",n1, n2, n1 / n2)
	default:
		fmt.Printf("\n")
	}
}
➜  main go run ./main.go
1.2 - 2.3 = -1.0999999999999999
  • 分析上面的代码问题
    1. 可以完成功能需求,但代码冗余
    2. 与此同时不利于代码的维护
    3. 函数可以解决这个问题奥

函数的基本概念

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

在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
	default:
		fmt.Println("操作符号错误哦!")
	}
	return res
}
func main(){
	var n1 float64 = 1.2
	var n2 float64 = 2.3
	var operator byte = '+'
	result := cal(n1, n2, operator)
	fmt.Println(result)
}

包的引出

  1. 在实际的开发中,我们往往需要在不同的文件中,去调用其他文件的定义的函数,比如main.go中,去使用utils.go文件中的函数,如何实现?->包
  2. 现在有两个程序员共同开发一个Go项目,程序员xiaoming希望定义函数Cal,程序员xiaoqiang也想定义函数也叫Cal。两个程序员为此还吵了起来,怎么办?->包

包的原理图

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

包的基本概念

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

包的三大作用

  • 区分相同名字的函数、变量等标识符

  • 当程序文件很多时,可以很好的管理项目

  • 控制函数、变量等访问范围,即作用域

包的相关说明

  • 打包基本语法

    package 包名

  • 引入包的基本语法

    import "包的路径"

包使用的快速入门

包快速入门-Go相互调用函数,我们将func Cal定义到文件utils.go,将utils.go放到一个包中,当其它文件需要使用到utils.go的方法时,可以import该包,就可以使用了。【演示:新建项目的目录结构】

  • utils.go:
package utils
import (
	"fmt"
)
//将计算的功能,放到一个函数中,然后在需要使用,调用即可
//为了让其它包的文件使用Cal函数,需要将C大写类似其它语言的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
}
  • Main.go
package main
import (
	"fmt"
	"go_code/chapter06/demo02/utils"
)
func main(){
	//请大家完成这样一个需求:
	//输入两个数,再输入一个运算符(+,-,*,/),得到结果。。
	//分析思路...
	var n1 float64 = 1.2
	var n2 float64 = 2.3
	var operator byte = '+'
	result := utils.Cal(n1,n2,operator)
	fmt.Println("result=",result)
}

包使用的注意事项和细节讨论

  1. 在给一个文件打包时,该包对应一个文件夹,比如这里的utils文件夹对应的包名就是utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
  2. 当一个文件要使用其它包函数或变量时,需要先引入对应的包
  • 引入方式1:import "包名"
  • 引入方式2:
import (
  "包名"
  "包名"
)
  • package指令在文件第一行,然后是import指令。
  • 在import包时,路径从$GOPATH的src下开始,不用带src,编译器会自动从src开始引入。
  1. 为了让其它包的文件可以访问到本包的函数,则该函数名的首字母需要大写,类似其他语言的public,这样才能跨包访问。比如utils.go的Cal函数
  2. 在访问其它包函数,变量时,其语法是 包名.函数名,比如这个main.go文件中的utils.Cal(90, 80, "+")
  3. 如果包名较长,Go支持给包取别名,注意细节;取别名后,原来的包名就不能使用了
    • 说明:如果给包取了别名,则**需要使用别名来访问该包的函数和变量。
  4. 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义
  5. 如果你要编译成一个可执行程序文件,就需要将这个包声明为main,即package main这个就是一个语法规范,如果你是写一个库,包名可以自定义

函数的调用机制

通俗易懂的方式的理解

函数-调用过程

介绍:为了让大家更好地理解函数调用过程,看两个案例,并画出示意图,这个很重要

  1. 传入一个数+1

对上图说明

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

  2. 在每个函数对应的栈中,数据空间时独立的,不会混淆

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

  4. 计算两个数,并返回

package main

import (
	"fmt"
)
//一个函数 test
func test(n1 int) {
	n1 = n1 + 1
	fmt.Println("test() n1=",n1) //输入结果=?

}
func getSum(n1 int, n2 int) int {
	sum := n1 + n2
	fmt.Println("getSum sum = ", sum)// 30
	//当函数有return语句时,就是将结果返回给调用者
	//即谁调用我,就返回给谁
	return sum
}

func main() {
	n1 := 10
	//调用test
	test(n1)
	fmt.Println("main() n1=",n1)//输出结果=?

	sum := getSum(10,20)
	fmt.Println("main sum=",sum)// 30
}
 ~/go/src/go_code/chapter06/demo03/main  go run main.go 
test() n1= 11
main() n1= 10
getSum sum =  30
main sum= 30

return语句

  • 基本语法和说明
Go函数支持返回多个值,这一点是其它编程语言没有的。
func 函数名(形参列表) (返回值类型列表) {
  语句...
  return 返回值列表
}
  1. 如果返回多个值时,在接收时,希望忽略某个返回值,则使用_符号表示占位忽略
  2. 如果返回值只有一个,(返回值类型列表)可以不写()
  • 案例演示1

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

func main() {
  n1 := 10
  //调用test
  test(n1)
  fmt.Println("main() n1=",n1)//输出结果?
  sum := getSum(10,20)
  fmt.Println("main sum =",sum)//30
  //调用getSumAndSub
  res1,res2 := getSumAndSub(1, 2) //res1 = 3 res2 = -1 
  fmt.Printf("res1=%v res2=%v\n",res1, res2)
}

//编写函数,可以计算两个数的和和差,并返回结果
func getSumAndSub(n1 int, n2 int)(int , int){
  sum := n1 + n2
  sub := n1 - n2
  return sum, sub
}
  • 案例演示2

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

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

函数的递归调用

基本介绍

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

递归调用快速入门

  • 代码1
package main
import (
  "fmt"
)
func test(n int) {
  if n > 2 {
    n--
    test(n)
  }
  fmt.Println("n=", n)
}
func main(){
  test(4) //通过分析来看递归调用的特点
}

上面代码的分析图:

  • 代码2
func test2(n int) {
  if n > 2 {
    n--
    test2(n)
  } else {
    fmt.Println("n=",n)
  }
}
func main() {
  //看一段代码
  //test(4) //通过分析来看下递归调用的特点
  test2(4) // ?通过分析来看下递归调用的特点
}

对上面代码分析的示意图:

递归调用的总结

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

  1. 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
  2. 函数的局部变量是独立的,不会相互影响
  3. 递归必须向退出递归的条件逼近,否则就是无限递归,死循环了 :)
  4. 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁

递归课堂练习题

  • 题1:斐波那契数

    请使用递归的方式,求出斐波那契数1,1,2,3,5,8,13…

    给你一个整数n,求出它的斐波那切数是多少?

    • 思路
    1. 当n==1 || n==2, 返回1
    2. 当n>2, 返回f(n-1) + f(n-2)
    func fbn(n int) int {
      if(n==1 || n==2) {
        return 1
      } else {
        return fbn(n - 1) + fbn(n - 2)
      }
    }
    func main() {
      res := fbn(3)
      //测试
      fmt.Println("res=",res)
      fmt.Println("res=",fbn(4))
      fmt.Println("res=",fbn(5))
      fmt.Println("res=",fbn(6))
    }
    • 题2:求函数值

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

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

      • 思路

        递归就完事了奥铁子

      package main
      
      import (
      	"fmt"
      )
      /*
      已知 f(1)=3;f(n)=2*(n-1)+1;
      请使用递归的思想编程,求出f(n)的值?
      */
      func f(n int) int{
      	if n==1 {
      		return 3
      	} else{
      		return 2 * f(n-1) + 1
      	}
      }
      
      func main() {
      	fmt.Println("f(1)=",f(1))
      	fmt.Println("f(5)=",f(5))
      }
  • 题3:猴子吃桃子问题

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

    • 思路分析:
      1. 第10天只有1个桃子
      2. 第9天有(第十天桃子数量+1)*2个
      3. 第n天有peach(n)=(peach(n+1)+1)*2
    • 代码
    func peach(n int) int {
      if n==10 {
        return 1
      } else {
        return (peach(n+1)+1)*2
      }
    }

函数使用的注意事项和细节讨论

  1. 函数的形参列表可以是多个,返回值列表也可以是多个。
  2. 形参列表和返回值列表的数据类型可以是值类型和引用类型。
  3. 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 private
  4. 函数中的变量是局部的,函数外不生效【案例说明】
func test() {
  //n1 是 test函数的局部变量,只能在test函数中使用
  var n1 int = 10
}
func main() {
  //这里不能使用n1,因为n1是test函数的局部变量
  //fmt.Println("n1=",n1)
}
  1. 基本数据类型和数组默认都是值传递,即进行值拷贝。在函数内修改,不会影响到原来的值。
func test02(n1 int) {
  n1 = n1 + 10
  fmt.Println("test02() n1=", n1)
}
func main() {
  num := 20
  test02(num)
  fmt.Println("main() num=",num)
}
  1. 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用。
func test03(n1 *int) {
  *n1 = *n1 + 10
  fmt.Println("test03() n1=", *n1) //30
}
func main() {
  num := 20
  test03(&num)
  fmt.Println("main() num=", num) // 30
}

  1. Golang的函数不支持函数重载
func test02(n1 int) {
  n1 = n1 + 10
  fmt.Println("test02() n1=",n1)
}
func test02(n1 int, n2 int) {
  //这是错误的奥!!!!!
}
  1. 在Golang中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
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)
}
  1. 函数既然是一种数据类型,因此在Golang中,函数可以作为形参,并且调用哦
res2 := myFun(getSum, 50, 60)
fmt.Println("res2=", res2)

func myFun(funvar func(int, int) int, num1 int, num2 int) int {
  return funvar(num1, num2)
}
  1. 为了简化数据类型定义,Golang支持自定义数据类型

    基本语法:type 自定义数据类型名 数据类型 //理解:相当于一个别名

    案例:type myInt int //这时myInt就等价int来使用了哦!

    案例:type mySum func(int, int) int //这时mySum就等价一个 函数类型fu nc(int, int) int

  1. 支持对函数返回值命名
func getSumAndSub(n1 int, n2 int) (sum int, sub int){
	sub = n1 - n2
	sum = n1 + n2
	return
}
func main(){
	a1, b1 := getSumAndSub(1, 2)
	fmt.Printf("a1=%v,b1=%v \n",a1, b1)
}
  1. 使用_标示符,忽略返回值
func cal(n1 int, n2 int) (sum int, sub int) {
  sum = n1 + n2
  sub = n1 - n2
  return
}
func main(){
  res1,_ = cal(10, 20)
  fml.Ptintf("res1=%d",res1)
}
  1. Go支持可变参数
//支持0到多个参数
func sum(args... int) sum int {
}
//支持1到多个参数
func sum(n1 int, args... int) sum int{
}
  • 说明:
    1. args是slice切片,通过args[index]可以访问到各个值。
    2. 案例演示:编写一个函数sum,可以求出1到多个int的和
    3. 如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后。
//案例演示:编写一个函数sum,可以求出 1到多个int的和
//参数的使用
func sum(n1 int, args... int) int{
  sum := n1
  //遍历args
  for i:= 0; i < len(args); i++ {
    sum += args[i] //args[0] 表示取出args切片的第一个元素值,其它以此类推
  }
  return sum
}

func main() {
  res4 := sum(10, 0, -1, 90, 10, 100)
  fmt.Println("res4=",res4)
}

函数的课堂练习

  • 题1
func sum(n1, n2 float32) float32 {
  fmt.Printf("n1 type=%T \n",n1)
  //n1 type = float32
  return n1 + n2
}
func main(){
  fmt.Println("sum=",sum(1,2))// sum = 3
}
//代码有误错误?输出什么?

没有错误。

n1 type=float32 
sum= 3
  • 题2
type mySum func(int, int) int

func sum(n1 int, n2 int) int {
  return n1 + n2
}
func sum2(n1,n2,n3 int) int {
  return n1 + n2
}
//使用自定义数据类型(type)来简化定义 
func myFunc(funcVar mySum, num1 int, num2 int) int {
  return funcVar(num1, num2)
}
func main(){
  a := sum
  b := sum2
  fmt.Println(myFunc(a,1,2))//ok
  fmt.Println(myFunc(b,1,2))//error
  
}
/*
	错误:
	fmt.Println(myFunc(b,1,2))错误,原因是类型不匹配。
	因为不能把func sum2(n1,n2,n3 int) int 赋给func(int,int) int
*/
  • 题3:请编写一个函数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
  swap(&a, &b)
  fmt.Printf("a=%v,b=%v \n",a, b)
}
a=20,b=10 

init函数

基本介绍

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

案例说明:

package main
import (
  "fmt"
)
//init函数,通常可以在init函数中完成初始化工作 
func init(){
  fmt.Println("init()...")
}
func main(){
  fmt.Println("main()...")
}

输出结果是:

 ~/go/src/go_code/chapter06/demo04/main  go run ./main.go
init()...
main()...

init函数的注意事项和细节

  1. 如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程全局变量定义->init函数->main函数

  1. init函数最主要的作用,就是完成一些初始化的工作,比如下面的案例

  1. 细节说明:面试题:案例如果main.go和utils.go都含有变量定义,init函数时,执行的流程又是什么样呢?

匿名函数

介绍

​ Go支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。

匿名函数使用方式1

​ 在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。【案例演示】

func main(){
  //在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
  
  //案例演示,求两个数的和,使用匿名函数的方式完成
  res1 := func (n1 int, n2 int) int {
    return n1 + n2
  }(10,20)
  fmt.Println("res1=", res1)
}

匿名函数使用方式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)
res3 := a(90, 30)
fmt.Println("res3=", res3)

全局匿名函数

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

var (
  //func1就是一个全局匿名函数
  Fun1 = func (n1 int, n2 int) int {
    return n1 * n2
  }
)

//全局匿名函数的使用
res4 := Fun1(4,9)
fmt.Println("res4=", res4)

闭包

介绍

​ 基本介绍:闭包就是一个函数其相关的引用环境组合的一个整体(实体)

案例演示

//累加器
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是字段。函数和它使用到n构成闭包。

    4. 当我们反复的调用f函数时,n初始化一次,所以每调用一次就进行累计。

    5. 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到那些变量,因为函数和它引用到的变量共同构成闭包。

    6. 对上面代码的一个修改,加深对闭包的理解

    package main
    import (
      "fmt"
    )
    //累加器
    func AddUpper() func (int) int {
      var n int = 10
      var str = "hello"
      return func (x int) int {
        n = n + x
        str += string(36) // => 36 = '$'
        fmt.Println("str=", str) //1. str = "hello$" 2. str = "hello$$" 3. str = "hello$$$"
        return n
      }
    }
    func main() {
      //使用前面的代码
      f := AddUpper()
      fmt.Println(f(1))//11
      fmt.Println(f(2))//13
      fmt.Println(f(3))//16
    }

闭包的最佳实践

  • 请编写一个程序,具体要求如下:
  1. 编写一个函数 makeSuffix (suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
  2. 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg),则返回 文件名.jpg,如果已经有.jpg后缀,则返回原文件名。
  3. 要求使用闭包的方式完成
  4. Strings.HasSuffix, 该函数可以判断某个字符串是否有指定的后缀 。
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
    }else {
      return name
    }
  }
}

func main(){
  //返回一个闭包
  f := makeSuffix(".jpg")
  fmt.Println("文件名处理后=",f("winter"))
  fmt.Println("文件名处理后=",f("winter.jpg.jpg"))

}

上面代码的总结和说明:

  1. 返回的匿名函数和 makeSuffix (suffix string) 的 suffix 变量 组合成一个闭包,因为 返回的函数引用 到 suffix 这个变量
  2. 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每 次都传入 后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复 使用。可以仔细的体会一把!

函数的defer

为什么需要defer

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

快速入门案例

func sum(n1 int, n2 int) int {
  //当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈)
  //当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
  defer fmt.Println("ok1 n1=",n1)//defer 3. ok1 n1 = 10
  defer fmt.Println("ok2 n2=",n2)//defer 2. ok2 n2 = 20
  
  res := n1 + n2 // res = 30
  fmt.Println("ok3 res=",res)
  return res
}
func main(){
  res := sum(10,20)
  fmt.Println("res=",res)// 4. res = 30
}
 ~/go/src/go_code/chapter06/demo06/main  go run ./main.go
ok3 res= 30
ok2 n2= 20
ok1 n1= 10
res= 30

defer的注意事项和细节

  1. 当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中【暂时称此栈为defer栈】,然后继续执行函数下一个语句
  2. 当函数执行完毕后,再从defer栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制),所以可以看到前面案例输出的顺序。
  3. 在defer将语句放入到栈时,也会将相关的值拷贝同时入栈。请看一段代码:
func sum(n1 int, n2 int) int {
  //当执行到defer时,暂时不执行,会讲defer后面的语句压入到独立的栈(defer栈)
  //当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
  defer fmt.Println("ok1 n1=",n1) //defer 3. ok1 n1=10
  defer fmt.Println("ok2 n2=",n2) //defer 2. ok2 n2=20
  //增加一句话
  n1++ //n1 = 11
  n2++ //n2 = 21
  res := n1 + n2 // res = 32
  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最主要的价值是在,当函数执行完毕后可以及时的释放函数创建的资源。看看模拟代码:

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

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

说明

  1. 在Golang编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源),可以执行defer file.Close() defer connect.Close()
  2. 在defer后,可以继续使用创建资源。
  3. 当函数完毕后,系统会依次从defer栈中,取出语句,关闭资源
  4. 这种机制,非常简洁,程序员不用再为什么时候关闭资源而烦心。

函数参数传递方式

基本介绍

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

两种传递方式

  1. 值传递
  2. 引用传递

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

值类型和引用类型

  1. 值类型:基本数据类型int系列,float系列,bool,string,数组和结构体struct
  2. 引用类型:指针、slice切片、map、管道chan、interface等都是引用类型

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

  1. 值类型默认是值传递,变量直接存储值,内存通常在栈中分配【示意图】

  1. 引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引出这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收。【示意图】

  1. 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用。这个案例在前面详解函数使用注意事项中有。

变量作用域

  1. 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部
func test(){
  //age 和 Name 的作用域就只在test函数内部
  age := 10
  Name := "tom~"
}
func main(){
  
}
  1. 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用 域在整个程序有效
package main
import(
  "fmt"
)
//函数外部声明/定义的变量叫全局变量
//作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
var age int = 50
var Name string = "jack~"
//函数
func test() {
  //age和Name的作用域就只在test函数内部
  age := 10
  Name := "tom~"
  fmt.Println("age=", age) //10
  fmt.Println("Name=", Name) // tom~
}
func main() {
  fmt.Println("age=", age) // 50
  fmt.Println("Name=", Name) //jack~
  test()
}
  1. 如果变量是在一个代码块,比如for/if中,那么这个变量的作用域就在该代码块
//如果变量是在一个代码块,比如for/if中,那么这个变量的作用域就在该代码块
for i:= 0; i <= 10; i++ {
  fmt.Println("i=", i)
}
var i int //局部变量
for i = 0; i <= 10; i++ {
  fmt.Println("i=", i)
}
fmt.Println("i=", i)

变量作用域的课堂练习

  • 题1
var name = "tom~"
func test01() {
  fmt.Println(name) // tom~
}
func test02() {
  name := "jack~"
  fmt.Println(name) // jack
}
func main() {
  fmt.Println(name) // tom
  test01() //tom
  test02() // jack
  test01() // tom
}
  • 题2
var Age int = 20 // ok
Name := "tom" // var Name string		Name = "tom"
func main() {
  fmt.Println("name",Name)
}

错误,因为赋值语句 不能在函数体外面

函数课堂练习(综合)

  1. 函数可以没有返回值案例,编写一个函数,从终端输入一个整数打印出对应的金字塔
//将打印金字塔的代码封装到函数中
func printPyramid(totalLevel int) {
  //i表示层数
  for i := 1; i <= totalLevel; i++ {
    //在打印*前先打印空格
    for k := 1; k <= totalLevel - i; k++ {
      fmt.Print(" ")
    }
    
    //j表示每层打印多少
    for j := 1; j <= 2 * i - 1; j++ {
      fmt.Print("*")
    }
    fmt.Println()
  }
}

func main() {
  //调用printPyramid函数就可以打印金字塔
  //从终端输入一个整数打印出对应的金字塔
  var n int
  fmt.Println("请输入打印金字塔的层数")
  fmt.Scanln(&n)
  printPyramid(n)
}
请输入打印金字塔的层数
3
  *
 ***
*****
  1. 编写一个函数,从终端输入一个整数(1到9),打印出对应的乘法表
  • 思路:将九九乘法表以函数的方式封装,在需要打印时,直接调用即可
func printMulti(num int) {
  //打印出九九乘法表
  //i表示层数
  for i := 1; i <= num; i++ {
    for j:=1; j <= i; j++ {
      fmt.Printf("%v * %v = %v \t", j, i, j * i)
    }
    fmt.Println()
  }
}
func main() {
  //从终端输入一个整数表示要打印的乘法表对应的数
  var num int
  fmt.Println("请输入九九乘法表的对应数")
  fmt.Scanln(&num)
  printMulti(num)
}
请输入九九乘法表的对应数
7
1 * 1 = 1 
1 * 2 = 2       2 * 2 = 4 
1 * 3 = 3       2 * 3 = 6       3 * 3 = 9 
1 * 4 = 4       2 * 4 = 8       3 * 4 = 12      4 * 4 = 16 
1 * 5 = 5       2 * 5 = 10      3 * 5 = 15      4 * 5 = 20      5 * 5 = 25 
1 * 6 = 6       2 * 6 = 12      3 * 6 = 18      4 * 6 = 24      5 * 6 = 30      6 * 6 = 36 
1 * 7 = 7       2 * 7 = 14      3 * 7 = 21      4 * 7 = 28      5 * 7 = 35      6 * 7 = 42       7 * 7 = 49 
  1. 编写函数,对给定的一个二位数组(3 * 3)转置,这个题将数组的时候再完成8

字符串常用的系统函数

​ 说明:字符串在我们程序开发中,使用的是非常多的,常用的函数需要同学们掌握[带看手册或者 官方编程指南]

  1. 统计字符串的长度,按字节 len(str)
func main() {
  str := "hello叶"
  fmt.Println("str len=", len(str)) // 8
}
  1. 字符串遍历,同时处理有中文的问题 r := []rune(str)
str2 := "hello北京"
//字符串遍历,同时处理有中文的问题 r := []rune(str)
r := []rune(str)
for i := 0; i < len(r); i++ {
  fmt.Printf("字符=%c\n", r[i])
}
  1. 字符串转整数:n,err := strong.Atoi("12")
n, err := strconv.Atoi("hello")
if err != nil {
  fmt.Println("转换错误", err)
}else {
  fmt.Println("转成的结果是", n)
}
  1. 整数转字符串 str = strconv.Itoa(12345)
str = strconv.Itoa(12345)
fmt.Printf("str=%v, str=%T", str, str)
  1. 字符串 转 []byte: var bytes = []byte("hello go")
var bytes = []byte("hello go")
fmt.Printf("bytes = %v \n",bytes)
  1. []byte 转 字符串:str = string([]byte{97,98,99})
str = string([]byte{97,98,99})
fmt.Printf("str=%v\n", str)
  1. 10进制转2,8,16进制: str = strconv.FormatInt(123,2) // 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)
  1. 查找子串是否在指定的字符串中: strings.Contains("seafood", "foo") //true
b := strings.Contaions("seafood", "mary")
fmt.Printf("b=%v\n", b)
  1. 统计一个字符串有几个指定的子串: strings.Count("seheese", "e") //4
num := strings.Count("ceheese", "e")
fmt.Printf("num=%v\n", num)
  1. 不区分大小写的字符串比较(==是区分字母大小写的): fmt.Println(strings.EqualFold("abc", "Abc")) // true
b = strings.EqualFold("abc", "Abc")
fmt.Printf("b=%v\n", b) // true
fmt.Println("结果","abc" == "Abc") //false 区分字母大小写
  1. 返回子串在字符串第一次出现的index值,如果没有返回-1:strings.Index("NLT_abc", "abc") //4
index = strings.Index("NLT_abc", "abc")
fmt.Printf("index=%v\n", index) // true
  1. 返回子串在字符串最后一次出现的index,如没有返回-1:strings.LastIndex("go golang","go")
index = strings.LastIndex("go golang","go") //3
fmt.Printf("index=%v\n", index)
  1. 将指定的子串替换成 另外一个子串: strings.Replace("go go hello", "go", "go 语言", n) n 可以指定你希望替换几个,如果 n=-1 表示全部替换
str2 = "go go hello"
str = strings.Replace(str2, "go", "北京", -1)
fmt.Printf("str=%v str2=%v\n",str, str2)
  1. 按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组:

    strings.Split("hello,wrold,ok", ",")

strArr := string.Split("hello,world,ok",",")
for i:= 0; i < len(strArr); i++ {
  fmt.Printf("str[%v]=%v\n", strArr[i])
}
fmt.Printf("strArr=%v\n",strArr)
  1. 将字符串的字母进行大小写的转换: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) // GOLANG HELLO
  1. 将字符串左右两边的空格去掉: strings.TrimSpace("tn a lone gopher ntrn")
str = strings.TrimSpace("tn a lone gopher ntrn")
fmt.Printf("str=%q\n", str) 
  1. 将字符串左右两边指定的字符去掉 : strings.Trim("! hello! ", " !")//// ["hello"] //将左右两边 !和 ""去掉
str = strings.Trim("! hello! ", " !") 
fmt.Printf("str=%q\n", str)
  1. 将字符串左边指定的字符去掉: strings.TrimLeft("!hello!","!") //["hello"]将左边!和""去掉

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

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

b = strings.HasPrefix("ftp://192.168.10.1", "hsp") // false
fmt.Printf("b=%v\n", b)
  1. 判断字符串是否以指定的字符串结束:strings.HasSuffix("NLT_abc.jpg","abc") //false

时间和日期相关函数

基本的介绍

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

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

  1. time.Time类型,用于表示时间
package main
import (
  "fmt"
  "time"
)
func main(){
  //	看看日期和时间相关函数和方法使用
  // 1.获取当前时间
  now := time.Now()
  fmt.Printf("now=%v now type=%T\n", now, now)
  // 2.通过now可以获取到年月日,时分秒
  fmt.Printf("年=%v\n", now.Year())
  fmt.Printf("月=%v\n", now.Month())
  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())
}
  1. 格式化日期时间

    • 方式1:就是使用Printf或者Sprintf
    //格式化日期时间
    fmt.Printf("当前年月日 %d-%d-%d %d:%d:%d \n",now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
    fmt.Printf("dateStr=%v\n", dateStr)
    • 使用time.Format()方法完成:
    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()

    对上面代码的说明:

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

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

    1. 时间的常量
    const(
      Nanosecond Duration = 1 //纳秒
      Microsecond					= 1000 * Nanosecond //微秒
      Millisecond					= 1000 * Microsecond //毫秒
      Second							= 1000 * Millisecond //秒
      Minute							= 60 * Second //分钟
      Hour								= 60 * Minute //小时
    )

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

    ​ 100 * time.Millisecond

    1. 结合Sleep来使用一下时间常量
    //需求,每隔1秒打印一个数字,打印到100时就退出
    //需求2:每隔0.1秒打印一个数字,打印到100时就退出
    i := 0
    for {
      i++
      fmt.Println(i)
      //休眠
      //time.Sleep(time.Second)
      time.Sleep(time.Millisecond * 100)
      if i == 100 {
        break
      }
    }
    1. time的Unix和UnixNano的方法

//Unix和UnixNano的使用
fmt.Printf("unix时间戳=%v unixnano时间戳=%v\n", now.Unix(), now.UnixNano())

2018
unix时间戳=1527584269 unixnano时间戳=1527584269975756200

时间和日期的课堂练习

编写一段代码来统计 函数test03执行的时间

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("执行test03()耗费时间为%v秒\n", end-start)
}

内置函数

说明:

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

  1. len:用来求长度,比如string、array、slice、map、channel
  2. new:用来分配内存,主要用来分配值类型,比如int、float32,struct…返回的是指针

举例说明 new 的使用:

func main() {
  num1 := 100
  fmt.Printf("num1的类型%T , num1的值=%v , num1的地址%v\n", num1, num1, &num1)
  num2 := new(int) // *int
  //num2的类型%T => *int
  //num2的值 = 地址 0xc0420404c098 (系统分配)
  //num2的地址 %v = 地址 0xc04206a020 (系统分配)
  //num2指向的值 = 100
  *num2 = 100
  fmt.Printf("num2的类型%T , num2的值=%v , num2的地址%v\n nu2这个指针, 指向的值=%v", num2, num2, &num2, *num2)
}
num1的类型int , num1的值=100 , num1的地址0xc00001e090
num2的类型*int , num2的值=0xc00001e098 , num2的地址0xc00000e030 num2这个指针指向的值=100 

上面代码对应的内存分析图:

  1. make: 用来分配内存,主要用来分配引用类型,比如channel 、 map 、slice。这个之后讲解。

错误处理

示例

package main
import (
  "fmt"
)
func test() {
  num1 := 10
  num2 := 0
  res := num1 / num2
  fmt.Println("res=", res)
}
func main(){
  //测试
  test()
  fmt.Println("main()下面的代码...")
}
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.test()
        /Users/leafii/go/src/go_code/chapter06/demo10/main/main.go:8 +0x11
main.main()
        /Users/leafii/go/src/go_code/chapter06/demo10/main/main.go:13 +0x25
exit status 2
  • 对上面代码的总结
    1. 在默认情况下,当发生错误后(panic) ,程序就会退出(崩溃.)
    2. 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可 以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
    3. 这里引出我们要将的错误处理机制

基本说明

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

  2. Go中引入的处理方式为:defer,panic,**recover
    **

  3. 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中

    通过 recover 捕获这个异常,然后正常处理

使用defer+recover来处理错误

package main
import (
  "fmt"
  "time"
)
func test() {
  //使用defer + recover 来捕获和处理异常
  defer func() {
    err := recover() //recover() 内置函数,可以捕获到异常
    if err != nul { //说明捕获到错误
      fmt.Println("err=", err)
    }
  }()
  num1 := 10
  num2 := 0
  res := num1 / num2
  fmt.Println("res=", res)
}
func main() {
  //测试
  test()
  for {
    fmt.Println("main()下面的代码...")
    time.Sleep(time.Second)
  }
}
 ~/go/src/go_code/chapter06/demo10/main  go run ./main.go
err= runtime error: integer divide by zero
main()下面的代码...
main()下面的代码...
main()下面的代码...
^Csignal: interrupt

错误处理的好处

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

 ~/go/src/go_code/chapter06/demo11/main  go run ./main.go
err= runtime error: integer divide by zero
发送邮件给admin@outlook.com~~~~~~~~~
main()下面的代码...
main()下面的代码...
main()下面的代码...
^Csignal: interrupt

自定义错误

自定义错误的介绍

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

  1. errors.New(“错误说明”),会返回一个error类型的值,表示一个错误
  2. panic内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。可以接收error类型的变量,输出错误信息,并退出程序。

案例说明

package main
import (
  "fmt"
  "errors"
)
//函数去读取以配置文件init.conf的信息
//如果文件名传入不正确,我们就返回一个自定义的错误
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()下面的代码...")
}
panic: 读取文件错误..

goroutine 1 [running]:
main.test02()
        /Users/leafii/go/src/go_code/chapter06/demo12/main/main.go:21 +0x5a
main.main()
        /Users/leafii/go/src/go_code/chapter06/demo12/main/main.go:27 +0x25
exit status 2