golang-study/notes/6.指针、函数.md
2021-09-17 01:15:50 +08:00

34 KiB
Raw Permalink Blame History

本章目录:

0x00 Go语言基础之指针

  • 1.指针地址
  • 2.指针类型
  • 3.指针取值
  • 4.指针特性
  • 5.内存地址分配

0x01 Go语言基础之函数

  • 1.函数定义

  • 2.函数调用

  • 3.函数参数

    • 固定参数
    • 可变参数
  • 4.函数返回

    • 单返回值
    • 多返回值
    • 返回值命名
    • 返回值补充
  • 5.函数中变量作用域

    • 全局变量
    • 局部变量
  • 6.函数类型与变量

    • 定义函数类型
    • 函数类型变量
  • 7.高阶函数

    • 函数作为参数
    • 函数作为返回值
  • 8.函数补充

    • 递归函数
    • 匿名函数
    • 闭包
    • defer 语句
    • 函数总结示例

img

0x00 Go语言基础之指针

描述: Go 语言中的指针区别于C/C++中的指针Go语言中的指针不能进行偏移和运算安全指针

Go 语言中三个重要概念: 指针地址指针类型以及指针取值

简单回顾: 任何程序数据载入内存后,在内存都有他们的地址这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。

比如“永远不要高估自己”这句话是我的座右铭我想把它写入程序中程序一启动这句话是要加载到内存假设内存地址0x123456我在程序中把这段话赋值给变量A把内存地址赋值给变量B。这时候变量B就是一个指针变量, 通过变量A和变量B都能找到我的座右铭。

Go语言中的指针操作非常简单我们只需要记住两个符号&(取地址)*(根据地址取值)

1.指针地址

描述: 每个变量在运行时都拥有一个地址,该地址代表变量在内存中的位置。

Go语言中使用&字符放在变量前面对变量进行“取地址”操作。

取变量指针(地址)的语法如下:

ptr := &v    // v的类型为T
// # 参数
// v:代表被取地址的变量类型为T
// ptr:用于接收地址的变量ptr的类型就为*T称做T的指针类型。*代表指针。

2.指针类型

描述: Go语言中的值类型int、float、bool、string、array、struct都有对应的指针类型,如:*int、*int64、*string等。

简单示例:

func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a)             // a:10 ptr:0xc00001a078 (指针地址)
fmt.Printf("*b:%d ptr:%p type:%T\n",*b, b, b)  // b:10 ptr:0xc00001a078 type:*int (指针类型)
fmt.Printf("&b ptr:%p ",b)                     // &b ptr:0xc00000e018
}

为了更好的理解指针地址,我们来看一下b := &a的图示:

WeiyiGeek.指针地址

3.指针取值

描述: 在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。

func main() {
//指针取值
a := 10
b := &a // 取变量a的地址将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}

输出结果:

type of b:*int type of c:int value of c:10

4.指针特性

描述: 通过上面的指标变量、类型、取值的学习,我们了解到取地址操作符&取值操作符*是一对互补操作符,其中&取出地址,*根据地址取出地址指向的值。

Tips : 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 1.对变量进行取地址(&)操作,可以获得这个变量的指针变量(指针地址)。
  • 2.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

例如:我们可以在局部函数中修改全局变量的值采用指针传值:

func modify1(x int) {
x = 100
}

func modify2(x *int) {
*x = 100
}

func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}

5.内存地址分配

描述: 在Go语言中对于引用类型的变量我们在使用的时候不仅要声明它还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。 Tips Go语言中new和make是内建的两个函数他主要用来分配内存。

例如:执行下述例子中的代码会引发panic错误

func main() {
  // 声明
  var a *int
   // 定义
  *a = 100

  fmt.Println(*a)

  var b map[string]int
  b["沙河娜扎"] = 100
  fmt.Println(b)
}

New 函数 描述: new是Go语言的一置的函数它的函数签名如下

func new(Type) *Type

其中,

  • Type 表示类型new 函数只接受一个参数,这个参数是一个类型
  • *Type 表示类型指针new 函数返回一个指向该类型内存地址的指针。

Tips New 函数不太常用但由它可以得到一个类型的指针,并且该指针对应的值应该为该类型的零值。

func main() {
 // 只是声明了一个指针变量a但是没有初始化
  a := new(int)
  b := new(bool)
  fmt.Printf("%T\n", a) // *int
  fmt.Printf("%T\n", b) // *bool
  fmt.Println(*a)       // 0
  fmt.Println(*b)       // false
}

