美文网首页
Go基础-011 函数

Go基础-011 函数

作者: 如逆水行舟不进则退 | 来源:发表于2020-02-24 00:15 被阅读0次

1. 概念

函数,function,具有特定功能的代码块,称之为函数。通常函数可以接收外部数据称 之为函数参数,函数可以将运算结果返回,称之为函数的返回值。
定义函数的作用,为了将特定功能的代码块重用。体现程序设计上的封装性。
Go 应用程序有一个特殊的函数, main.main() 函数,入口函数,应用程序的初始执行点!

2. 声明语法

使用关键字 func 来定义。
语法如下:

func 函数标识符(形参列表) (返回值类型列表) { 
  函数体
}

其中,返回值类型列表,根据是否存在返回来确定是否使用。若仅仅存在一个返回值,可以 省略小括号。
也就是说,函数由:func 关键字,标识符(函数名)、参数、返回值类型、函数体构成。 其中:函数 func 关键字,形参列表、返回值类型以及函数体,构成了函数的字面量。
之所以称之为函数字面量,在 Go 语言中,函数也作为一种类型进行管理。而且是一种引用类型。

代码演示:

func F(p1 int, p2 string) string {
    return "" 
}

注意:函数在定义时,不能嵌套。func {func {}} 是错误的。

3. 调用函数

F() 的方式进行调用。
调用函数时,需要根据函数的需要传递参数给函数,同时根据需求得到函数的返回结果。
演示如下:

//调用函数
result := F(42, "hank") 
fmt.Println(result)

4. 函数标识符和匿名函数

满足标识符的要求。
函数标识符用于引用函数执行代码的地址,通过函数名,即可调用该函数。
函数名,不完全算作函数的一个部分,是一个查找机制。

在程序设计时,可以定义没有名字的函数,称之为匿名函数
匿名函数仅仅存在函数的字面量部分,需要将其立即调用或者存储到某个特定的变量中。 演示:

//匿名函数 

// 存储起来
f := func (p1 int, p2 string) string {
  return "return value" 
}
fmt.Printf("%T, %v\n", f, f)  // func(int, string) string,   f值是 0x49c5d0 
fmt.Println(f(42, "")) // return value

// 直接调用
result := func (p1 int, p2 string) string {
  return "return value" 
}(42, "hank")
fmt.Println(result) // return value


本例中, 输出的 func(int, string) string 称之为函数签名,也可以叫函数类型字面量。
函数签名,可以用来描述函数是否相同。
例如,本例中的具名函数 F() ,和 匿名函数变量 f,就是具有相同的函数签名,可以被视为同一个功能函数,指的是在 语法中可以通用。

5.参数

1) 概述

参数用于在调用函数时,向函数体内传递外部数据。

2)形参和实参

参数在描述时分为:形式参数(形参,paramter)和实际参数(实参,argument)。
形参:函数定义时使用的参数。在函数被调用时,被当做函数内的局部变量来处理。
实参:函数在被调用时,传递的参数。 在调用函数时,是使用实参为形参赋值的过程。

代码演示:

func F(p1 int, p2 string) string { 
  fmt.Println(p1, p2)
  return "return value"
}
result := F(42, "hank") // p1 = 42, p2 = "hank"

其中:
p1, p2 就是形参
42, “hank” 就是实参
调用函数时,使用 42 和 hank 为 p1 和 p2 赋值。

3)参数传递

调用函数时,使用 42 和 hank 为 p1 和 p2 赋值。
由于形参 p1, p2 类型不同,要求传递参数也是不同的。
典型的传参定义:

  • 常规类型,非引用类型,int,string,array。修改函数内的形参,不会影响函数外 的实参。
  • 引用类型 slice,map。修改函数内的形参,会导致函数外的实参改变。
  • 指针类型,*T。T类型的指针,通过*解析地址操作,修改形参数据,会影响到外
    部的实参。
func Func1(p1 int, p2 []int, p3 *int) { 
   p1 += 10
   p2[1] += 10
  *p3 += 20 
  fmt.Println(p1, p2, *p3) //  52 [1 12 3] 62
}
a1, a2, a3 := 42, []int{1, 2, 3}, 42 
Func1(a1, a2, &a3)
fmt.Println(a1, a2, a3)  // 42 [1 12 3] 62

注意结果,内部形参改变,切片和指针导致外部实参也随之改变,而常规类型 int,外部实参没有改变。

4)简洁类型语法

当多个参数的类型一致时候,可以在最后一个该类型参数后声明类型即可。
演示:

func Func2(p1, p2 int, p3 string) int {
  return 0 
}

与 p1 int, p2 int 一致。此时函数的签名还是:func(int, int, string) int 。没有省略第一个参 数参类型的描述。

5)剩余参数,不定数量参数

函数的最后一个形参,在定义的时候,可以在类型前使用剩余运算符定义,语法:
p ...T
注意:一定是最后一个参数。 剩余参数的作用,当调用函数时,可以传递不定数量该类型的参数。
此时形参接收的数据类型为 []T,T 类型的切片。 演示:

func Func3(op string, ps ...int) int { 
  fmt.Println(ps) 
  return 0 
}
Func3("plus")  // []
Func3("plus", 1)  // [1]
Func3("plus", 1, 2)  // [1 2]
Func3("plus", 1, 2, 3) // [1 2 3]

不定数量,0 个也可以。

当参数定义为不定数量时,传递实参时,也可以利用 ... 展开运算符,将同类型的数据展开传递,演示:

Func3("plus", []int{1, 2, 3, 4, 5}...)
func Func3(op string, ps ...int) int

此时相当于使用 []int{1, 2, 3, 4, 5} 为 ps 赋值的过程。(ps = []int{1, 2, 3, 4, 5} )

注意:
直接使用切片展开进行剩余参数赋值的操作,是两个变量间的直接赋值。
要求切片的类型和形参的类型完全一致。

思考:
fmt.Println() 的签名是:
func Println(a ...interface{}) (n int, err error)
参数 a,是空接口类型的切片。

问题是: 为什么不能 fmt.Println(slice...)的形式来调用。slice = []int{1, 2, 3, 4, 5}。

fmt.Println(1,2, 3, 4, 5) 
x := []int{1, 2 ,3, 4, 5} // 错误的语法
fmt.Println(x...) // 不同于 fmt.Println(1,2, 3, 4, 5)

原因是:
展开运运算 为 剩余参数赋值时,是直接变量间赋值的过程。Println 的形参 a,直接被赋值
x 赋值的过程。
a=x
该操作,就要去 a 和 x 的类型一致。当前不一致:
x = []int
a = []interface{}
[]interface{}空接口类型的切片不是 []int 整型切片类型。

6.返回值

1) 概述

函数的处理结果,为函数的返回值。
语法上,需要声明函数的返回值类型以及在函数体内使用 return 完成数据返回。

2)返回值类型声明

语法如下,有 4 种语法: 无返回值,单值返回,多值返回,命名返回。
代码演示:

// 无返回值 
func F4() {
}
// 单返回值
func F5() int {
  return 0 
}
// 亦可
//func F5() (int) {
// return 0
//}

// 多返回值
func F6() (int, string) {
  return 0, ""
}
// 命名返回
func F7() (result1 int) {
  return
}
func F8() (result1 int, result2 string) {
  return
}

函数的返回值声明,在函数参数列表后,使用括号进行包裹声明,声明的部分可以由返回值变量及类型构成。其中返回值变量可以省略,在函数内处理即可。同时若仅存在一个返回值 类型声明,可以省略括号部分。

注意: 函数签名(函数类型字面量)部分 包含返回值类型声明部分。(不包含返回值变量名部分)

3)return 语句

函数体内的 return 语句,用于完成返回值处理。 两种语法:

  • 返回特定数据表达式
  • 独立的return,通常同于命名返回值上
    演示:
func F6() (int, string) {
  return 0, "" 
}
func F8() (result1 int, result2 string) {
  return
}

