大头先生的博客

第五章 函数

函数声明

函数声明:

语法如下:

func name(parameter-list) (result-list) {

  body
}

包括函数名、形式参数列表:参数名及参数类型、返回值列表:返回无名变量或无返回值时,可省略、以及函数体

函数的类型被称为函数的标识符。如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符。形参和返回值的变量名不影响函数标识符也不影响它们是否可以以省略参数类型的形式表示。

每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
实参通过值的方式传递,形参是实参的拷贝,因此在形参不是引用类型的时候,修改形参不会影响实参,但引用类型可能会对实参有影响。
没有函数声明体的函数,这表示函数不是以 Go 的方式实现的,这样的声明定义了函数标识符。

递归

大部分编程语言使用固定大小的函数调用栈,常见的大小从 64KB 到 2MB 不等。固定大小栈会限制递归的深度,当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。与相反, Go 语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。

多返回值

Go 语言的垃圾回收机制会回收不被使用的内存,但不包括系统层面的资源,比如打开的文件、网络连接等,所以必须显式释放这些资源。
如果一个函数将所有的返回值都显示的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。

错误

内置的 error 类型是接口类型
错误处理策略:
1.传播错误:函数中子程序失败会变成函数失败。
2.重新尝试失败的操作:错误发生是偶然性或不可预知的问题导致的。
3.输出错误信息并结束程序:发生错误后程序无法运行,一般用于main函数;库函数除非遇到内部程序包含不一致性,否则应该仅向上传播
4.输出错误信息,不需要中断程序的运行;通过log包提供函数或者标准错误流输出信息实现
5.直接忽略错误

函数值:
在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。
函数类型的零值是 nil; 函数值可以与 nil 比较;函数值之间不可用比较,也不能用函数值作为 map 的 key;
函数值使得我们不仅仅可以通过数据来参数化函数,亦可通过行为。

匿名函数:函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名
拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。
当匿名函数需要被递归调用时,我们必须首先声明一个变量(在上面的例子中,我们首先声明了 visitAll),再将匿名函数赋值给这个变量。

警告:捕获迭代变量!!!

可变参数

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。
可变参数函数将原始参数复制到一个数组中,然后再把数组的切片作为参数传递到被调用函数;若参数已经是切片,则参数后加省略符"..."
在可变参数函数内部,参数类型的行为看起来很像切片类型,但可变参数函数和以切片作为参数的函数是不同的。

Deferred函数

你只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要的语法。
当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。
你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。
defer 语句经常被用在处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁等。

Panic 异常

运行时错误:数组访问越界、空指针引用等会引起 panic 异常

Recover捕获异常

方法

Go 语言覆盖率面向对象编程的两个方面:封装和组合。
在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。
Go 语言中,可以给任意类型底层类型不是指针或 interface 的命名类型定义方法。
对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名,比如我们这里Point和Path就都有Distance这个名字的方法;
Go 中调用包外函数得带上包名。

基于指针对象的方法

什么时候考虑使用指针:
1.使用函数需要更新变量
2.函数的其中一个参数实在太大我们希望能够避免参数的拷贝
3.更新接收器的对象的方法,当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法

只有类型(Point)和指向他们的指针 (*Point) ,才是可能会出现在接收器声明里的两种接收器。
接收器实参类型为 T,接收器形参为 *T,编译器隐式取变量地址;
接收器实参类型为 *T,形参为 T,编译器解引用,取指针指向的实际变量。
其中:
1.如果命名类型 T 的所有方法都是用 T 类型来做接收器(非 *T),那么拷贝这种类型的实例就是安全的。
2.若方法使用指针作为接收器,则避免对其进行拷贝,这会破坏掉内部不变性,原因是拷贝对象和原始对象可能只是别名,对拷贝变量修改会产生意外的结果。
总结:

  1. 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
  2. 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。

通过嵌入结构体来扩展类型

1.外部结构体可以直接通过简写方式访问内部结构体的所有字段,并且可以把外部类型直接当做内部类型方法的接收器来调用,即使外部类型里没有声明这些方法。
2.在通过外部结构体调用内部结构体的方法时,需要指明调用实参参数的字段
3.如果内嵌匿名字段是命名类型的指针,则会将字段和方法引入当前的类型,这时可以通过外部类型变量的指针去取。
4.若一个结构体有多个匿名字段,则会拥有所有匿名字段类型的所有方法,以及直接定义在该结构体中的方法,同一级中出现两个同名方法时会报错。

方法值和方法表达式

1.选择一个方法并在同一个表达式里执行,例如 p.Distance(),但在 Go 中可以分两步执行
2.选择器:p.Distance,返回方法"值":将方法绑定到特定接收器变量的函数,这时不通过指定接收器就可以被调用,使用场景待确定
3.方法表达式:根据一个变量决定调用同一个类型的哪个函数时,使用方法表达式,其中要多传一个参数,第一个参数为方法接收器。

封装

Go 只有一种控制可见性的手段:大写首字母的标识符会从定义的包中导出,小写的字母不会。
基于名字的封装使得最小的封装单元是 package,而不是类型; struct 类型里的字段对同一个包内所有代码都具有可见性;
封装的优点:
1.调用方不能直接修改对象的变量值,只需要关注少量的语句并且明白少量变量的值
2.隐藏实现细节,防止调用方依赖的可能的变化的具体实现,使得设计包的程序员不破坏对外 API 情况下获得更大的自由
3.阻止外部调用方对对象的值进行任意的修改。

函数和方法区别:有没有接收器

posted on 2021-12-01 15:28  大头先生的博客  阅读(64)  评论(0编辑  收藏  举报

导航