技术学习总结

150 阅读4分钟

七、函数

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

3.7.1 函数分类

在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。

举例代码如下:

具名函数:就和c语言中的普通函数意义相同,具有函数名、返回值以及函数参数的函数。

func Add(a, b int) int {
    return a+b
}

匿名函数:指不需要定义函数名的一种函数实现方式,它由一个不带函数名的函数声明和函数体组成。

匿名函数:指不需要定义函数名的一种函数实现方式,它由一个不带函数名的函数声明和函数体组成。

var Add = func(a, b int) int {
    return a+b
}

解释几个名词如下:

解释几个名词如下:

闭包函数:返回为函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域。 一级对象:支持闭包的多数语言都将函数作为第一级对象,就是说函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。 包:go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的。

3.7.2 函数声明和定义

Go 语言函数定义格式如下:

func fuction_name([parameter list])[return types]{
    函数体
}
解析
func函数由func开始声明
function_name函数名称
parameter list参数列表
return_types返回类型
函数体函数定义的代码集合

3.7.3 函数传参

Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。

当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果,我们解释一下解包的含义,代码如下:

func main(){
    var a = []int{1, 2, 3}
    Print(a...)   // 解包
    Print(a)      // 未解包
}
​
func Print(a ...int{}) {
    fmt.Println(a...)
}

以上当传入参数为a... 时即是对切片a进行了解包,此时其实相当于直接调用Print(1,2,3) 。当传入参数直接为 a时等价于直接调用Print([]int{}{1,2,3})

3.7.4 函数返回值

不仅函数的参数可以有名字,也可以给函数的返回值命名。

举例代码如下:

func Find(m map[int]int, key int)(value int, ok bool) {
    value,ok = m[key]
    return
}

如果返回值命名了,可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值,举例代码如下:

func mian() {
    for i := 0 ; i<3; i++ {
        defer func() { println(i) }
    }
}
​
// 该函数最终的输出为:
// 3
// 3
// 3

以上代码中如果没有defer其实返回值就是0,1,2,但defer语句会在函数return之后才会执行,也就是或只有以上函数在执行结束return之后才会执行defer语句,而该函数return时的i值将会达到3,所以最终的defer语句执行printlin的输出都是3。

defer语句延迟执行的其实是一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。

这种方式往往会带来一些问题,修复方法为在每一轮迭代中都为defer函数提供一个独有的变量,修改代码如下:

func main() {
    for i := 0; i < 3; i++ {
        i := i // 定义一个循环体内局部变量i
        defer func(){ println(i) } ()
    }
}
​
func main() {
    for i := 0; i < 3; i++ {
        // 通过函数传入i
        // defer 语句会马上对调用参数求值
        // 不再捕获,而是直接传值
        defer func(i int){ println(i) } (i)
    }
}

3.7.5 递归调用

Go语言中,函数还可以直接或间接地调用自己,也就是支持递归调用。Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。这部分的知识将会涉及goroutint和动态栈的相关知识,我们将会在之后的博文中向大家解释。

它的语法和c很相似,格式如下:

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

完整代码

package main
​
import "fmt"func add(a int, b int) int {
    return a + b
}
​
func add2(a, b int) int {
    return a + b
}
​
/*
这个函数 exists 接受一个字符串到字符串的映射 m 和一个键值 k,然后返回相应的值 v 和一个布尔值 ok,表示键是否存在于映射中
你可以使用这个函数来判断一个键是否存在于一个 map 中,并获取对应的值。如果该键存在于 map 中,ok 的值将为 true,否则为 false。如果键存在于 map 中,你可以使用 v 来访问与该键关联的值。
*/
func exists(m map[string]string, k string) (v string, ok bool) { //返回两值,一个是真正的值,另一个是是否存在
    v, ok = m[k]
    return v, ok
}
​
func main() {
    res := add(1, 2)
    fmt.Println(res)
​
    v, ok := exists(map[string]string{"a": "A"}, "a")
    fmt.Println(v, ok) // A TRUE
}
​

八、指针

与C相同,Go语言让程序员决定何时使用指针。变量其实是一种使用方便的占位符,用于引用计算机内存地址。Go 语言中的的取地址符是 & ,放到一个变量前使用就会返回相应变量的内存地址。

指针变量其实就是用于存放某一个对象的内存地址。

和基础类型数据相同,在使用指针变量之前我们首先需要申明指针,声明格式如下:

var var_name *var-type

其中的var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。

代码举例如下:

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

指针的初始化就是取出相对应的变量地址对指针进行赋值,具体如下:

   var a int= 20   /* 声明实际变量 */
   var ip *int        /* 声明指针变量 */
​
   ip = &a  /* 指针变量的存储地址 */

1.1空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil,也称为空指针。它概念上和其它语言的null、NULL一样,都指代零值或空值。

完整代码:

package main
​
import "fmt"func add3(n int) {
    n += 2
}
​
func add2ptr(p *int) {
    *p += 2
}
​
func main() {
    n := 5
    add3(n)
    fmt.Println(n) //5
    add2ptr(&n)
    fmt.Println(n) //7
}
​