第五天 | 青训营笔记

71 阅读8分钟

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

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

举例代码如下:

具名函数:就和c语言中的普通函数意义相同,具有函数名、返回值以及函数参数的函数。 func Add(a, b int) int { return a+b } 1 2 3 匿名函数:指不需要定义函数名的一种函数实现方式,它由一个不带函数名的函数声明和函数体组成。 var Add = func(a, b int) int { return a+b } 1 2 3 解释几个名词如下:

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

func fuction_name([parameter list])[return types]{ 函数体 } 1 2 3 解析 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...) } 1 2 3 4 5 6 7 8 9 以上当传入参数为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 } 1 2 3 4 如果返回值命名了,可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值,举例代码如下:

func mian() { for i := 0 ; i<3; i++ { defer func() { println(i) } } }

// 该函数最终的输出为: // 3 // 3 // 3 1 2 3 4 5 6 7 8 9 10 以上代码中如果没有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) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 3.7.5 递归调用 Go语言中,函数还可以直接或间接地调用自己,也就是支持递归调用。Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。这部分的知识将会涉及goroutint和动态栈的相关知识,我们将会在之后的博文中向大家解释。

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

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

func main() { recursion() } 1 2 3 4 5 6 7 3.8 方法 方法一般是面向对象编程(OOP)的一个特性,在C++语言中方法对应一个类对象的成员函数,是关联到具体对象上的虚表中的。但是Go语言的方法却是关联到类型的,这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

实现C语言中的一组函数如下:

// 文件对象 type File struct { fd int }

// 打开文件 func OpenFile(name string) (f *File, err error) { // ... }

// 关闭文件 func CloseFile(f *File) error { // ... }

// 读文件数据 func ReadFile(f *File, offset int64, data []byte) int { // ... }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 以上的三个函数都是普通的函数,需要占用包级空间中的名字资源。不过CloseFile和ReadFile函数只是针对File类型对象的操作,这时候我们更希望这类函数和操作对象的类型紧密绑定在一起。

所以在go语言中我们修改如下:

// 关闭文件 func (f *File) CloseFile() error { // ... }

// 读文件数据 func (f *File) ReadFile(offset int64, data []byte) int { // ... } 1 2 3 4 5 6 7 8 9 将CloseFile和ReadFile函数的第一个参数移动到函数名的开头,这两个函数就成了File类型独有的方法了(而不是File对象方法)

从代码角度看虽然只是一个小的改动,但是从编程哲学角度来看,Go语言已经是进入面向对象语言的行列了。我们可以给任何自定义类型添加一个或多个方法。每种类型对应的方法必须和类型的定义在同一个包中,因此是无法给int这类内置类型添加方法的(因为方法的定义和类型的定义不在一个包中)。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。

3.9 接口 3.9.1 什么是接口 Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

Go的接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。

所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。Go语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。

就比如说在c语言中,使用printf在终端输出的时候只能输出有限类型的几个变量,而在go中可以使用fmt.Printf,实际上是fmt.Fprintf向任意自定义的输出流对象打印,甚至可以打印到网络甚至是压缩文件,同时打印的数据不限于语言内置的基础类型,任意隐士满足fmt.Stringer接口的对象都可以打印,不满足fmt.Stringer接口的依然可以通过反射的技术打印。

3.9.2 结构体类型 interface实际上就是一个结构体,包含两个成员。其中一个成员是指向具体数据的指针,另一个成员中包含了类型信息。空接口和带方法的接口略有不同,下面分别是空接口的数据结构:

struct Eface { Type* type; void* data; }; 1 2 3 4 5 其中的Type指的是:

struct Type { uintptr size; uint32 hash; uint8 _unused; uint8 align; uint8 fieldAlign; uint8 kind; Alg *alg; void *gc; String *string; UncommonType *x; Type *ptrto; }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 和带方法的接口使用的数据结构: