Go语言学习4-函数类型和接口类型

3.Go语言数据类型

这是接着Go语言学习笔记3讲的一篇,仍是主要介绍Go语言数据类型。主要以下:golang

3.5 函数和方法

在Go语言中,函数类型是一等类型,能够把函数当作一个值来传递和使用。函数类型的值(简称为函数值)既能够做为其余函数的参数,也能够做为其余函数的结果(之一)。算法

3.5.1 类型表示法

函数类型指代了全部能够接受若干参数并可以返回若干结果的函数。docker

声明一个函数类型总会以关键字 func 做为开始,紧跟在关键字 func 以后的应该是这个函数的签名,包括了参数声明列表(在左边)和结果声明列表(在右边),二者用空格分隔。参数声明列表必须由圆括号括起来,多个参数声明之间需用逗号分隔。编程

参数声明是参数名称在前,参数类型在后,中间以空格分隔。若是有一个参数列表,除了一个名称为 name、类型为 string 的参数以外,还包括一个名称为 age 、类型为 int 的参数。参数列表以下:数组

(name string, age int)

注意:在同一个参数声明列表中的全部参数名称都必须是惟一的。闭包

若是相邻的两个参数属于同一数据类型,那么咱们只须要写一次参数类型。在上面的参数类型中添加一个名称为 level 、类型为 int 的参数:框架

(name string, age, level int)

这个就至关于:ide

(name string, age int, level int)

固然,这里甚至能够省略全部参数的名称。可是强烈不推荐这种作法,它的可读性不好。函数式编程

如今向参数声明中添加一个名称为 informations 、类型为 …string 的可变长参数:函数

(name string, age int, level int, informations ...string)

注意:可变参数必须是参数列表中的最后一个。

函数类型声明的结果声明列表中通常包含若干个结果声明。结果声明列表的编写规则与参数声明基本一致。不过有两点区别:

  1. 只存在可变长参数的声明而不存在可变长结果的声明;

  2. 若是结果声明列表中只有一个结果声明且这个结果声明中并不包含结果的名称,那么就能够忽略它的圆括号。

以下, bool 就是这个函数类型的惟一结果的类型声明。该结果声明独自组成了该函数类型的结果声明列表。

func (name string, age int, level int, informations ...string) bool

若是咱们须要命名这个结果为 done,能够以下编写:

func (name string, age int, level int, informations ...string) (done bool)

注意:这时的结果声明列表必须被圆括号括起来了。命名的结果其名称能够做为附属于该函数类型声明的文档的一部分,方便其余阅读的人员了解其含义。

一个函数类型能够有一个结果声明的列表,这是由于Go语言的函数类型能够有多个结果,这是Go语言的先进特性之一。以下函数类型声明:

func (name string, age int, level int, informations ...string) (effected uint, err error)

为函数声明多个结果可让每一个结果的职责更加单一,这既易于理解又方便使用。如上能够利用这一特性将错误值做为结果(之一)返回给调用它的代码,而不是包错误抛出来,而后再不得不在调用它的地方编写若干代码来抓住这个错误。(有关Go语言的错误处理机制后续博文会详细讨论)

函数类型的多个结果声明能够从不一样的角度来体现函数的内部操做的结果。例如:

func (name string, age int, level int, informations ...string) (done bool, id uint, synchronized bool)

假设上面声明的函数类型专门用于保存某项数据,它的3个结果的做用以下:

  1. done : 用于表示数据是否被成功保存。

  2. id : 数据被保存后的ID,此ID能够被用来检索数据。

  3. synchronized : 用于表示数据是否被同步到相关系统中。

这样该函数的调用法会更加清晰明了地获知具体的操做结果,处理这些操做的结果的代码也会更加简单和扁平化。

3.5.2 值表示法

函数类型的零值是nil。未初始化的函数类型的变量的值就是nil。函数类型的值分为两类:命名函数值匿名函数值。在Go语言中,不少时候一般称命名函数值命名函数,称匿名函数值匿名函数,可是它们都是的一种。

命名函数

命名函数的声明通常由关键字func函数名称函数签名(由参数声明列表和结果声明列表组成)和函数体组成。若是在函数的签名中包含告终果声明列表,那么在该函数的函数体中的任何可到达的流程分支的最后一条语句都必须是终止语句终止语句有不少种,好比以关键字returngoto开始的语句、仅包含针对内建函数panic(用于产生一个运行时恐慌)的调用表达式的语句。