若函数声明了返回值,则需要强制使用 return 语句。而且是可检测到一定执行的 return 语句。 例如,下面的语法错误:

func F6() (int, string) {
  //return 0, ""
} // missing return at end of function

可检测到的 return,指的是 return 必须可能执行到。在 条件判断语句体内的 return,也会被认为没有 return:

func F9() (int, string) { 
  cond := true
  if cond {
    return 0, "" 
  }
} // missing return at end of function

上面语法的处理应该在最后,再加一个 return:

func F9() (int, string) { 
  cond := true
  if cond {
    return 0, "" 
  }
    return 0, "" 
} 

要求 return 后,没有其他语句了。return 为最后一条语句。
因为 return 表示函数运行结束, 后续的语句没有任何意义。反过来说,即使执行了后续的语句,函数就没有正确的返回。演示:

func F9() (int, string) { 
  cond := true
  if cond {
    return 0, "" 
  }
  return 0, ""
  fmt.Println("after return")
} // missing return at end of function
4)具名返回(命名返回)

在声明返回值时,同时指定了返回变量。
该变量,相当于已经生命好的函数内的局部变量(类似于形参),在函数结束时,直接使用简单的 return 即可完成返回。

代码演示:

func F8() (result1 int, result2 string) {
  return
}

命名返回的优势在于可以在返回值声明的位置,通过变量名确定返回值意义。 例如下面的函数定义,返回为一个长度和一个标题

func F11() (int, string) {
  return 0, "" 
}

func F12() (length int, title string) {
  return
}
5)返回引用

返回值也可以是引用类型。

func F13() *int { 
   result := 42
   return &result
}
result := F13() 
fmt.Println(result, *result) // 0xc00000a0c0 42

注意:若返回值为当前函数内变量的引用形式,意味着在函数运行结束后,该被应用的变量 值空间,还会被继续使用。此时函数已经运行结束,函数内所控制的资源变量,已经被释放。
该情况的处理,Go 采用了【栈逃逸】的机制。

栈逃逸: 当需要在函数外使用函数内的变量资源时,为函数内的相应变量分配空间时,不会
使用常规的函数调用栈空间,而是在栈外开辟空间存储,该策略称之为栈逃逸。

7.调用栈

函数,在被调用时,会在内存的函数调用栈,为函数开辟空间,将函数的相关数据进行
存储,例如函数内的局部变量(包括形参,函数内声明的参数,命名返回值变量)。

栈:先进后出的一种结构。

如图所示:


注意:当函数运行完毕,对应函数调用栈,会被释放,内部的资源会全部回收!

8.递归调用

1)概述

调用函数的一种方案,指的是,在函数内部调用函数本身,称之为递归调用。
在函数体的某条语句中,完成了对自身的调用。
是迭代(循环)执行一种方案。
用于解决,一个复杂的问题,可以拆解成规模小且方案一致的简单问题。方案一致,意味着需要使用相同的方法进行解决,使用相同的函数进行解决。当问题的规模足够小时,解决方案显而易见。
因此,将大规模问题,逐步拆解成小规模问题,逐一解决小规模问题,将解决的结果向 上集合,进而将大问题解决。

分为:分,治,合,三个步骤。拆分,治理,合并。

例如:遍历某个目录下的全部(包含子目录)内容。
语法演示(伪代码):

func DirRead(path string) {
  for file := readFile(path) {
      print(file)
      if is_dir(file) {
        DirRead(file)
      }
  }

}

编写递归时,主要考虑:

  • 是否可以使用递归编程?是否可以大规模拆解为相同算法的小规模?
  • 递归调用的条件(递归点)。
  • 递归的出口在哪里?当问题拆分到什么程度,不再需要继续拆分了?
2)递归计算阶乘

(阶乘更好的方案,应该是循环结构,此处为了演示递归) 阶乘:

5! = 5 * 4 * 3 * 2 *1
6! = 6 * 5 * 4 * 3 * 2 *1 = 6 * 5!
N! = N * (N-1) * .... * 2 * 1

总结:
N! = N * (N-1)!

可见,当需要求解阶乘问题时,出现了大规模问题可以拆解成小规模问题的现象: N 的问题,可以拆解长 N-1 的问题。
可以使用递归编程来实现。

  • 递归的条件:
    直接计算 N-1 的阶乘,递归调用即可。

  • 递归的出口:
    当 N==1 时,阶乘为 1。1! == 1, 不需要继续递归了,有出口。

  • 编程实现:

func Factorial(n int) int {
    if 1 == n {  // 递归出口,不需要继续递归了
      return 1  // 该规模的问题,可以直接解决。 
   }
    return n * Factorial(n - 1) 
}

fmt.Println(Factorial(1))  // 1
fmt.Println(Factorial(2))  // 2 
fmt.Println(Factorial(3))  // 6
fmt.Println(Factorial(4))  // 24
fmt.Println(Factorial(5))  // 120
fmt.Println(Factorial(6))  // 720

分析该函数的执行过程。如下:

Factorial(5)
// return 5 * Factorial(5-1)
// return 5 * (return 4 * Factorial(4-1))
// return 5 * (return 4 * (return 3 * Factorial(3-1)))
// return 5 * (return 4 * (return 3 * (return 2 * Factorial(2-1)))) 
// return 5 * (return 4 * (return 3 * (return 2 * 1)))
// return 5 * (return 4 * (return 3 * 2))
// return 5 * (return 4 * 6)
// return 5 * 24
// 120

可见,最外层的 return 一直在等,内部的调用结束。在函数的调用栈中,会存在很多 Factorial()的栈,如图所示:

3)递归计算斐波那契数列第 N 项的值

(递归编程的演示, 本例还是建议使用循环结构)

斐波那契,前两项已知一般是 1,1,从第三项开始,为前两项的和。
F(3) = F(3-1) + F(3-2)
1, 1, 2, 3, 5, 8, 13, 21 ....

要求定义函数,计算第 N 项斐波那契数的值。

分析:
大规模可以拆解成思路一致的小规模。
递归点在于 N-1 和 N-2 上。
出口:前两项已知,n==1, n==2 不用递归实现。

编程实现:

func Fibonacci(n int) int { 
  //出口
  if 1 == n {
    return 1 
  }
  if 2 == n {
    return 1 
  }
  // 递归计算
  return Fibonacci(n-1) + Fibonacci(n-2)
} 
fmt.Println(Fibonacci(1)) 
fmt.Println(Fibonacci(2)) 
fmt.Println(Fibonacci(3)) 
fmt.Println(Fibonacci(4)) 
fmt.Println(Fibonacci(5)) 
fmt.Println(Fibonacci(6)) 
fmt.Println(Fibonacci(7)) 
fmt.Println(Fibonacci(8)) 
fmt.Println(Fibonacci(9)) 
fmt.Println(Fibonacci(10)) 
fmt.Println(Fibonacci(11))
4)处理树状结构

树状结构,是比较典型的嵌套结构。例如,文件夹的关系,分类的关系,以及 word 的
标题层次 都是典型的树状结构。

典型的树状结构,可以处理无限层次。

树状结构数据的存储,很难是直接的树状结构,通常都是在数据库里的并列的表格结构。 业务逻辑中,将表格数据,维护成树状的结构,典型的程序。例如:遍历文件夹中的全部文件,做产品的无限极分类。都是类似的结构。

演示分类的处理:
输入数据,并列的表格数据.
如下演示: 每个分类有三个字段,标题,ID,上级分类 ID。

type Category struct {
   ID int // ID
   Title string // 标题
   PID int // 上级分类 ID
}

categories := []Category {
  Category{1, "电脑", 0}, 
  Category{2, "服装", 0}, 
  Category{3, "手机", 0}, 
  Category{4, "图书", 0}, 
  Category{5, "手机通讯", 3}, 
  Category{6, "智能设备", 3}, 
  Category{7, "手机配件", 3},
  Category{8, "手机壳", 7},
  Category{9, "数据线", 7}, 
  Category{10, "支架", 7}, 
  Category{11, "type-c", 9}, 
  Category{12, "usb", 9}, 
  Category{13, "light", 9}, 
  Category{14, "教材", 4}, 
  Category{15, "文学", 4}, 
  Category{16, "科技", 4}, 
  Category{17, "中小学", 14}, 
  Category{18, "外语词典", 14}, 
  Category{19, "男装", 2},
}

基于该数据,完成树状结构或嵌套结构的获取。
利用递归编程实现。

  • 可以用递归的原因:
    上级分类检索下级分类的大规模问题,可以拆解为下级分类再检索 下下级分类的问题。大规模拆解成小规模。
  • 实现思路:
    利用 PID 检索某个分类下的子分类,当检索到某个子分类后,递归基于该子分类继续 检索后代分类。检索的过程就是遍历全部分类的过程。

演示: 增加一个数据类型,用于记录分类及其层级,层级用来标识当前分类的缩进深度。

func Tree(dataList []Category, id, l int) (tree []CategoryTree) {
  // 遍历全部的 category
  for _, c := range dataList {
  // 基于 PID 判断是否为当所检索 Id 的子分类 
  if c.PID == id {
      // 是 子分类 
      // 1 记录下来
      tree = append(tree, CategoryTree{ 
        Category: c,
        Level: l, 
      })
      // 2 递归检索,将所到当前分类的子分类,将其追加到结果之后。 
      tree = append(tree, Tree(dataList, c.ID, l+1)...)
  } }
  return
}

此时得到的返回值就是带有层级的分类树。

需要时可以根据层级做缩进展示:

categoryTree := Tree(categories, 0, 0) 
for _, c := range categoryTree {
  fmt.Print(strings.Repeat(" ", c.Level * 2))
  fmt.Println(c.Category.Title) 
}

输出样式

电脑 
服装
    男装 
手机
   手机通讯
   智能设备
   手机配件
      手机壳 
      数据线
 图书
    教材 
    中小学
    外语词典 
    文学
  ...... 省略
5)嵌套结构

对于该层级分类结构,另一种典型的数据格式为嵌套格式,在当前分类上记录其后代分类。
结构如下:

//得到的数据模拟
var nested = []CategoryNested{
     CategoryNested {
       Category: Category{3, "手机", 0},
       Children: []CategoryNested{
            {
              Category: Category{5, "手机通讯", 3}, 
              Children: nil,
            }, 
            {
              Category: Category{7, "手机配件", 3}, 
              Children: []CategoryNested{
                }, 
            },
        }, 
    },
}

该结构的生成,也是典型的递归程序,实现如下:

func Nested(dataList []Category, id int) (nested []CategoryNested){
    for _, c := range dataList { 
        if c.PID == id {
          // 继续检索其后代分类
          nested = append(nested, CategoryNested{ 
            Category: c,
            Children: Nested(dataList, c.ID),
            }) 
        }
    }
    return
}
  categoryNested := Nested(categories, 0) 
  fmt.Println(categoryNested)

得到的结果:

[{{1 电脑 0}[]}{{2 服装 0}[{{19 男装 2}[]}]}{{3 手机 0}[{{5 手机 通讯 3}[]}{{6 智能设备 3}[]}{{7 手机配件 3}[{{8 手机壳 7}[]}{{9 数 据线 7} [{{11 type-c 9} []} {{12 usb 9} []
} {{13 light 9} []}]} {{10 支架 7} []}]}]} {{4 图书 0} [{{14 教材 4} [{{17 中 小学 14} []} {{18 外语词典 14} []}]} {{15 文学 4} []} {{16 科技 4} []}]}]

展示时,通常配合多层循环完成展示,例如:

categoryNested := Nested(categories, 0) 
//fmt.Println(categoryNested)
for _, c := range categoryNested {
      fmt.Println(c.Category.Title)
      // 判断是否存在子分类
      if len(c.Children) > 0 {
          for _, cc := range c.Children {
              fmt.Println(cc.Category.Title)
              // 继续
              if len(cc.Children) > 0 {
                  for _, ccc := range cc.Children {
                        fmt.Println(ccc.Category.Title) 
                  }
              } 
          }
      } 
}

可见,下列展示:


或者也有类似目录那种多级的 缩进结构展示

. 9 延迟调用

常规情况下,函数调用后立即执行。可以利用 defer 关键字,将函数的执行延迟当所在
在函数的最后执行。
演示:

fmt.Println("before F14") 
defer F14() 
fmt.Println("after F14")
func F14() {
  fmt.Println("F14 is running.")
}
//输出:
//before F14 
//after F14
//F14 is running.

延迟调用,与函数本身无关,仅仅是调用机制问题。

主要用在关闭一些资源上,例如打开的数据库连接,文件句柄等。

一般来说,资源用完需要关闭。为了防止当操作资源后,忘记关闭资源,通常的做法都 是打开资源后立即使用 defer 将其关闭,不会立即关闭,可以保证函数执行完毕,一定会关闭。将打开与关闭写在一起,演示:

func F15 () { 
  // 操作文件
  // 打开文件
  handle, _ := os.Open("./if.go") 
  // 延迟关闭
  defer handle.Close()
  // 文件操作
  // 操作结束 
}

defer 延迟调用,接收的参数为调用时的参数值,而不是运行时的参数值,演示:

func F14(n int) {
fmt.Println(n, " ", "F14 is running.") }
fmt.Println("before F14") 
a := 42
defer F14(a)
a = 1024
fmt.Println(a, " ", "after F14")
// 输出
// before F14
// 1024 after F14
// 42 F14 is running.

注意: F14 运行时 n 的值为 42 而不是更新后的 1024. 因为在 defer 是会获取参数的拷贝,传递给函数。

多个 defer 的执行顺序,先注册的 defer 后执行,因为函数会维护一个 defer 栈,栈,先 进后出结构。(后进先出)。

defer F14()
defer F16()
// 输出
// f16 is running. 
// F14 is running.

defer 三点:

  • 延迟到函数结束时调用
  • 接收的参数为调用时的参数拷贝
  • 先defer后调用,后defer先调用

相关文章

  • Go基础-011 函数

    1. 概念 函数,function,具有特定功能的代码块,称之为函数。通常函数可以接收外部数据称 之为函数参数,函...

  • Golang基础(四)——函数

    Golang基础——函数 @([07] golang)[Go总结] [TOC] go程序中至少有一个函数——mai...

  • Golang基础(五)——函数二

    Golang基础(五)——函数二 @([07] golang)[Go总结] 匿名函数 匿名函数就是将一个函数直接赋...

  • 11.***函数***

    Go语言基础之函数 Golang 函数是组织好的、可重复使用的、用于执行指定任务的代码块。本文介绍了Go语言中函数...

  • golang函数

    函数是构建 Go 程序的基础部件 函数的定义 func function_name( [parameter lis...

  • Go基础

    Go基础 Go与c对比 Go中的注释与C的注释一样,有多行和单行,细节都一样 都是由函数组成的 入口函数都是mai...

  • Go基础编程---函数

    函数(无返回值) 函数(有返回值) 函数(有参有返回值) 函数类型 Go中函数也是一种类型,可以通过type 定义...

  • Go语言基础——函数

    Go函数的特性 不支持:嵌套、函数重载、默认值参数支持:可变变参、多返回值、命名返回值参数、匿名函数、闭包特别注意...

  • go 语言基础--函数

    1、函数 和C语言类型,Go语言也有函数的概念,语法如下: 2 参数和返回值 2.1 返回值 go 语言可以返回零...

  • Go语言学习笔记(二)-基础语法

    Go语言结构 Go语言的基础组成有一下几个部分: 包声明 包引入 函数 变量 语句&表达式 注释 关于Go的语言组...

网友评论

      本文标题:Go基础-011 函数

      本文链接:https://www.haomeiwen.com/subject/htrkqhtx.html