Tips : 指针作为引用类型需要初始化后才会拥有内存空间才可以给它赋值,所以需要按照下述方式使用内置的new函数对a进行初始化之后就可正常对其赋值了。

func main() {
  var a *int
  a = new(int)
  *a = 10
  fmt.Println(*a)
}

make 函数 描述: make也是用于内存分配的区别于new它只用于slice、map以及chan的内存创建而且它返回的类型就是这三个类型本身而不是他们的指针类型因为这三种类型就是引用类型所以就没有必要返回他们的指针了。

函数签名如下:

func make(t Type, size ...IntegerType) Type

Tips : Type 主要是 slice、map 以及channel类型并且必须使用make进行初始化后才能对它进行操作。

例如:

func main() {
 // 只是声明变量b是一个map类型的变量
  var b map[string]int
   //使用make函数进行初始化操作之后
  b = make(map[string]int, 10)
   //才能对其进行键值对赋值:
  b["WeiyiGeek"] = 100
  fmt.Println(b)
}

总结:new 函数与 make函数的区别

  • 二者都是用来做内存分配的。
  • make只用于slice、map以及channel的初始化返回的还是这三个引用类型本身
  • new用于类型的内存分配并且内存对应的值为类型零值返回的是指向类型的指针。

示例演示:

// 转入int类型的参数
func normal(x int) {
x = 65535
 fmt.Printf("Func Param &x ptr : %p \n", &x)
}

// 传入的参数为指针类型
func pointer(x *int) {
*x = 65535
fmt.Printf("Func Param x ptr : %p \n", x)
}

func demo1() {
// 1.2获得变量a的内存地址
a := 1024
b := &a
fmt.Printf("a : %d , a ptr: %p, b ptr : %v , *b = %d \n", a, &a, b, *b)
fmt.Printf("b type: %T, &b ptr : %p \n", b, &b)
fmt.Println()
// 2.针对变量a的内存地址进行重赋值此时会覆盖变量a的原值
*b = 2048
fmt.Printf("Change -> a : %d , a ptr: %p, b ptr : %v , *b = %d \n", a, &a, b, *b)
fmt.Printf("b type: %T, &b ptr : %p \n\n", b, &b)

// 3.指针传值
c := 4096
normal(c)
fmt.Println("After Normal Function c : ", c)
pointer(&c)
fmt.Printf("After Pointer Function c : %v, c ptr: %p \n\n", c, &c)

// 4.new 内存地址申请
var a4 *int
//*a4 = 100 // 此行会报 _panic 错误,因为未分配内存空间
fmt.Println("a4 ptr : ", a4) // 空指针 <nil>)还没有内存地址

d := new(int)                         // 申请一块内存空间 (内存地址)
fmt.Printf("%T %p, %v \n", d, d, *d) // 其指针类型默认值为 0 与其类型相关联。
*d = 8192                             // 对该内存地址赋值
fmt.Printf("%T %p, %v \n\n", d, d, *d)

// 5.make 内存地址申请
var b5 map[string]string
//b5["Name"] = "WeiyGeek" //此行会报 _panic 错误,因为未分配内存空间
fmt.Printf("%T , %p , %v\n", b5, &b5, *&b5)

b5 = make(map[string]string, 10) // 申请一块内存空间 (内存地址)
b5["Name"] = "WeiyGeek"          // 此时便可对该Map类型进行赋值了
b5["Address"] = "ChongQIng China"
fmt.Printf("%T , %p , %v\n\n", b5, &b5, b5)
}

执行结果:

a : 1024 , a ptr: 0xc00001a0d8, b ptr : 0xc00001a0d8 , *b = 1024
b type: *int, &b ptr : 0xc00000e028

Change -> a : 2048 , a ptr: 0xc00001a0d8, b ptr : 0xc00001a0d8 , *b = 2048
b type: *int, &b ptr : 0xc00000e028

Func Param &x ptr : 0xc00013a048
After Normal Function c :  4096
Func Param x ptr : 0xc00013a040
After Pointer Function c : 65535, c ptr: 0xc00013a040

a4 ptr :  <nil>
*int 0xc00001a130, 0
*int 0xc00001a130, 8192

map[string]string , 0xc00000e038 , map[]
map[string]string , 0xc00000e038 , map[Address:ChongQIng China Name:WeiyGeek]