定义了一个用于取模运算的 Module 函数:

func Module(x, y int) int {
    return x % y
}

注意:在关键字 return 右边的结果必须在数量上与该函数的结果声明列表中的内容彻底一致,且在对应位置的结果的类型上存在可赋予的关系,不然将不能经过编译。

Module 函数的结果命名,例如:

func Module(x, y int) (result int){
    return x % y
}

为函数的结果命名会使它们能过以常规变量的形式存在,就像函数的参数那样。当结果被命名,它们在函数被调用时就会被初始化为对应的数据类型的零值。若是这样的函数的函数体中有一条不带任何参数的 return 语句,那么在执行到这条 return 语句的时候,做为结果的变量的当前值就会被返回给函数调用方。例如:

func Module(x, y int) (result int){
    result = x % y
    return 
}

如上面 Module 函数被调用时,变量 result 被初始化为 int 类型的零值 0。当该函数的函数体中的第一条语句被执行时,变量 result 被赋予了表达式 x % y 的结果值。当该函数体中的无参数的 return 语句被执行时,result 的当前值就会做为结果被返回给函数调用方。

知识点: Go语言命名函数的声明还能够省略掉函数体。这意味着,该函数会由外部程序(如汇编语言程序)实现,而不会由Go语言程序实现。

匿名函数

匿名函数由函数字面量表示。函数字面量也是表达式的一种。在声明的内容上,匿名函数与命名函数的区别也只是少了一个函数名称。以下匿名函数:

func (x, y int) (result int){
    result = x % y
    return 
}

函数字面量也能够看作是对某个函数类型的即时实现,它比函数类型声明多了一个函数体。一个函数字面量能够被赋给一个变量,也能够被直接调用。

3.5.3 属性和基本操做

函数做为Go语言的数据类型之一,能够把函数做为一个变量的类型。例如声明一个变量:

var recorder func (name string, age int, level int)(done bool)

声明事后,全部符合这个函数类型的实现均可以被赋给变量 recorder,以下:

recorder = func (name string, age int, level int) (done bool) {
    //省略若干实现语句
    return
}

注意:被赋给变量 recorder 的函数字面量必须与 recorder 的类型拥有相同的函数签名

能够在一个函数类型的变量上直接应用函数表达式来调用它,例如:

done := recorder("Huazie", 23, 1)

注意:被赋值的变量在数量上必须与函数的结果声明列表中的内容彻底一致,且对应位置的变量和结果的类型上存在可赋予的关系。一样适用于对命名函数进行调用并赋值的状况。

在函数字面量被编写出来的时候直接调用它,例如:

recorder = func (name string, age int, level int) (done bool) {
    //省略若干实现语句
    return
}(“Huazie”, 23, 1)

如上所示函数既然能够做为变量的值,那么也就能够像其余值同样在函数之间传递(即做为其余函数的参数或其余函数的结果)。

如今举出一个例子,如今要声明一个能够对一段文本进行加密的函数,同时,要求能够根据不一样的应用场景实时地、频繁地对加密算法进行变动。如上,咱们应该声明一个可以生成加密函数的函数,而后在程序运行期间,根据不一样的要求使用这个函数来生成须要的加密函数。此外,全部用于封装加密算法的函数都应该是同一个函数类型的,这有利于加密算法的无缝替换。

首先声明一个以下的函数类型:

type Encipher func(plaintext string) []byte

如上Encipher是函数类型 func(plaintext string) []byte 的别名,这个函数接收一个 string 类型的参数,而且返回一个元素类型为 byte 的切片类型的结果,这分别表明了一类比较通用的加密算法的输入数据和输出数据。

有了这个用于封装加密算法的函数类型以后,以下声明能够生成加密函数的函数:

func GenEncryptionFunc(encrypt Encipher) func(string) (ciphertext string) {
    return func(plaintext string) string {
        return fmt.Sprintf("%x", encrypt(plaintext))
    }
}

如上看着比较复杂的函数 GenEncryptionFunc 的签名中包括了一个参数声明和一个结果声明。其中,参数声明中的参数类型就是以前定义的用于封装加密算法的函数类型,结果声明表示了一个函数类型的结果。而这个函数类型正是 GenEncryptionFunc 函数所生成的加密函数的类型,它接收一个 string 类型的明文做为参考,并返回一个 string 类型的密文做为结果。

GenEncryptionFunc 函数的函数体内直接返回了复合加密函数类型的匿名函数。这个匿名函数的函数体内这一条语句首先调用了名称为 encrypt 的函数,对匿名函数的参数的明文加密;而后,它使用了标准库代码包 fmt 中的 Sprintf 函数,把 encrypt 函数的调用结果转换为字符串。该字符串的内容其实是用十六进制数表示的加密结果,而这个加密结果其实是 []byte 类型的。

每一次调用 GenEncryptionFunc 函数时,传递给他的那个加密算法函数都会一直被对应的加密函数引用这。只要生成的加密函数还能够被访问,其中的加密算法函数就会一直存在,而不会被Go语言的垃圾回收器回收。
理解GenEncryptionFunc函数所涉及到的一些概念:

这里写图片描述

知识点: 闭包这个词源自于经过“捕获”自由变量的绑定对函数文本执行的“闭合”动做。

只有当函数类型是一等类型而且其值能够做为其余函数的参数或结果的时候,才可以编写出实现闭包的代码。函数类型是Go语言支持函数式编程范式的重要体现,也就是咱们编写函数式风格代码的主要手段。函数还能够附属于任何自定义的数据类型,或者与接口类型和结构体类型相结合做为针对某个或某些数据类型的操做方法。

3.5.4 方法

方法就是附属于某个自定义的数据类型的函数。一个方法就是一个与某个接受者关联的函数。方法的声明中包含了关键字func接收者声明方法名称参数声明列表结果声明列表方法体。其中的接收者声明、参数声明列表和结果声明列表统称为方法签名,而方法体能够在某些状况下被忽略。例如:

type MyIntSlice []int
func (self MyIntSlice) Max() (result int) {
    //省略若干实现语句
    return
}

如上,咱们首先自定义了一个数据类型MyIntSlice,能够看作 []int 的别名类型。同时,这里还声明了一个方法。在这个名称为 Max 的方法中,接收者声明为(self MyIntSlice)。右边的标识符表示该方法所属的数据类型,即 MyIntSlice ; 左边的接收者标识符则表明了 MyIntSlice 类型的值在方法 Max 中的名称。

方法声明中的接收者声明有关的几条编写规则:

  1. 接收者声明中的类型必须是某个自定义的数据类型,或者是一个与某个自定义数据类型对应的指针类型。但不论接收者的类型是哪种,接收者的基本类型都会是那个自定义数据类型。接收者的基本类型既不能是一个指针类型,也不能是一个接口类型。例如, 方法声明:

    func (self *MyIntSlice) Min() (result int)//接收者的类是*MyIntSlice,而其基本类型是MyIntSlice.
  2. 接收者声明中的类型必须由非限定标识符表明。方法所属的数据类型的声明必须与该方法声明处在同一个代码包内。

  3. 接收者标识符不能是空标识符“_”, 而且必须在其所在的方法签名中是惟一的。

  4. 若是接收者的值(由接收者标识符表明)未在当前方法的方法体内被引用,那么咱们就能够将这个接收者标识符从当前方法的接收者声明中删除掉。注意,这条不建议这么作,缘由和函数声明中的参数声明相似,会使代码的可读性变差。

在Go语言中,经常把接收者类型是某个自定义数据类型的方法叫作该数据类型的值方法,而把接收者类型是某个自定义数据类型对应的指针类型的方法叫做该数据类型的指针方法

对于一个接收者的基本类型来讲,它所包含的方法的名称之间不能有重复。若是这个接收者的基本类型是一个结构体类型,还须要保证它包含的字段和方法的名称之间不能出现重复。

定义一个方法:

func (self *MyIntSlice) Min() (result int)

该方法的类型:

func Min() (self *MyIntSlice, result int)

注意:形如上述方法的类型表示的函数的值只能算是一个函数,而不能叫做方法。这样的函数并无与任何自定义数据类型相关联。

在接收者的基本类型肯定的状况下,如何在值方法和指针方法作出选择:

  1. 在某个自定义数据类型的值上,只可以调用与这个数据类型相关联的值方法,而在指向这个值的指针值上,却可以调用与其数据类型关联的值方法和指针方法。虽然自定义数据类型的方法集合中不包含与它关联的指针类型,可是咱们仍可以经过这个类型的值调用它的指针方法,这里须要使用取地址符&

  2. 在指针方法中必定可以改变接收者的值。而在值方法中,对接收者的值的改变对于该方法以外通常是无效的。以接收者标识符表明的接收者的值实际上也是当前方法所属的数据类型的当前值的一个复制品。对于值方法来讲,因为这个接收者的值就是一个当前值的复制品,因此对它的改变并不会影响到当前值。而对于指针方法来讲,这个接收者的值则是一个当前值的指针的复制品。依据这个指针对当前值修改,就等于直接对该值进行了改变。不过有个例外,当接收者的类型若是是引用类型的别名类型,那么在该类型值的值方法中对该值的改变也是对外有效的。

3.6 接口

一个Go语言的接口由一个方法的集合表明。只要一个数据类型(或与其对应的指针类型)附带的方法集合是某一个接口的方法集合的超集,那么就能够断定该类型实现了这个接口。

3.6.1 类型表示法

接口类型的声明由若干个方法的声明组成。方法的声明由方法名称方法签名构成。在一个接口类型的声明中不容许出现重复的方法名称。

接口类型是全部自定义的接口类型的统称。以标准库代码包 sort 中的接口类型 Interface 为例,声明以下:

type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(I, j int)
}

在Go语言中能够将一个接口类型嵌入到另外一个接口类型中。以下接口类型声明:

type Sortable interface {
    sort.Interface
    Sort()
}

如上接口类型 Sortable 实际包含了4个方法声明,分别是LenLessSwapSort

Go语言并不提供典型的类型驱动的子类化方法,可是却利用这种嵌入的方式实现了一样的效果。类型嵌入一样体现了非嵌入式的风格,一样适用于下面要讲的结构体类型。

注意:一个接口类型只接受其余接口类型的嵌入。

对于接口的嵌入,一个约束就是不能嵌入自身,包括直接嵌入间接嵌入

直接嵌入以下:

type Interface1 interface {
    Interface1
}

间接嵌入以下:

type Interface2 interface {
    Interface3
}

type Interface3 interface {
    Interface2
}

错误的接口嵌入会形成编译错误。另外,当前接口类型中声明的方法也不能与任何被嵌入其中的接口类型的方法重名,不然也会形成编译错误。

至于Go语言的自身定义的一个特殊的接口类型----空类型 interface{},前面也提到过,就是不包含任何方法声明的接口。而且,Go语言中全部数据类型都是它的实现。

3.6.2 值表示法

Go语言的接口类型没有相应的值表示法,由于接口是规范而不是实现。但一个接口类型的变量能够被赋予任何实现了这个接口类型的数据类型的值,所以接口类型的值能够由任何实现了这个接口类型的其余数据类型的值来表示。

3.6.3 属性和基本操做

接口的最基本属性就是它们的方法集合。

实现一个接口类型的能够是任何自定义的数据类型,只要这个数据类型附带的方法集合是该接口类型的方法集合的超集。编写一个自定义的数据类型 SortableStrings ,以下:

type SortableStrings [3]string

如上这个自定义的数据类型至关于 [3]string 类型的一个别名类型。如今想让这个自定义数据类型实现 sort.Interface 接口类型,就须要实现sort.Interface 中声明的所有方法,这些方法的实现都须要以类型 SortableStrings 为接收者的类型。这些方法的声明以下:

func (self SortableStrings) Len() int {
    return len(self)
}

func (self SortableStrings) Less(i, j int) bool {
    return self[i] < self[j]
}

func (self SortableStrings) Swap(i, j int) {
    self[i], self[j] = self[j], self[i]
}

有了上面三个方法的声明,SortableStrings类型就已是一个sort.Interface接口类型的实现了。使用 Go语言学习笔记2 中讲的类型断言表达式验证,编写代码以下:

_, ok := interface{}(SortableStrings{}).(sort.Interface)

注意: 想要让这条语句编译经过,首先须要导入标准代码包sort。

在如上赋值语句的右边是一个类型断言表达式,左边的两个标识符表明了这个表达式的求值结果。这里不关心转换后的结果,只关注类型转换是否成功,所以第一个标识符为空标识符“_”;第二个标识符 ok 表明了一个布尔类型的变量,true 表示转换成功,false 表示转换失败。以下图,显示 ok 的结果为 true,由于 SortableStrings 类型确实实现了接口类型 sort.Interface 中声明的全部方法。

这里写图片描述

