3.Go语言数据类型
这是接着Go语言学习笔记3讲的一篇,还是主要介绍Go语言数据类型。主要如下:
3.5 函数和方法
在Go语言中,函数类型是一等类型,可以把函数当做一个值来传递和使用。函数类型的值(简称为函数值)既可以作为其他函数的参数,也可以作为其他函数的结果(之一)。
3.5.1 类型表示法
函数类型指代了所有可以接受若干参数并能够返回若干结果的函数。
声明一个函数类型总会以关键字 func 作为开始,紧跟在关键字 func 之后的应该是这个函数的签名,包括了参数声明列表(在左边)和结果声明列表(在右边),两者用空格分隔。参数声明列表必须由圆括号括起来,多个参数声明之间需用逗号分隔。
参数声明是参数名称在前,参数类型在后,中间以空格分隔。如果有一个参数列表,除了一个名称为 name、类型为 string 的参数之外,还包括一个名称为 age 、类型为 int 的参数。参数列表如下:
(name string, age int)
注意:在同一个参数声明列表中的所有参数名称都必须是唯一的。
如果相邻的两个参数属于同一数据类型,那么我们只需要写一次参数类型。在上面的参数类型中添加一个名称为 level 、类型为 int 的参数:
(name string, age, level int)
这个就相当于:
(name string, age int, level int)
当然,这里甚至可以省略所有参数的名称。但是强烈不推荐这种做法,它的可读性很差。
现在向参数声明中添加一个名称为 informations 、类型为 …string 的可变长参数:
(name string, age int, level int, informations ...string)
注意:可变参数必须是参数列表中的最后一个。
函数类型声明的结果声明列表中一般包含若干个结果声明。结果声明列表的编写规则与参数声明基本一致。不过有两点区别:
-
只存在可变长参数的声明而不存在可变长结果的声明;
- 如果结果声明列表中只有一个结果声明且这个结果声明中并不包含结果的名称,那么就可以忽略它的圆括号。
如下, 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个结果的作用如下:
-
done : 用于表示数据是否被成功保存。
-
id : 数据被保存后的ID,此ID可以被用来检索数据。
- synchronized : 用于表示数据是否被同步到相关系统中。
这样该函数的调用法会更加清晰明了地获知具体的操作结果,处理这些操作的结果的代码也会更加简单和扁平化。
3.5.2 值表示法
函数类型的零值是nil。未初始化的函数类型的变量的值就是nil。函数类型的值分为两类:命名函数值和匿名函数值。在Go语言中,很多时候通常称命名函数值为命名函数,称匿名函数值为匿名函数,但是它们都是值的一种。
命名函数
命名函数的声明一般由关键字func、函数名称、函数签名(由参数声明列表和结果声明列表组成)和函数体组成。如果在函数的签名中包含了结果声明列表,那么在该函数的函数体中的任何可到达的流程分支的最后一条语句都必须是终止语句。终止语句有很多种,比如以关键字return或goto开始的语句、仅包含针对内建函数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 中的名称。
方法声明中的接收者声明有关的几条编写规则:
-
接收者声明中的类型必须是某个自定义的数据类型,或者是一个与某个自定义数据类型对应的指针类型。但不论接收者的类型是哪一种,接收者的基本类型都会是那个自定义数据类型。接收者的基本类型既不能是一个指针类型,也不能是一个接口类型。例如, 方法声明:
func (self *MyIntSlice) Min() (result int)//接收者的类是*MyIntSlice,而其基本类型是MyIntSlice.
-
接收者声明中的类型必须由非限定标识符代表。方法所属的数据类型的声明必须与该方法声明处在同一个代码包内。
-
接收者标识符不能是空标识符“_”, 并且必须在其所在的方法签名中是唯一的。
- 如果接收者的值(由接收者标识符代表)未在当前方法的方法体内被引用,那么我们就可以将这个接收者标识符从当前方法的接收者声明中删除掉。注意,这条不建议这么做,原因和函数声明中的参数声明类似,会使代码的可读性变差。
在Go语言中,常常把接收者类型是某个自定义数据类型的方法叫做该数据类型的值方法,而把接收者类型是某个自定义数据类型对应的指针类型的方法叫作该数据类型的指针方法。
对于一个接收者的基本类型来说,它所包含的方法的名称之间不能有重复。如果这个接收者的基本类型是一个结构体类型,还需要保证它包含的字段和方法的名称之间不能出现重复。
定义一个方法:
func (self *MyIntSlice) Min() (result int)
该方法的类型:
func Min() (self *MyIntSlice, result int)
注意:形如上述方法的类型表示的函数的值只能算是一个函数,而不能叫作方法。这样的函数并没有与任何自定义数据类型相关联。
在接收者的基本类型确定的情况下,如何在值方法和指针方法做出选择:
-
在某个自定义数据类型的值上,只能够调用与这个数据类型相关联的值方法,而在指向这个值的指针值上,却能够调用与其数据类型关联的值方法和指针方法。虽然自定义数据类型的方法集合中不包含与它关联的指针类型,但是我们仍能够通过这个类型的值调用它的指针方法,这里需要使用取地址符&。
- 在指针方法中一定能够改变接收者的值。而在值方法中,对接收者的值的改变对于该方法之外一般是无效的。以接收者标识符代表的接收者的值实际上也是当前方法所属的数据类型的当前值的一个复制品。对于值方法来说,由于这个接收者的值就是一个当前值的复制品,所以对它的改变并不会影响到当前值。而对于指针方法来说,这个接收者的值则是一个当前值的指针的复制品。依据这个指针对当前值修改,就等于直接对该值进行了改变。不过有个例外,当接收者的类型如果是引用类型的别名类型,那么在该类型值的值方法中对该值的改变也是对外有效的。
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个方法声明,分别是Len、Less、Swap 和 Sort。
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 的值为 false,ok3 的值为 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 的参数值,并利用这个值的方法Len、Less 和 Swap来修改其参数中的各个元素的位置以完成排序工作。对于 SortableStrings 类型,虽然它实现了接口类型 sort.Interface 中声明的全部方法,但是这些方法都是值方法,从而这些方法中对接收者值的改变并不会影响到它的源值,只是改变了源值的复制品而已。
对于上面的问题,目前的解决方案是将 SortableStrings 类型的方法Len、Less 和 Swap的接收者类型都改为 *SortableStrings,如下图展示的运行结果:
但这时的 SortableStrings 类型就不再是接口类型 sort.Interface 的实现,*SortableStrings 才是接口类型 sort.Interface 的实现,如上图中 ok 的值为 false。
现在我们再考虑一种方案,对 SortableStrings 类型的声明稍作改动:
type SortableStrings []string //去掉了方括号中的3
这个时候实际上是将 SortableStrings 有数组类型的别名类型改为了切片类型的别名类型,但是又使得现在与之相关的方法无法通过编译。主要的错误如下图:
上面显示的主要错误有两个,一是内建函数 len 的参数不能是指向切片值的指针类型值;二是索引表达式不能被应用在指向切片值的指针类型值上。
下面对于此的解决方法就是将方法Len、Less、Swap 和 Sort 的接收者类型都由*SortableStrings改回SortableStrings。这里是因为改动后的SortableStrings是切片类型,而切片类型是引用类型;对于引用类型来说,值方法对接收者值的改变也会反映在其源值上。如下图为修改过的结果:
亲们,对于上面的接口出现的代码,可以点击下载 Go源码文件,自己修改修改,好好体会体会接口的用法。只需要在自己的工作区的src目录中的任意包中(这些包有意义即可)放入以下源码文件,进入命令行该文件目录输入上面的命令即可,当然首先你的Go语言环境变量要配好。
本篇就聊到这里,下篇继续未完的Go语言数据类型…
最后附上知名的Go语言开源框架(每篇更新一个):
Docker: 一个软件部署解决方案,也是一个轻量级的应用容器框架。使用 Docker,我们可以轻松地打包、发布和运行任何应用。现在,Docker 已经成为了名副其实的 Go 语言杀手级应用框架。其官网:http://www.docker.com。非官方的中文网站 : http://www.docker.org.cn
有疑问加站长微信联系(非本文作者)