0x01 Go语言基础之函数

描述: 其实在前面的示例中我们都已经接触到了函数例如Go语言的内置函数或者是您自己编写的函数在本章之中我们将详细讲解Go语言函数的使用。

Why , 为啥各个编程语言都要要引入函数?

答: 函数是组织好的、可重复使用的、用于执行指定任务的代码块。 通用得说即减少代码量、增强可读性、代码复用、提高开发效率、节约资源等特点。

1.函数定义

描述: Go语言中支持函数、匿名函数和闭包并且函数在Go语言中属于一等公民

Go语言中定义函数使用func关键字具体格式如下

func 函数名(参数)(返回值){ 函数体 }

其中:

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。注意在同一个包内函数名也称不能重名(包的概念详见后文)。
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
  • 函数体:实现指定功能的代码块。

示例1

// 方式1
func sayHello() {
  fmt.Println("Hello World, Let's Go")
}

2.函数调用

描述: 定义了函数之后,我们可以通过函数名()的方式调用函数。

例如.我们调用上面定义的函数代码如下:

func main() {
  fmt.Println("Start")
  sayHello()
  fmt.Println("End")
}

3.函数参数

描述: 通常我们需要为函数传递参数进行相应的处理以达到我们最终需要的产物。

函数参数类型

  • 固定参数
  • 可变参数

固定参数

常规参数类型 针对固定函数的参数我们需要制定其类型,例如

func intSum(x int, y int) { fmt.Println("x + y =",x+y) }

参数类型简写 函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:

func intSum(x , y int) { fmt.Println("x + y =",x+y) }

Tips : 上面的代码中intSum函数有两个参数这两个参数的类型均为int因此可以省略x的类型因为y后面有类型说明x参数也是该类型。

可变参数

描述: 可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加 ... 来标识(是否似曾相识,我们在数组那章节时使用过它,表示自动判断数组中元素个数进行初始化操作)。

示例1:

func intSum2(x ...int) int {
  fmt.Println(x) //x是一个切片
  sum := 0
  for _, v := range x {
    sum = sum + v
   }
  return sum
}

调用上面的函数:

ret1 := intSum2() ret2 := intSum2(10) ret3 := intSum2(10, 20) ret4 := intSum2(10, 20, 30) fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60

注意:可变参数通常要作为函数的最后一个参数。

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,

示例2

func intSum3(x int, y ...int) int {
  fmt.Println(x, y)
  sum := x
  for _, v := range y {
  	sum = sum + v
   }
  return sum
}

调用上述函数:

ret5 := intSum3(100) ret6 := intSum3(100, 10) ret7 := intSum3(100, 10, 20) ret8 := intSum3(100, 10, 20, 30) fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160

Tips : 本质上,函数的可变参数是通过切片来实现的。

4.函数返回

描述: 与其他编程语言一样Go语言中通过return关键字向外输出返回值。

单返回值

描述: Go语言中常规函数返回值。

举个例子:

func sum(x, y int)(res int) { return x + y }

多返回值

描述: Go语言中函数支持多返回值函数如果有多个返回值时必须用()将所有返回值包裹起来。

举个例子:

func calc(x, y int) (int, int) {
  sum := x + y
  sub := x - y
  return sum, sub
}

函数调用并接收返回值: sum,sub := calc(5, 3) // 8 , 2

返回值命名

描述: 函数定义时可以给返回值命名并在函数体中直接使用这些变量最后通过return关键字返回。

举个例子:

func calc(x, y int) (sum, sub int) {
  sum = x + y
  sub = x - y
  return
}

函数调用并接收返回值:

sum,sub := calc(5, 3) // 8 , 2

Tips :如果使用返回值命令时,只要其中一个返回值命名则另外一个返回值也必须命名。

返回值补充

描述: 当我们的一个函数返回值类型为slice时nil可以看做是一个有效的slice没必要显示返回一个长度为0的切片。

func someFunc(x string) []int {
  if x == "" {
  return nil // 没必要返回[]int{}
  }
  ...
}

5.函数中变量作用域

描述: 变量作用域分为全局变量作用域局部变量作用域以及代码块作用域

全局变量

描述: 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效, 在函数中可以访问到全局变量。

package main
import "fmt"