一个接口类型能够被任意数量的数据类型实现。一个数据类型也能够同时实现多个接口类型。

如上的自定义数据类型 SortableStrings 也能够实现接口类型 Sortable,以下再编写一个方法声明:

func (self SortableStrings) Sort() {
    sort.Sort(self)
}

如今,SortableStrings 类型在实现了接口类型 sort.Interface 的同时也实现了接口类型 Sortable。类型断言表达式验证以下:

_, ok2 := interface{}(SortableStrings{}).(Sortable)

ok2的结果为true,以下图:

这里写图片描述

如今,把 SortableStrings 类型包含的 Sort 方法中的接收者类型由 SortableStrings 改成 *SortableStrings,以下:

func (self *SortableStrings) Sort() {
    sort.Sort(self)
}

这个函数的接收者类型改成了与 SortableStrings 类型对应的指针类型。方法Sort再也不是一个值方法了,已经变成了一个指针方法。只有与 SortableStrings 类型的值对应的指针值才可以经过上面的类型断言,以下:

_, ok3 := interface{}(&SortableStrings{}).(Sortable)

这时 ok2 的值为 falseok3 的值为 true,以下图:

这里写图片描述

再添加以下测试代码:

ss := SortableStrings("2", "3", "1")
ss.Sort()
fmt.Printf("Sortable strings: %v\n", ss)

以上出现的关于标准库代码包 fmt 的用法,亲们能够参考 http://docscn.studygolang.com/pkg/fmt

测试结果以下图:

这里写图片描述

上面打印的信息中的 [2, 3, 1] 是 SortableStrings 类型值的字符串表示,从上面的结果能够看见,变量 ss 的值并无排序,但在打印前已经调用了 Sort 方法。

下面且听解释:
: 上面讲到,在值方法中,对接收者的值的改变在该方法以外是不可见的。SortableStrings 类型的 Sort 方法其实是经过函数 sort.Sort 来对接收者的值进行排序的。sort.Sort 函数接受一个类型为 sort.Interface 的参数值,并利用这个值的方法LenLessSwap来修改其参数中的各个元素的位置以完成排序工做。对于 SortableStrings 类型,虽然它实现了接口类型 sort.Interface 中声明的所有方法,可是这些方法都是值方法,从而这些方法中对接收者值的改变并不会影响到它的源值,只是改变了源值的复制品而已。

对于上面的问题,目前的解决方案是将 SortableStrings 类型的方法LenLessSwap的接收者类型都改成 *SortableStrings,以下图展现的运行结果:

这里写图片描述

但这时的 SortableStrings 类型就再也不是接口类型 sort.Interface 的实现,*SortableStrings 才是接口类型 sort.Interface 的实现,如上图中 ok 的值为 false

如今咱们再考虑一种方案,对 SortableStrings 类型的声明稍做改动:

type SortableStrings []string //去掉了方括号中的3

这个时候其实是将 SortableStrings 有数组类型的别名类型改成了切片类型的别名类型,可是又使得如今与之相关的方法没法经过编译。主要的错误以下图:

这里写图片描述

上面显示的主要错误有两个,一是内建函数 len 的参数不能是指向切片值的指针类型值;二是索引表达式不能被应用在指向切片值的指针类型值上。

下面对于此的解决方法就是将方法LenLessSwapSort 的接收者类型都由*SortableStrings改回SortableStrings。这里是由于改动后的SortableStrings是切片类型,而切片类型是引用类型;对于引用类型来讲,值方法对接收者值的改变也会反映在其源值上。以下图为修改过的结果:

这里写图片描述

亲们,对于上面的接口出现的代码,能够点击下载 Go源码文件,本身修改修改,好好体会体会接口的用法。只须要在本身的工做区的src目录中的任意包中(这些包有意义便可)放入如下源码文件,进入命令行该文件目录输入上面的命令便可,固然首先你的Go语言环境变量要配好。

本篇就聊到这里,下篇继续未完的Go语言数据类型…

最后附上知名的Go语言开源框架(每篇更新一个):

Docker: 一个软件部署解决方案,也是一个轻量级的应用容器框架。使用 Docker,咱们能够轻松地打包、发布和运行任何应用。如今,Docker 已经成为了名副其实的 Go 语言杀手级应用框架。其官网:http://www.docker.com。非官方的中文网站 : http://www.docker.org.cn

相关文章
相关标签/搜索