//定义全局变量num
var num int64 = 10
func testGlobalVar() {
 fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func main() {
 testGlobalVar()         // 10
 fmt.Printf("num=%d\n", num) // 10
}

局部变量

描述: 局部变量由分为两类一种是在函数内部定义的局部变量,另外一种则是在函数内部代码块中定义的局部变量

类1.在函数内定义的变量无法在该函数外使用。

例如: 下面的示例代码main函数中无法使用testLocalVar函数中定义的变量x

func testLocalVar() {
  //定义一个函数局部变量x,仅在该函数内生效
  var x int64 = 100
  fmt.Printf("x=%d\n", x)
}

func main() {
  testLocalVar()
  fmt.Println(x) // 此时无法使用变量x,并且此时会报错undefine x。
}

类2.在函数内的语句块定义的变量 描述: 通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。

// if 代码块
func testLocalVar2(x, y int) {
  fmt.Println(x, y) //函数的参数也是只在本函数中生效
  if x > 0 {
  z := 100 //变量z只在if语句块生效
  fmt.Println(z)
  }
  //fmt.Println(z) //此处无法使用变量z
}


// for 代码块
func testLocalVar3() {
  for i := 0; i < 10; i++ {
  fmt.Println(i) //变量i只在当前for语句块中生效
  }
  // fmt.Println(i) //此处无法使用变量i
}

Tips : 如果局部变量和全局变量重名,则优先访问局部变量。

Tips : 函数中查找变量的顺序步骤,(1) 现在函数内部查找,(2) 在函数上层或者外层查找, (3) 最后在全局中查找(此时如果找不到则会报错)

6.函数类型与变量

定义函数类型

描述: 我们可以使用type关键字来定义一个函数类型具体格式如下type calculation func(int, int) int

上面语句定义了一个calculation类型它是一种函数类型这种函数接收两个int类型的参数并且返回一个int类型的返回值。

简单来说凡是满足这个条件的函数都是calculation类型的函数例如下面的add和sub都是calculation类型的函数。

示例:

type calculation func(int, int) int
func add(x, y int) int {
  return x + y
}
func sub(x, y int) int {
  return x - y
}
// add和sub都能赋值给calculation类型的变量。
var c calculation
c = add
fmt.Println(c(1,2)) // 函数类型变量传递

函数类型变量

描述: 我们可以声明函数类型的变量并且为该变量赋值:

func main() {
  var c calculation               // 声明一个calculation类型的变量c
  c = add                         // 把add赋值给calculation类型的变量c
  fmt.Printf("type of c:%T\n", c) // type of c:main.calculation  (区别点)
  fmt.Println(c(1, 2))            // 像调用add一样调用c 1 + 2 = 3

  f := sub                        // 将函数sub赋值给变量f1
  fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int (区别点) 非函数类型的变量
  fmt.Println(f(30, 20))          // 像调用add一样调用f 30 - 20 = 10
}

7.高阶函数

描述: 高阶函数分为函数作为参数函数作为返回值两部分。

函数作为参数

函数可以作为参数示例:

func add(x, y int) int {
  return x + y
}
func calc(x, y int, op func(int, int) int) int {
  return op(x, y)
}
func main() {
  ret2 := calc(10, 20, add)
  fmt.Println(ret2) //30
}

函数作为返回值

函数也可以作为返回值示例(此种方式非常值得学习):

func do(s string) (func(int, int) int, error) {
  switch s {
  case "+":
  return add, nil
  case "-":
  return sub, nil
  default:
  err := errors.New("无法识别的操作符")
  return nil, err
  }
}

8.函数补充

递归函数

Q: 什么是递归(Recursion)函数?

答: 递归,就是在运行的过程中函数调用自身。 但是值得注意的是我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中(一定一定要有退出条件)。

语法格式:

func recursion() {
 recursion() /* 函数调用自身 */
}

func main() {
 recursion()
}

Tips : 递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。

示例1.n的阶乘计算

func factorial(n uint64) (ret uint64) {
  if n <= 1 {
  return 1
  }
  return n * factorial(n-1)
 }

func demo1() {
  fmt.Println("5 的阶乘 : ", factorial(5))
}

执行结果:

5 的阶乘 : 120

示例2.利用递归求斐波那契数列

// 方式1
func Fibonacci(count uint64) (ret uint64) {
  if count == 0 {
  return 0
  }
  if count == 1 || count == 2 {
  return 1
  }
  ret = Fibonacci(count-1) + Fibonacci(count-2)
  return
}

func demo3() {
  count := 10
  fmt.Printf("%v 个斐波那契数列:", count)
  for i := 1; i < count; i++ {
  fmt.Printf("%v ", Fibonacci(uint64(i)))
  }
}

// 方式2.值得学习
// fib returns a function that returns successive Fibonacci numbers.
func fib() func() int {
  a, b := 0, 1
  return func() int {
  a, b = b, a+b
  return a
  }
}
func main() {
  f := fib()
  fmt.Println(f(), f(), f(), f(), f())
}

执行结果:

10 个斐波那契数列:1 1 2 3 5 8 13 21 34

匿名函数

描述: 函数当然还可以作为返回值但是在Go语言中函数内部不能再像之前那样定义函数了只能定义匿名函数。

Q: 什么是匿名函数?

答: 匿名函数就是没有函数名的函数,在很多编程语言中都有这样的特性。 匿名函数多用于实现回调函数和闭包。

Tips : 匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

func main() {
  // 方式1.将匿名函数保存到变量
  add := func(x, y int) {
   fmt.Println(x + y)
  }
  add(10, 20) // 通过变量调用匿名函数

  //方式2.自执行函数:匿名函数定义完加()直接执行
  func(x, y int) {
    fmt.Println(x + y)
  }(10, 20)
}

闭包

描述: 闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+外遍变量的引用, 例如在第三方包里只能传递一个不带参数的函数,此时我们可以通过闭包的方式创建一个带参数处理的流程,并返回一个不带参数的函数。

Tips : 非常注意引用的外部外部变量在其生命周期内都是存在的(即下次调用还能使用该变量值)。

闭包基础示例1

func adder() func(int) int {
  var x int  // 在f的生命周期内变量x也一直有效
  return func(y int) int {
    x += y
    return x
  }
}
func main() {
  var f = adder()
  fmt.Println(f(10)) //x=0,y=10 ->  x = 10
  fmt.Println(f(20)) //x=10,y=20 -> x = 30
  fmt.Println(f(30)) //x=30,y=30 -> x = 60

  f1 := adder()
  fmt.Println(f1(40)) //40
  fmt.Println(f1(50)) //90
}

闭包基础示例2:

package main

import (
"fmt"
"math"
)

// 1.假设这是个第三方包
func f1(f func()) {
	fmt.Printf("# This is f1 func , Param is f func() : %T \n", f)
	f() // 调用传入的函数
}

// 2.自己实现的函数
func f2(x, y int) {
	fmt.Printf("# This is f2 func , Param is x,y: %v %v\n", x, y)
	fmt.Printf("x ^ y = %v \n", math.Pow(float64(x), float64(y)))
}

// 要求 f1(f2) 可以执行此时由于f1 中的传递的函数参数并无参数,所以默认调用执行一定会报错。
// 此时我们需要一个中间商利用闭包和匿名函数来实现,返回一个不带参数的函数。

func f3(f func(int, int), x, y int) func() {
	tmp := func() {
		f(x, y) // 此处实际为了执行f2函数
	}
	return tmp // 返回一个不带参数的函数为返回给f1函数
}

func main() {
	ret := f3(f2, 2, 10) // 此时函数并为执行只是将匿名函数进行返回。先执行 f3(fun,x,y int)
	f1(ret)              // 当传入f1中时ret()函数便会进行执行。再执行 f1() ,最后执行 f2(x,y int)
}

执行结果: # This is f1 func , Param is f func() : func() # This is f2 func , Param is x,y: 2 10 x ^ y = 1024

Tips : 变量f是一个函数并且它引用了其外部作用域中的x变量此时f就是一个闭包。并且在f的生命周期内变量x也一直有效。

闭包进阶示例1相比较于上面这种方式该种是将x变量放入函数参数之中在进行函数调用时赋值。

func adder2(x int) func(int) int {
  return func(y int) int {
  x += y
  return x
  }
}
func main() {
  var f = adder2(10) // `在f的生命周期内变量x也一直有效。`
  fmt.Println(f(10)) //20
  fmt.Println(f(20)) //40
  fmt.Println(f(30)) //70

  f1 := adder2(20)
  fmt.Println(f1(40)) //60
  fmt.Println(f1(50)) //110
}

闭包进阶示例2判断文件名称是否以指定的后缀结尾是则返回原文件名称,否则返回文件名称+指定后缀的文件

func makeSuffixFunc(suffix string) func(string) string {
  return func(name string) string {
     // 判断name变量中的字符串是否已suffix结尾
  if !strings.HasSuffix(name, suffix) {
  return name + suffix
  }
  return name
  }
}

func main() {
  jpgFunc := makeSuffixFunc(".jpg")
  txtFunc := makeSuffixFunc(".txt")
  fmt.Println(jpgFunc("test")) //test.jpg
  fmt.Println(txtFunc("test")) //test.txt
}

闭包进阶示例3该示例中函数同时返回add,sub两个函数.

func calc(base int) (func(int) int, func(int) int) {
  add := func(i int) int {
  base += i
  return base
  }

  sub := func(i int) int {
  base -= i
  return base
  }
  return add, sub
}

func main() {
  f1, f2 := calc(10)
  fmt.Println(f1(1), f2(2)) //11 9
  fmt.Println(f1(3), f2(4)) //12 8
  fmt.Println(f1(5), f2(6)) //13 7
}

Important : 闭包其实并不复杂,只要牢记闭包=函数+引用环境。

defer 语句

描述: Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时将延迟处理的语句按defer定义的逆序进行执行(压栈-后进先出)也就是说先被defer的语句最后被执行最后被defer的语句最先被执行。

(1) defer 执行时机 描述: 在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值RET指令两步。而defer语句执行的时机就在返回值赋值操作后RET指令执行前

具体如下图所示:

WeiyiGeek.defer执行时机

(2) 简单例子

示例1: defer 延迟特性演示

func main() {
  fmt.Println("start")
  defer fmt.Println(1)
  defer fmt.Println(2)
  defer fmt.Println(3)
  fmt.Println("end")
}
// 输出结果:
start
end
3
2
1

示例2.探究程序执行开始时间以及最后函数返回前的时间

func funcTime() int {
  fmt.Println("函数开始时间: ", time.Now().Local())
  var x = 0
  defer fmt.Println("init x = ", x) // 注意点: 此处已经将x=0值赋值了只是没有被输出。// 最终输出
  for i := 0; i <= 100; i++ {
  x += i
  }
  defer fmt.Println("函数返回前时间: ", time.Now().Local()) // 再输出
  defer fmt.Println("ret x = ", x)                   // 后进先出 -> 先输出
  return x
}
// 输出结果:
函数开始时间:  2021-08-15 18:28:58.37787611 +0800 CST
ret x =  5050
函数返回前时间:  2021-08-15 18:28:58.377991344 +0800 CST
init x =  0

Tips : 由于defer语句延迟调用的特性所以defer语句能非常方便的处理资源释放问题。比如资源清理、文件关闭、解锁及记录时间等。

(3) 经典面试案例 示例1:

package main
import "fmt"
// 函数返回值无命名
func f1() int {
	x := 5 // 局部变量
	defer func() {
		x++
	}()
	return x // 1.返回值 x = 5, 2.defer 语句执行后修改的是 x = 63.RET指令最后返回的值是 5 (由于无返回值命令则就是return已赋予的值5)
}

// 函数返回值命名 y 进行返回
func f2() (x int) {
	defer func() {
		x++
	}()
	return 5 // 1.返回值 x = 5, 2.defer 语句执行后修改的是 x = 63.RET指令最后返回的x值是 6 (由于存在返回值命名x则就是return x 值6)
}

// 函数返回值命名 y 进行返回
func f3() (y int) {
	x := 5 // 局部变量
	defer func() {
		x++ // 修改 x 变量的值 x + 1
	}()
	return x // 1.返回值 x = y = 5, 2.defer 语句执行后修改的是 x 3.RET指令最后返回的y值还是 5
}

// 匿名函数无返回值
func f4() (x int) {
	defer func(x int) {
		x++ // 改变得是函数中局部变量x非外部x变量。
	}(x)
	return 5 // 1.返回值 x = 5, 2.defer 语句执行后 x 副本 = 6 , 3.RET指令最后返回的值还是 5
}

// 匿名函数中返回值
func f5() (x int) {
	defer func(x int) int {
		x++ // 改变得是函数中局部变量x非外部x变量。
		return x
	}(x)
	return 5 // 1.返回值 x = 5, 2.defer 语句执行后 x 副本 = 6 , 3.RET指令最后返回的值还是 5
}

// 传入一个指针到匿名函数中(方式1)
func f6() (x int) {
	defer func(x *int) {
		*x++
	}(&x)
	return 5 // 1.返回值 x = 5, 2.由于defer语句传入x指针地址到匿名函数中 x = 6, 3.RET指令最后返回的值 6
}

// 传入一个指针到匿名函数中(方式2)
func f7() (x int) {
	defer func(x *int) int {
		(*x)++
		return *x
	}(&x)
	return 5 // 1.返回值x = 5, 2.由于defer语句传入x指针地址到匿名函数中 x = 6, 3.RET指令最后返回值 6
}

func main() {
	fmt.Println("f1() = ", f1())
	fmt.Println("f2() = ", f2())
	fmt.Println("f3() = ", f3())
	fmt.Println("f4() = ", f4())
	fmt.Println("f5() = ", f5())
	fmt.Println("f6() = ", f6())
	fmt.Println("f7() = ", f7())
}
// 执行结果:
f1() =  5
f2() =  6
f3() =  5
f4() =  5
f5() =  5
f6() =  6
f7() =  6

示例2.问下面代码的输出结果是提示defer注册要延迟执行的函数时该函数所有的参数都需要确定其值

func calc(index string, a, b int) int {
 ret := a + b
 fmt.Println(index, a, b, ret)
 return ret
}

func main() {
 x := 1
 y := 2
 defer calc("AA", x, calc("A", x, y))
 // calc("A", x, y) =>calc("A", 1, 2) = 3  {"A" , 1, 2, 3}
 // defer calc("AA", 1, 3) = 4 {"AA", 1, 3, 4}
 x = 10
 defer calc("BB", x, calc("B", x, y))
 // calc("B", x, y) = calc("B", 10, 2) = 12  {"B" , 10, 2, 12}
 // defer calc("BB", 10, 12) = 22 {"BB",10,12,22}
 y = 20
}
// 执行结果:
{"A" , 1, 2, 3}
{"B" , 10, 2, 12}
{"BB", 10, 12, 22}
{"AA", 1, 3, 4}

Tips : 当遇到defer语句时其中的函数中调用的变量值是外部变量时是离该defer语句最近的外部变量其赋予的值(存在于一个变量多次赋值的场景)。

函数总结示例

package main
import (
	"errors"
	"fmt"
	"strings"
	"time"
)
// 函数:将一段代码封装到代码块之中
// 1.无参函数
func f1() {
	fmt.Println("Hello World, Let's Go")
}

// 2.有参函数
func f2(name string) {
	fmt.Println("Hello", name)
}

// 3.函数返回值
func f3(i int, j int) int {
	sum := i + j
	return sum
}

// 4.函数多命名返回值与参数类型简写
func f4(x, y int) (sum, sub int) {
	sum = x + y
	sub = x - y
	return
}

// 5.可变参数
func f5(title string, value ...int) string {
	return fmt.Sprintf("Title : %v , Value : %v \n", title, value)
}

// 6.变量作用域之全局变量

const PATH = "/home/weiyigeek"

var author = "WeiyiGeek"

func f6() {
	fmt.Println("author:", author, ",Home PATH:", PATH)
}

// 7.变量作用域之局部变量
func f7(x, y int) {
	localAuthor := "WeiyiGeek" // 局部变量外部无法引用
	fmt.Println("localAuthor = ", localAuthor, ",x = ", x, ",y = ", y)
	// 语句块定义的变量
	if x > 0 {
		z := 1024
		fmt.Println(z)
	}
	for i := 0; i < 10; i++ {
		fmt.Print(i, " ")
	}
	// fmt.Println(z,i)  //此处无法使用变量z 和 i
	fmt.Println()
}

// 8.函数类型与变量
type calc func(int, int) int

func sum(x, y int) int {
	return x + y
}
func sub(x, y int) int {
	return x - y
}
func f8() {
	// 方式1
	var c calc
	c = sum
	fmt.Printf("type of c:%T , c(1,2)  %v \n", c, c(1, 2)) // type of c:main.calculation  (区别点)

	// 方式2
	d := sub
	fmt.Printf("type of d:%T , d(1,2)  %v \n", d, d(1, 2)) // type of d:func(int, int) int (区别点)

}

// 9.函数作为参数值或者作为返回值
func mul(x, y int) int {
	return x * y
}
func div(x, y int) int {
	return x / y
}

// 函数作为参数值
func calculation(x, y int, op func(int, int) int) int {
	return op(x, y)
}

// 函数作为返回值
func ops(s string) (func(int, int) int, error) {
	switch s {
	case "+":
		return sum, nil
	case "-":
		return sub, nil
	case "*":
		return mul, nil
	case "/":
		return div, nil
	default:
		err := errors.New("无法识别的操作符")
		return nil, err
	}
}

func f9() {
	// 演示1
	fmt.Printf("Type : %T , calculation (10 , 20, mul) = %v \n", calculation(10, 20, mul), calculation(10, 20, mul))

	// 演示2
	value, _ := ops("/")
	fmt.Printf("Type : %T , ops('/') ->  div(100,10) = %v \n\n", value(100, 10), value(100, 10))
}

// 10.匿名函数
func f10() {
	// 方式1
	muls := func(x, y int) int {
		fmt.Println("匿名函数1 之 x , y =", x, y)
		return x * y
	}
	ret := muls(3, 2)
	fmt.Println("匿名函数1 返回结果: ", ret)

	// 方式2
	func(x, y int) {
		fmt.Println("匿名函数2 之 x , y =", x*y)
	}(3, 2)

}

// 11.闭包
func adder1() func(int) int {
	var x int
	return func(y int) int {
		x += y
		return x
	}
}

func adder2(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}

func makeSuffixFunc(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}

func f11() {
	// 方式1
	var f = adder1()
	fmt.Printf("\n闭包 adder1: %v\n", f(10)) //x=0,y=10  -> x = 10
	fmt.Println("闭包 adder1:", f(20))       //x=10,y=20 -> x = 30
	fmt.Println("闭包 adder1:", f(30))       //x=30,y=30 -> x = 60

	// 方式2
	g := adder2(10)
	fmt.Printf("闭包 adder2: %v\n", g(10)) //x=10,y=10 -> x = 20
	fmt.Println("闭包 adder2:", g(20))     //x=20,y=20 -> x = 40
	fmt.Println("闭包 adder2:", g(30))     //x=40,y=30 -> x = 70

	// 示例3
	testJPG := makeSuffixFunc("jpg")
	fmt.Printf("闭包 makeSuffixFunc : file test = %v , file test.jpg = %v \n\n", testJPG("test"), testJPG("test.jpg"))
}

// 12.defer 语句使用演示
func funcTime() int {
	fmt.Println("函数开始时间: ", time.Now().Local())
	var x = 0
	defer fmt.Println("init x = ", x) // 注意点: 此处已经将x=0值赋值了只是没有被输出。 // 最终输出
	for i := 0; i <= 100; i++ {
		x += i
	}
	defer fmt.Println("函数返回前时间: ", time.Now().Local()) // 再输出
	defer fmt.Println("ret x = ", x)                   // 后进先出 -> 先输出
	return x
}

func f12() {
	ret := funcTime()
	fmt.Println("defer 示例1 1+2+3+....+99+100 =", ret)
}

func main() {
	f1()

	f2("WeiyiGeek")

	fmt.Println(f3(1, 1))

	x, y := f4(1, 3)
	fmt.Printf("x = %d ,y = %d \n", x, y)

	fmt.Println(f5("我是一串数字:", 1, 2, 3, 4))

	f6()

	f7(1, 2)

	f8()

	f9()

	f10()

	f11()

	f12()
}

执行结果:

Hello World, Let''s Go

Hello WeiyiGeek

2

x = 4 ,y = -2

Title : 我是一串数字: , Value : [1 2 3 4]


author: WeiyiGeek ,Home PATH: /home/weiyigeek

localAuthor =  WeiyiGeek ,x =  1 ,y =  2

1024

0 1 2 3 4 5 6 7 8 9

type of c:main.calc , c(1,2)  3

type of d:func(int, int) int , d(1,2)  -1

Type : int , calculation (10 , 20, mul) = 200

Type : int , ops('/') ->  div(100,10) = 10


匿名函数1  x , y = 3 2

匿名函数1 返回结果:  6

匿名函数2  x , y = 6


闭包 adder1: 10

闭包 adder1: 30

闭包 adder1: 60

闭包 adder2: 20

闭包 adder2: 40

闭包 adder2: 70

闭包 makeSuffixFunc : file test = testjpg , file test.jpg = test.jpg


函数开始时间:  2021-08-15 19:35:19.159014152 +0800 CST

ret x =  5050

函数返回前时间:  2021-08-15 19:35:19.159208306 +0800 CST

init x =  0

defer 示例1 1+2+3+....+99+100 = 5050